From 61f68e9333206f0eebf15d5adab076a0ae7c4b71 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Wed, 12 Aug 2015 14:03:38 -0700 Subject: [PATCH] =?UTF-8?q?(=E3=81=A3=CB=98=E2=96=BD=CB=98)=E3=81=A3=20:cl?= =?UTF-8?q?oud:=20=E2=8A=82(=E2=97=95=E3=80=82=E2=97=95=E2=8A=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clang-format | 71 + .gitignore | 20 + .gitmodules | 6 + .slather.yml | 6 + .travis.yml | 20 + CONTRIBUTING.md | 64 + Configurations/BoltsSDK-OSX.xcconfig | 10 + Configurations/BoltsSDK-iOS.xcconfig | 10 + Configurations/Parse-OSX.xcconfig | 21 + Configurations/Parse-iOS.xcconfig | 20 + Configurations/ParseUnitTests-OSX.xcconfig | 25 + Configurations/ParseUnitTests-iOS.xcconfig | 25 + Configurations/Shared/Common.xcconfig | 21 + Configurations/Shared/Platform/OSX.xcconfig | 11 + Configurations/Shared/Platform/iOS.xcconfig | 21 + .../Shared/Product/Application.xcconfig | 13 + .../Shared/Product/Framework.xcconfig | 16 + .../Shared/Product/UnitTest.xcconfig | 14 + Configurations/Shared/Project/Debug.xcconfig | 16 + .../Shared/Project/Release.xcconfig | 18 + Configurations/Shared/Project/Test.xcconfig | 15 + Configurations/Shared/Warnings.xcconfig | 43 + Gemfile | 9 + Gemfile.lock | 80 + LICENSE | 30 + PATENTS | 33 + Parse-OSX.podspec | 41 + Parse.podspec | 33 + Parse.xcodeproj/project.pbxproj | 4427 ++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Parse-OSX.xcscheme | 113 + .../xcshareddata/xcschemes/Parse-iOS.xcscheme | 113 + Parse.xcworkspace/contents.xcworkspacedata | 38 + .../PFDefaultACLController.h | 50 + .../PFDefaultACLController.m | 99 + Parse/Internal/ACL/PFACLPrivate.h | 42 + Parse/Internal/ACL/State/PFACLState.h | 42 + Parse/Internal/ACL/State/PFACLState.m | 87 + Parse/Internal/ACL/State/PFACLState_Private.h | 21 + Parse/Internal/ACL/State/PFMutableACLState.h | 17 + Parse/Internal/ACL/State/PFMutableACLState.m | 32 + .../Controller/PFAnalyticsController.h | 58 + .../Controller/PFAnalyticsController.m | 82 + .../Internal/Analytics/PFAnalytics_Private.h | 21 + .../Utilities/PFAnalyticsUtilities.h | 28 + .../Utilities/PFAnalyticsUtilities.m | 38 + Parse/Internal/BFTask+Private.h | 71 + Parse/Internal/BFTask+Private.m | 134 + .../CloudCode/PFCloudCodeController.h | 45 + .../CloudCode/PFCloudCodeController.m | 66 + .../Commands/CommandRunner/PFCommandRunning.h | 98 + .../Commands/CommandRunner/PFCommandRunning.m | 12 + .../CommandRunner/PFCommandRunningConstants.h | 35 + .../CommandRunner/PFCommandRunningConstants.m | 23 + .../PFCommandURLRequestConstructor.h | 50 + .../PFCommandURLRequestConstructor.m | 148 + .../URLSession/PFURLSessionCommandRunner.h | 22 + .../URLSession/PFURLSessionCommandRunner.m | 240 + .../PFURLSessionCommandRunner_Private.h | 28 + .../URLSession/Session/PFURLSession.h | 62 + .../URLSession/Session/PFURLSession.m | 251 + .../URLSession/Session/PFURLSession_Private.h | 22 + .../PFURLSessionDataTaskDelegate.h | 31 + .../PFURLSessionDataTaskDelegate.m | 180 + .../PFURLSessionDataTaskDelegate_Private.h | 30 + .../PFURLSessionFileDownloadTaskDelegate.h | 39 + .../PFURLSessionFileDownloadTaskDelegate.m | 100 + .../PFURLSessionJSONDataTaskDelegate.h | 20 + .../PFURLSessionJSONDataTaskDelegate.m | 81 + .../PFURLSessionUploadTaskDelegate.h | 33 + .../PFURLSessionUploadTaskDelegate.m | 56 + .../Commands/PFRESTAnalyticsCommand.h | 31 + .../Commands/PFRESTAnalyticsCommand.m | 61 + Parse/Internal/Commands/PFRESTCloudCommand.h | 22 + Parse/Internal/Commands/PFRESTCloudCommand.m | 27 + Parse/Internal/Commands/PFRESTCommand.h | 45 + Parse/Internal/Commands/PFRESTCommand.m | 231 + .../Internal/Commands/PFRESTCommand_Private.h | 25 + Parse/Internal/Commands/PFRESTConfigCommand.h | 22 + Parse/Internal/Commands/PFRESTConfigCommand.m | 33 + Parse/Internal/Commands/PFRESTFileCommand.h | 21 + Parse/Internal/Commands/PFRESTFileCommand.m | 29 + .../Commands/PFRESTObjectBatchCommand.h | 22 + .../Commands/PFRESTObjectBatchCommand.m | 42 + Parse/Internal/Commands/PFRESTObjectCommand.h | 36 + Parse/Internal/Commands/PFRESTObjectCommand.m | 77 + Parse/Internal/Commands/PFRESTPushCommand.h | 23 + Parse/Internal/Commands/PFRESTPushCommand.m | 57 + Parse/Internal/Commands/PFRESTQueryCommand.h | 57 + Parse/Internal/Commands/PFRESTQueryCommand.m | 218 + .../Internal/Commands/PFRESTSessionCommand.h | 20 + .../Internal/Commands/PFRESTSessionCommand.m | 23 + Parse/Internal/Commands/PFRESTUserCommand.h | 56 + Parse/Internal/Commands/PFRESTUserCommand.m | 132 + .../Config/Controller/PFConfigController.h | 45 + .../Config/Controller/PFConfigController.m | 95 + .../Controller/PFCurrentConfigController.h | 39 + .../Controller/PFCurrentConfigController.m | 113 + Parse/Internal/Config/PFConfig_Private.h | 26 + .../FieldOperation/PFFieldOperation.h | 168 + .../FieldOperation/PFFieldOperation.m | 586 +++ .../FieldOperation/PFFieldOperationDecoder.h | 39 + .../FieldOperation/PFFieldOperationDecoder.m | 126 + .../File/Controller/PFFileController.h | 100 + .../File/Controller/PFFileController.m | 259 + Parse/Internal/File/PFFile_Private.h | 35 + Parse/Internal/File/State/PFFileState.h | 30 + Parse/Internal/File/State/PFFileState.m | 61 + .../Internal/File/State/PFFileState_Private.h | 18 + .../Internal/File/State/PFMutableFileState.h | 18 + .../Internal/File/State/PFMutableFileState.m | 18 + Parse/Internal/HTTPRequest/PFHTTPRequest.h | 24 + .../HTTPRequest/PFHTTPURLRequestConstructor.h | 19 + .../HTTPRequest/PFHTTPURLRequestConstructor.m | 52 + Parse/Internal/HTTPRequest/PFURLConstructor.h | 38 + Parse/Internal/HTTPRequest/PFURLConstructor.m | 130 + .../Constants/PFInstallationConstants.h | 21 + .../Constants/PFInstallationConstants.m | 21 + .../Controller/PFInstallationController.h | 31 + .../Controller/PFInstallationController.m | 105 + .../PFCurrentInstallationController.h | 49 + .../PFCurrentInstallationController.m | 289 + .../PFInstallationIdentifierStore.h | 39 + .../PFInstallationIdentifierStore.m | 118 + .../PFInstallationIdentifierStore_Private.h | 19 + .../Installation/PFInstallationPrivate.h | 32 + .../Internal/KeyValueCache/PFKeyValueCache.h | 47 + .../Internal/KeyValueCache/PFKeyValueCache.m | 270 + .../KeyValueCache/PFKeyValueCache_Private.h | 66 + .../OfflineQueryLogic/PFOfflineQueryLogic.h | 75 + .../OfflineQueryLogic/PFOfflineQueryLogic.m | 928 ++++ .../OfflineStore/PFOfflineStore.h | 197 + .../OfflineStore/PFOfflineStore.m | 1069 ++++ Parse/Internal/LocalDataStore/Pin/PFPin.h | 38 + Parse/Internal/LocalDataStore/Pin/PFPin.m | 81 + .../LocalDataStore/SQLite/PFSQLiteDatabase.h | 110 + .../LocalDataStore/SQLite/PFSQLiteDatabase.m | 341 ++ .../SQLite/PFSQLiteDatabaseController.h | 47 + .../SQLite/PFSQLiteDatabaseController.m | 79 + .../SQLite/PFSQLiteDatabaseResult.h | 70 + .../SQLite/PFSQLiteDatabaseResult.m | 168 + .../SQLite/PFSQLiteDatabase_Private.h | 20 + .../LocalDataStore/SQLite/PFSQLiteStatement.h | 30 + .../LocalDataStore/SQLite/PFSQLiteStatement.m | 49 + .../MultiProcessLock/PFMultiProcessFileLock.h | 26 + .../MultiProcessLock/PFMultiProcessFileLock.m | 105 + .../PFMultiProcessFileLockController.h | 36 + .../PFMultiProcessFileLockController.m | 88 + .../BatchController/PFObjectBatchController.h | 44 + .../BatchController/PFObjectBatchController.m | 129 + .../Object/Coder/File/PFObjectFileCoder.h | 38 + .../Object/Coder/File/PFObjectFileCoder.m | 44 + .../Coder/File/PFObjectFileCodingLogic.h | 33 + .../Coder/File/PFObjectFileCodingLogic.m | 58 + .../Object/Constants/PFObjectConstants.h | 23 + .../Object/Constants/PFObjectConstants.m | 22 + .../PFOfflineObjectController.h | 27 + .../PFOfflineObjectController.m | 86 + .../Object/Controller/PFObjectController.h | 34 + .../Object/Controller/PFObjectController.m | 128 + .../Controller/PFObjectController_Private.h | 22 + .../Object/Controller/PFObjectControlling.h | 54 + .../PFCurrentObjectControlling.h | 35 + .../EstimatedData/PFObjectEstimatedData.h | 44 + .../EstimatedData/PFObjectEstimatedData.m | 86 + .../PFObjectFilePersistenceController.h | 52 + .../PFObjectFilePersistenceController.m | 98 + .../LocalIdStore/PFObjectLocalIdStore.h | 49 + .../LocalIdStore/PFObjectLocalIdStore.m | 303 ++ .../Object/OperationSet/PFOperationSet.h | 69 + .../Object/OperationSet/PFOperationSet.m | 191 + Parse/Internal/Object/PFObjectPrivate.h | 294 ++ .../PinningStore/PFPinningObjectStore.h | 81 + .../PinningStore/PFPinningObjectStore.m | 163 + .../Object/State/PFMutableObjectState.h | 45 + .../Object/State/PFMutableObjectState.m | 61 + Parse/Internal/Object/State/PFObjectState.h | 60 + Parse/Internal/Object/State/PFObjectState.m | 179 + .../Object/State/PFObjectState_Private.h | 57 + .../Object/Subclassing/PFObjectSubclassInfo.h | 25 + .../Object/Subclassing/PFObjectSubclassInfo.m | 200 + .../PFObjectSubclassingController.h | 40 + .../PFObjectSubclassingController.m | 314 ++ .../Object/Utilities/PFObjectUtilities.h | 36 + .../Object/Utilities/PFObjectUtilities.m | 48 + Parse/Internal/PFAlertView.h | 22 + Parse/Internal/PFAlertView.m | 92 + Parse/Internal/PFApplication.h | 25 + Parse/Internal/PFApplication.m | 82 + Parse/Internal/PFAssert.h | 94 + Parse/Internal/PFAsyncTaskQueue.h | 24 + Parse/Internal/PFAsyncTaskQueue.m | 70 + Parse/Internal/PFBase64Encoder.h | 17 + Parse/Internal/PFBase64Encoder.m | 28 + Parse/Internal/PFBaseState.h | 76 + Parse/Internal/PFBaseState.m | 267 + Parse/Internal/PFBlockRetryer.h | 47 + Parse/Internal/PFBlockRetryer.m | 54 + Parse/Internal/PFCategoryLoader.h | 16 + Parse/Internal/PFCategoryLoader.m | 20 + Parse/Internal/PFCommandCache.h | 51 + Parse/Internal/PFCommandCache.m | 330 ++ Parse/Internal/PFCommandCache_Private.h | 16 + Parse/Internal/PFCommandResult.h | 34 + Parse/Internal/PFCommandResult.m | 43 + Parse/Internal/PFCoreDataProvider.h | 101 + Parse/Internal/PFCoreManager.h | 78 + Parse/Internal/PFCoreManager.m | 439 ++ Parse/Internal/PFDataProvider.h | 90 + Parse/Internal/PFDateFormatter.h | 55 + Parse/Internal/PFDateFormatter.m | 118 + Parse/Internal/PFDecoder.h | 49 + Parse/Internal/PFDecoder.m | 194 + Parse/Internal/PFDevice.h | 24 + Parse/Internal/PFDevice.m | 122 + Parse/Internal/PFEncoder.h | 63 + Parse/Internal/PFEncoder.m | 250 + Parse/Internal/PFErrorUtilities.h | 45 + Parse/Internal/PFErrorUtilities.m | 48 + Parse/Internal/PFEventuallyPin.h | 74 + Parse/Internal/PFEventuallyPin.m | 181 + Parse/Internal/PFEventuallyQueue.h | 91 + Parse/Internal/PFEventuallyQueue.m | 497 ++ Parse/Internal/PFEventuallyQueue_Private.h | 139 + Parse/Internal/PFFileManager.h | 72 + Parse/Internal/PFFileManager.m | 356 ++ Parse/Internal/PFGeoPointPrivate.h | 37 + Parse/Internal/PFHash.h | 21 + Parse/Internal/PFHash.m | 73 + Parse/Internal/PFInternalUtils.h | 82 + Parse/Internal/PFInternalUtils.m | 303 ++ Parse/Internal/PFJSONSerialization.h | 50 + Parse/Internal/PFJSONSerialization.m | 47 + Parse/Internal/PFKeychainStore.h | 37 + Parse/Internal/PFKeychainStore.m | 200 + Parse/Internal/PFLocationManager.h | 56 + Parse/Internal/PFLocationManager.m | 167 + Parse/Internal/PFLogger.h | 51 + Parse/Internal/PFLogger.m | 105 + Parse/Internal/PFLogging.h | 44 + Parse/Internal/PFMacros.h | 125 + Parse/Internal/PFMulticastDelegate.h | 31 + Parse/Internal/PFMulticastDelegate.m | 39 + Parse/Internal/PFNetworkCommand.h | 47 + Parse/Internal/PFPinningEventuallyQueue.h | 22 + Parse/Internal/PFPinningEventuallyQueue.m | 327 ++ Parse/Internal/PFReachability.h | 60 + Parse/Internal/PFReachability.m | 211 + Parse/Internal/PFTaskQueue.h | 27 + Parse/Internal/PFTaskQueue.m | 50 + Parse/Internal/PFWeakValue.h | 18 + Parse/Internal/PFWeakValue.m | 26 + Parse/Internal/ParseInternal.h | 37 + Parse/Internal/ParseManager.h | 97 + Parse/Internal/ParseManager.m | 456 ++ Parse/Internal/ParseModule.h | 26 + Parse/Internal/ParseModule.m | 134 + Parse/Internal/Parse_Private.h | 29 + Parse/Internal/Product/PFProduct+Private.h | 46 + .../PFProductsRequestHandler.h | 35 + .../PFProductsRequestHandler.m | 92 + Parse/Internal/PropertyInfo/PFPropertyInfo.h | 50 + Parse/Internal/PropertyInfo/PFPropertyInfo.m | 189 + .../PropertyInfo/PFPropertyInfo_Private.h | 25 + .../PropertyInfo/PFPropertyInfo_Runtime.h | 44 + .../PropertyInfo/PFPropertyInfo_Runtime.m | 122 + .../Controller/PFPurchaseController.h | 55 + .../Controller/PFPurchaseController.m | 227 + .../PFPaymentTransactionObserver.h | 27 + .../PFPaymentTransactionObserver.m | 105 + .../PFPaymentTransactionObserver_Private.h | 19 + .../PFPushChannelsController.h | 45 + .../PFPushChannelsController.m | 120 + .../Push/Controller/PFPushController.h | 47 + .../Push/Controller/PFPushController.m | 57 + Parse/Internal/Push/Manager/PFPushManager.h | 41 + Parse/Internal/Push/Manager/PFPushManager.m | 95 + Parse/Internal/Push/PFPushPrivate.h | 47 + .../Internal/Push/State/PFMutablePushState.h | 32 + .../Internal/Push/State/PFMutablePushState.m | 34 + Parse/Internal/Push/State/PFPushState.h | 37 + Parse/Internal/Push/State/PFPushState.m | 60 + .../Internal/Push/State/PFPushState_Private.h | 26 + .../Internal/Push/Utilites/PFPushUtilities.h | 20 + .../Internal/Push/Utilites/PFPushUtilities.m | 91 + .../Controller/PFCachedQueryController.h | 25 + .../Controller/PFCachedQueryController.m | 208 + .../Controller/PFOfflineQueryController.h | 31 + .../Controller/PFOfflineQueryController.m | 180 + .../Query/Controller/PFQueryController.h | 100 + .../Query/Controller/PFQueryController.m | 160 + Parse/Internal/Query/PFQueryPrivate.h | 60 + .../Query/State/PFMutableQueryState.h | 81 + .../Query/State/PFMutableQueryState.m | 177 + Parse/Internal/Query/State/PFQueryState.h | 69 + Parse/Internal/Query/State/PFQueryState.m | 88 + .../Query/State/PFQueryState_Private.h | 61 + .../Query/Utilities/PFQueryUtilities.h | 42 + .../Query/Utilities/PFQueryUtilities.m | 536 ++ Parse/Internal/Relation/PFRelationPrivate.h | 31 + .../Relation/State/PFMutableRelationState.h | 19 + .../Relation/State/PFMutableRelationState.m | 62 + .../Internal/Relation/State/PFRelationState.h | 30 + .../Internal/Relation/State/PFRelationState.m | 65 + .../Relation/State/PFRelationState_Private.h | 28 + .../Session/Controller/PFSessionController.h | 37 + .../Session/Controller/PFSessionController.m | 60 + Parse/Internal/Session/PFSession_Private.h | 28 + .../Session/Utilities/PFSessionUtilities.h | 24 + .../Session/Utilities/PFSessionUtilities.m | 22 + Parse/Internal/ThreadSafety/PFThreadsafety.h | 13 + Parse/Internal/ThreadSafety/PFThreadsafety.m | 33 + .../PFUserAuthenticationController.h | 56 + .../PFUserAuthenticationController.m | 138 + .../PFAnonymousAuthenticationProvider.h | 25 + .../PFAnonymousAuthenticationProvider.m | 46 + .../Anonymous/PFAnonymousUtils_Private.h | 21 + .../Providers/PFAuthenticationProvider.h | 48 + .../User/Coder/File/PFUserFileCodingLogic.h | 18 + .../User/Coder/File/PFUserFileCodingLogic.m | 60 + .../Internal/User/Constants/PFUserConstants.h | 15 + .../Internal/User/Constants/PFUserConstants.m | 15 + .../User/Controller/PFUserController.h | 61 + .../User/Controller/PFUserController.m | 164 + .../PFCurrentUserController.h | 56 + .../PFCurrentUserController.m | 364 ++ Parse/Internal/User/PFUserPrivate.h | 81 + .../Internal/User/State/PFMutableUserState.h | 19 + .../Internal/User/State/PFMutableUserState.m | 20 + Parse/Internal/User/State/PFUserState.h | 26 + Parse/Internal/User/State/PFUserState.m | 64 + .../Internal/User/State/PFUserState_Private.h | 25 + Parse/OSX/ParseOSX.h | 10 + Parse/PFACL.h | 268 + Parse/PFACL.m | 370 ++ Parse/PFAnalytics.h | 170 + Parse/PFAnalytics.m | 87 + Parse/PFAnonymousUtils.h | 85 + Parse/PFAnonymousUtils.m | 59 + Parse/PFCloud.h | 94 + Parse/PFCloud.m | 58 + Parse/PFConfig.h | 108 + Parse/PFConfig.m | 113 + Parse/PFConstants.h | 426 ++ Parse/PFConstants.m | 22 + Parse/PFFile.h | 392 ++ Parse/PFFile.m | 509 ++ Parse/PFGeoPoint.h | 118 + Parse/PFGeoPoint.m | 198 + Parse/PFInstallation.h | 119 + Parse/PFInstallation.m | 312 ++ Parse/PFNetworkActivityIndicatorManager.h | 70 + Parse/PFNetworkActivityIndicatorManager.m | 140 + Parse/PFNullability.h | 47 + Parse/PFObject+Subclass.h | 130 + Parse/PFObject.h | 1420 +++++ Parse/PFObject.m | 2749 ++++++++++ Parse/PFProduct.h | 68 + Parse/PFProduct.m | 48 + Parse/PFPurchase.h | 90 + Parse/PFPurchase.m | 88 + Parse/PFPush.h | 530 ++ Parse/PFPush.m | 461 ++ Parse/PFQuery.h | 893 ++++ Parse/PFQuery.m | 1133 ++++ Parse/PFRelation.h | 64 + Parse/PFRelation.m | 236 + Parse/PFRole.h | 106 + Parse/PFRole.m | 100 + Parse/PFSession.h | 55 + Parse/PFSession.m | 106 + Parse/PFSubclassing.h | 91 + Parse/PFUser.h | 459 ++ Parse/PFUser.m | 1225 +++++ Parse/Parse.h | 210 + Parse/Parse.m | 226 + Parse/Resources/Framework.plist | 29 + Parse/Resources/FrameworkOSX.plist | 22 + Parse/Resources/Localizable.strings | Bin 0 -> 112 bytes ParseStarterProject/.gitignore | 1 + .../project.pbxproj | 397 ++ .../contents.xcworkspacedata | 7 + .../ParseOSXStarterProject-Swift.xcscheme | 116 + .../ParseOSXStarterProject/AppDelegate.swift | 73 + .../Resources/Base.lproj/MainMenu.xib | 680 +++ .../AppIcon.appiconset/Contents.json | 58 + .../Resources/Info.plist | 32 + .../project.pbxproj | 457 ++ .../contents.xcworkspacedata | 7 + .../xcschemes/ParseOSXStarterProject.xcscheme | 116 + .../ParseOSXStarterProject/AppDelegate.h | 18 + .../ParseOSXStarterProject/AppDelegate.m | 76 + .../ParseOSXStarterProject/main.m | 15 + .../Resources/Info.plist | 32 + .../Resources/en.lproj/Credits.rtf | 29 + .../Resources/en.lproj/InfoPlist.strings | 1 + .../Resources/en.lproj/MainMenu.xib | 4666 +++++++++++++++++ .../project.pbxproj | 430 ++ .../contents.xcworkspacedata | 7 + .../ParseStarterProject-Swift.xcscheme | 102 + .../ParseStarterProject/AppDelegate.swift | 137 + .../ParseStarterProject/ViewController.swift | 24 + .../Resources/Base.lproj/Main.storyboard | 25 + .../AppIcon.appiconset/Contents.json | 68 + .../LaunchImage.launchimage/Contents.json | 49 + .../Resources/Info.plist | 45 + .../project.pbxproj | 463 ++ .../contents.xcworkspacedata | 7 + .../xcschemes/ParseStarterProject.xcscheme | 102 + .../ParseStarterProjectAppDelegate.h | 20 + .../ParseStarterProjectAppDelegate.m | 146 + .../ParseStarterProjectViewController.h | 14 + .../ParseStarterProjectViewController.m | 36 + .../ParseStarterProject/main.m | 20 + .../Resources/Default-568h@2x.png | Bin 0 -> 17091 bytes .../ParseStarterProject/Resources/Default.png | Bin 0 -> 5761 bytes .../Resources/Default@2x.png | Bin 0 -> 12280 bytes .../ParseStarterProject/Resources/Info.plist | 38 + .../Resources/en.lproj/InfoPlist.strings | 1 + .../Resources/en.lproj/MainWindow.xib | 444 ++ .../ParseStarterProjectViewController.xib | 190 + README.md | 99 + Rakefile | 313 ++ Scripts/build_third_party.sh | 64 + Scripts/xctask/build_framework_task.rb | 140 + Scripts/xctask/build_task.rb | 132 + Tests/Other/Cache/TestCache.h | 26 + Tests/Other/Cache/TestCache.m | 58 + .../PFExtensionDataSharingTestHelper.h | 20 + .../PFExtensionDataSharingTestHelper.m | 88 + Tests/Other/FileManager/TestFileManager.h | 27 + Tests/Other/FileManager/TestFileManager.m | 167 + .../CLLocationManager+TestAdditions.h | 27 + .../CLLocationManager+TestAdditions.m | 92 + .../Other/NetworkMocking/PFMockURLProtocol.h | 28 + .../Other/NetworkMocking/PFMockURLProtocol.m | 194 + .../Other/NetworkMocking/PFMockURLResponse.h | 40 + .../Other/NetworkMocking/PFMockURLResponse.m | 74 + Tests/Other/OCMock/OCMock+Parse.h | 16 + Tests/Other/OCMock/OCMock+Parse.m | 31 + .../StoreKitMocking/PFTestSKPaymentQueue.h | 16 + .../StoreKitMocking/PFTestSKPaymentQueue.m | 85 + .../PFTestSKPaymentTransaction.h | 26 + .../PFTestSKPaymentTransaction.m | 33 + Tests/Other/StoreKitMocking/PFTestSKProduct.h | 19 + Tests/Other/StoreKitMocking/PFTestSKProduct.m | 40 + .../StoreKitMocking/PFTestSKProductsRequest.h | 16 + .../StoreKitMocking/PFTestSKProductsRequest.m | 69 + .../PFTestSKProductsResponse.h | 17 + .../PFTestSKProductsResponse.m | 41 + .../ParseUnitTests-OSX-Bridging-Header.h | 11 + .../ParseUnitTests-iOS-Bridging-Header.h | 11 + Tests/Other/Swift/SwiftSubclass.swift | 23 + Tests/Other/Swizzling/PFTestSwizzledMethod.h | 27 + Tests/Other/Swizzling/PFTestSwizzledMethod.m | 71 + .../Swizzling/PFTestSwizzlingUtilities.h | 34 + .../Swizzling/PFTestSwizzlingUtilities.m | 50 + Tests/Other/TestCases/TestCase/PFTestCase.h | 113 + Tests/Other/TestCases/TestCase/PFTestCase.m | 184 + .../TestCases/UnitTestCase/PFUnitTestCase.h | 28 + .../TestCases/UnitTestCase/PFUnitTestCase.m | 50 + Tests/Resources/ParseUnitTests-OSX-Info.plist | 56 + Tests/Resources/ParseUnitTests-iOS-Info.plist | 56 + Tests/Unit/ACLStateTests.m | 72 + Tests/Unit/ACLUnitTests.m | 263 + Tests/Unit/AlertViewTests.m | 170 + Tests/Unit/AnalyticsCommandTests.m | 69 + Tests/Unit/AnalyticsControllerTests.m | 142 + Tests/Unit/AnalyticsUnitTests.m | 184 + Tests/Unit/AnalyticsUtilitiesTests.m | 43 + .../AnonymousAuthenticationProviderTests.m | 77 + Tests/Unit/AnonymousUtilsTests.m | 125 + Tests/Unit/BaseStateTests.m | 311 ++ Tests/Unit/BlockRetryerTests.m | 51 + Tests/Unit/CloudCodeControllerTests.m | 101 + Tests/Unit/CloudCommandTests.m | 40 + Tests/Unit/CloudUnitTests.m | 176 + Tests/Unit/CommandResultTests.m | 41 + .../Unit/CommandURLRequestConstructorTests.m | 147 + Tests/Unit/CommandUnitTests.m | 123 + Tests/Unit/ConfigCommandTests.m | 39 + Tests/Unit/ConfigControllerTests.m | 82 + Tests/Unit/ConfigUnitTests.m | 115 + Tests/Unit/CurrentConfigControllerTests.m | 190 + Tests/Unit/DateFormatterTests.m | 61 + Tests/Unit/DecoderTests.m | 256 + Tests/Unit/DefaultACLControllerTests.m | 148 + Tests/Unit/DeviceTests.m | 57 + Tests/Unit/ExtensionDataSharingMobileTests.m | 72 + Tests/Unit/ExtensionDataSharingTests.m | 218 + Tests/Unit/FieldOperationDecoderTests.m | 161 + Tests/Unit/FieldOperationTests.m | 401 ++ Tests/Unit/FileCommandTests.m | 29 + Tests/Unit/FileControllerTests.m | 522 ++ Tests/Unit/FileStateTests.m | 115 + Tests/Unit/FileUnitTests.m | 451 ++ Tests/Unit/GeoPointLocationTests.m | 126 + Tests/Unit/GeoPointUnitTests.m | 243 + Tests/Unit/HashTests.m | 36 + Tests/Unit/IncrementUnitTests.m | 27 + Tests/Unit/InstallationIdentifierUnitTests.m | 59 + Tests/Unit/InstallationUnitTests.m | 46 + Tests/Unit/KeyValueCacheTests.m | 177 + Tests/Unit/KeychainStoreTests.m | 167 + Tests/Unit/LocationManagerMobileTests.m | 138 + Tests/Unit/LocationManagerTests.m | 127 + Tests/Unit/ObjectBatchCommandTests.m | 83 + Tests/Unit/ObjectBatchControllerTests.m | 175 + Tests/Unit/ObjectCommandTests.m | 134 + Tests/Unit/ObjectEstimatedDataTests.m | 100 + Tests/Unit/ObjectFileCoderTests.m | 47 + Tests/Unit/ObjectFileCodingLogicTests.m | 71 + .../ObjectFilePersistenceControllerTests.m | 113 + Tests/Unit/ObjectLocalIdStoreTests.m | 149 + Tests/Unit/ObjectOfflineTests.m | 99 + Tests/Unit/ObjectPinTests.m | 540 ++ Tests/Unit/ObjectStateTests.m | 223 + Tests/Unit/ObjectSubclassPropertiesTests.m | 324 ++ Tests/Unit/ObjectSubclassTests.m | 173 + Tests/Unit/ObjectSubclassingControllerTests.m | 422 ++ Tests/Unit/ObjectUnitTests.m | 186 + Tests/Unit/ObjectUtilitiesTests.m | 49 + Tests/Unit/OfflineQueryControllerTests.m | 540 ++ Tests/Unit/OfflineQueryLogicUnitTests.m | 1570 ++++++ Tests/Unit/OperationSetUnitTests.m | 129 + Tests/Unit/ParseModuleUnitTests.m | 84 + Tests/Unit/ParseSetupUnitTests.m | 48 + Tests/Unit/PinUnitTests.m | 52 + Tests/Unit/PinningObjectStoreTests.m | 311 ++ Tests/Unit/ProductTests.m | 26 + Tests/Unit/PropertyInfoTests.m | 113 + Tests/Unit/PurchaseControllerTests.m | 312 ++ Tests/Unit/PurchaseUnitTests.m | 173 + Tests/Unit/PushChannelsControllerTests.m | 259 + Tests/Unit/PushCommandTests.m | 102 + Tests/Unit/PushControllerTests.m | 77 + Tests/Unit/PushManagerTests.m | 100 + Tests/Unit/PushMobileTests.m | 57 + Tests/Unit/PushStateTests.m | 128 + Tests/Unit/PushUnitTests.m | 584 +++ Tests/Unit/QueryCachedControllerTests.m | 424 ++ Tests/Unit/QueryControllerUnitTests.m | 187 + Tests/Unit/QueryPredicateUnitTests.m | 121 + Tests/Unit/QueryStateUnitTests.m | 232 + Tests/Unit/QueryUnitTests.m | 1359 +++++ Tests/Unit/QueryUtilitiesTests.m | 121 + Tests/Unit/RelationStateTests.m | 91 + Tests/Unit/RelationUnitTests.m | 174 + Tests/Unit/RoleUnitTests.m | 78 + Tests/Unit/SQLiteDatabaseTest.m | 600 +++ Tests/Unit/SessionControllerTests.m | 140 + Tests/Unit/SessionUnitTests.m | 163 + Tests/Unit/SessionUtilitiesTests.m | 35 + Tests/Unit/URLSessionCommandRunnerTests.m | 247 + Tests/Unit/URLSessionDataTaskDelegateTests.m | 211 + Tests/Unit/URLSessionTests.m | 503 ++ .../Unit/URLSessionUploadTaskDelegateTests.m | 286 + Tests/Unit/UserCommandTests.m | 142 + Tests/Unit/UserControllerTests.m | 283 + Tests/Unit/UserFileCodingLogicTests.m | 71 + Tests/Unit/UserUnitTests.m | 43 + Tests/testServer.config | 0 Vendor/Bolts-ObjC | 1 + Vendor/OCMock | 1 + third_party_licenses.txt | 68 + 565 files changed, 77605 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .slather.yml create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 Configurations/BoltsSDK-OSX.xcconfig create mode 100644 Configurations/BoltsSDK-iOS.xcconfig create mode 100644 Configurations/Parse-OSX.xcconfig create mode 100644 Configurations/Parse-iOS.xcconfig create mode 100644 Configurations/ParseUnitTests-OSX.xcconfig create mode 100644 Configurations/ParseUnitTests-iOS.xcconfig create mode 100644 Configurations/Shared/Common.xcconfig create mode 100644 Configurations/Shared/Platform/OSX.xcconfig create mode 100644 Configurations/Shared/Platform/iOS.xcconfig create mode 100644 Configurations/Shared/Product/Application.xcconfig create mode 100644 Configurations/Shared/Product/Framework.xcconfig create mode 100644 Configurations/Shared/Product/UnitTest.xcconfig create mode 100644 Configurations/Shared/Project/Debug.xcconfig create mode 100644 Configurations/Shared/Project/Release.xcconfig create mode 100644 Configurations/Shared/Project/Test.xcconfig create mode 100644 Configurations/Shared/Warnings.xcconfig create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 PATENTS create mode 100644 Parse-OSX.podspec create mode 100644 Parse.podspec create mode 100644 Parse.xcodeproj/project.pbxproj create mode 100644 Parse.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Parse.xcodeproj/xcshareddata/xcschemes/Parse-OSX.xcscheme create mode 100644 Parse.xcodeproj/xcshareddata/xcschemes/Parse-iOS.xcscheme create mode 100644 Parse.xcworkspace/contents.xcworkspacedata create mode 100644 Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.h create mode 100644 Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.m create mode 100644 Parse/Internal/ACL/PFACLPrivate.h create mode 100644 Parse/Internal/ACL/State/PFACLState.h create mode 100644 Parse/Internal/ACL/State/PFACLState.m create mode 100644 Parse/Internal/ACL/State/PFACLState_Private.h create mode 100644 Parse/Internal/ACL/State/PFMutableACLState.h create mode 100644 Parse/Internal/ACL/State/PFMutableACLState.m create mode 100644 Parse/Internal/Analytics/Controller/PFAnalyticsController.h create mode 100644 Parse/Internal/Analytics/Controller/PFAnalyticsController.m create mode 100644 Parse/Internal/Analytics/PFAnalytics_Private.h create mode 100644 Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.h create mode 100644 Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.m create mode 100644 Parse/Internal/BFTask+Private.h create mode 100644 Parse/Internal/BFTask+Private.m create mode 100644 Parse/Internal/CloudCode/PFCloudCodeController.h create mode 100644 Parse/Internal/CloudCode/PFCloudCodeController.m create mode 100644 Parse/Internal/Commands/CommandRunner/PFCommandRunning.h create mode 100644 Parse/Internal/Commands/CommandRunner/PFCommandRunning.m create mode 100644 Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.h create mode 100644 Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.m create mode 100644 Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.m create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.m create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner_Private.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.m create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession_Private.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.m create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate_Private.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.m create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.m create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.h create mode 100644 Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.m create mode 100644 Parse/Internal/Commands/PFRESTAnalyticsCommand.h create mode 100644 Parse/Internal/Commands/PFRESTAnalyticsCommand.m create mode 100644 Parse/Internal/Commands/PFRESTCloudCommand.h create mode 100644 Parse/Internal/Commands/PFRESTCloudCommand.m create mode 100644 Parse/Internal/Commands/PFRESTCommand.h create mode 100644 Parse/Internal/Commands/PFRESTCommand.m create mode 100644 Parse/Internal/Commands/PFRESTCommand_Private.h create mode 100644 Parse/Internal/Commands/PFRESTConfigCommand.h create mode 100644 Parse/Internal/Commands/PFRESTConfigCommand.m create mode 100644 Parse/Internal/Commands/PFRESTFileCommand.h create mode 100644 Parse/Internal/Commands/PFRESTFileCommand.m create mode 100644 Parse/Internal/Commands/PFRESTObjectBatchCommand.h create mode 100644 Parse/Internal/Commands/PFRESTObjectBatchCommand.m create mode 100644 Parse/Internal/Commands/PFRESTObjectCommand.h create mode 100644 Parse/Internal/Commands/PFRESTObjectCommand.m create mode 100644 Parse/Internal/Commands/PFRESTPushCommand.h create mode 100644 Parse/Internal/Commands/PFRESTPushCommand.m create mode 100644 Parse/Internal/Commands/PFRESTQueryCommand.h create mode 100644 Parse/Internal/Commands/PFRESTQueryCommand.m create mode 100644 Parse/Internal/Commands/PFRESTSessionCommand.h create mode 100644 Parse/Internal/Commands/PFRESTSessionCommand.m create mode 100644 Parse/Internal/Commands/PFRESTUserCommand.h create mode 100644 Parse/Internal/Commands/PFRESTUserCommand.m create mode 100644 Parse/Internal/Config/Controller/PFConfigController.h create mode 100644 Parse/Internal/Config/Controller/PFConfigController.m create mode 100644 Parse/Internal/Config/Controller/PFCurrentConfigController.h create mode 100644 Parse/Internal/Config/Controller/PFCurrentConfigController.m create mode 100644 Parse/Internal/Config/PFConfig_Private.h create mode 100644 Parse/Internal/FieldOperation/PFFieldOperation.h create mode 100644 Parse/Internal/FieldOperation/PFFieldOperation.m create mode 100644 Parse/Internal/FieldOperation/PFFieldOperationDecoder.h create mode 100644 Parse/Internal/FieldOperation/PFFieldOperationDecoder.m create mode 100644 Parse/Internal/File/Controller/PFFileController.h create mode 100644 Parse/Internal/File/Controller/PFFileController.m create mode 100644 Parse/Internal/File/PFFile_Private.h create mode 100644 Parse/Internal/File/State/PFFileState.h create mode 100644 Parse/Internal/File/State/PFFileState.m create mode 100644 Parse/Internal/File/State/PFFileState_Private.h create mode 100644 Parse/Internal/File/State/PFMutableFileState.h create mode 100644 Parse/Internal/File/State/PFMutableFileState.m create mode 100644 Parse/Internal/HTTPRequest/PFHTTPRequest.h create mode 100644 Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.h create mode 100644 Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.m create mode 100644 Parse/Internal/HTTPRequest/PFURLConstructor.h create mode 100644 Parse/Internal/HTTPRequest/PFURLConstructor.m create mode 100644 Parse/Internal/Installation/Constants/PFInstallationConstants.h create mode 100644 Parse/Internal/Installation/Constants/PFInstallationConstants.m create mode 100644 Parse/Internal/Installation/Controller/PFInstallationController.h create mode 100644 Parse/Internal/Installation/Controller/PFInstallationController.m create mode 100644 Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.h create mode 100644 Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.m create mode 100644 Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.h create mode 100644 Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.m create mode 100644 Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore_Private.h create mode 100644 Parse/Internal/Installation/PFInstallationPrivate.h create mode 100644 Parse/Internal/KeyValueCache/PFKeyValueCache.h create mode 100644 Parse/Internal/KeyValueCache/PFKeyValueCache.m create mode 100644 Parse/Internal/KeyValueCache/PFKeyValueCache_Private.h create mode 100644 Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.h create mode 100644 Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m create mode 100644 Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.h create mode 100644 Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.m create mode 100644 Parse/Internal/LocalDataStore/Pin/PFPin.h create mode 100644 Parse/Internal/LocalDataStore/Pin/PFPin.m create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.h create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.m create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.h create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.m create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.h create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.m create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase_Private.h create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.h create mode 100644 Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.m create mode 100644 Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.h create mode 100644 Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.m create mode 100644 Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.h create mode 100644 Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.m create mode 100644 Parse/Internal/Object/BatchController/PFObjectBatchController.h create mode 100644 Parse/Internal/Object/BatchController/PFObjectBatchController.m create mode 100644 Parse/Internal/Object/Coder/File/PFObjectFileCoder.h create mode 100644 Parse/Internal/Object/Coder/File/PFObjectFileCoder.m create mode 100644 Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.h create mode 100644 Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.m create mode 100644 Parse/Internal/Object/Constants/PFObjectConstants.h create mode 100644 Parse/Internal/Object/Constants/PFObjectConstants.m create mode 100644 Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.h create mode 100644 Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.m create mode 100644 Parse/Internal/Object/Controller/PFObjectController.h create mode 100644 Parse/Internal/Object/Controller/PFObjectController.m create mode 100644 Parse/Internal/Object/Controller/PFObjectController_Private.h create mode 100644 Parse/Internal/Object/Controller/PFObjectControlling.h create mode 100644 Parse/Internal/Object/CurrentController/PFCurrentObjectControlling.h create mode 100644 Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.h create mode 100644 Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.m create mode 100644 Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.h create mode 100644 Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.m create mode 100644 Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.h create mode 100644 Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.m create mode 100644 Parse/Internal/Object/OperationSet/PFOperationSet.h create mode 100644 Parse/Internal/Object/OperationSet/PFOperationSet.m create mode 100644 Parse/Internal/Object/PFObjectPrivate.h create mode 100644 Parse/Internal/Object/PinningStore/PFPinningObjectStore.h create mode 100644 Parse/Internal/Object/PinningStore/PFPinningObjectStore.m create mode 100644 Parse/Internal/Object/State/PFMutableObjectState.h create mode 100644 Parse/Internal/Object/State/PFMutableObjectState.m create mode 100644 Parse/Internal/Object/State/PFObjectState.h create mode 100644 Parse/Internal/Object/State/PFObjectState.m create mode 100644 Parse/Internal/Object/State/PFObjectState_Private.h create mode 100644 Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.h create mode 100644 Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.m create mode 100644 Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h create mode 100644 Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m create mode 100644 Parse/Internal/Object/Utilities/PFObjectUtilities.h create mode 100644 Parse/Internal/Object/Utilities/PFObjectUtilities.m create mode 100644 Parse/Internal/PFAlertView.h create mode 100644 Parse/Internal/PFAlertView.m create mode 100644 Parse/Internal/PFApplication.h create mode 100644 Parse/Internal/PFApplication.m create mode 100644 Parse/Internal/PFAssert.h create mode 100644 Parse/Internal/PFAsyncTaskQueue.h create mode 100644 Parse/Internal/PFAsyncTaskQueue.m create mode 100644 Parse/Internal/PFBase64Encoder.h create mode 100644 Parse/Internal/PFBase64Encoder.m create mode 100644 Parse/Internal/PFBaseState.h create mode 100644 Parse/Internal/PFBaseState.m create mode 100644 Parse/Internal/PFBlockRetryer.h create mode 100644 Parse/Internal/PFBlockRetryer.m create mode 100644 Parse/Internal/PFCategoryLoader.h create mode 100644 Parse/Internal/PFCategoryLoader.m create mode 100644 Parse/Internal/PFCommandCache.h create mode 100644 Parse/Internal/PFCommandCache.m create mode 100644 Parse/Internal/PFCommandCache_Private.h create mode 100644 Parse/Internal/PFCommandResult.h create mode 100644 Parse/Internal/PFCommandResult.m create mode 100644 Parse/Internal/PFCoreDataProvider.h create mode 100644 Parse/Internal/PFCoreManager.h create mode 100644 Parse/Internal/PFCoreManager.m create mode 100644 Parse/Internal/PFDataProvider.h create mode 100644 Parse/Internal/PFDateFormatter.h create mode 100644 Parse/Internal/PFDateFormatter.m create mode 100644 Parse/Internal/PFDecoder.h create mode 100644 Parse/Internal/PFDecoder.m create mode 100644 Parse/Internal/PFDevice.h create mode 100644 Parse/Internal/PFDevice.m create mode 100644 Parse/Internal/PFEncoder.h create mode 100644 Parse/Internal/PFEncoder.m create mode 100644 Parse/Internal/PFErrorUtilities.h create mode 100644 Parse/Internal/PFErrorUtilities.m create mode 100644 Parse/Internal/PFEventuallyPin.h create mode 100644 Parse/Internal/PFEventuallyPin.m create mode 100644 Parse/Internal/PFEventuallyQueue.h create mode 100644 Parse/Internal/PFEventuallyQueue.m create mode 100644 Parse/Internal/PFEventuallyQueue_Private.h create mode 100644 Parse/Internal/PFFileManager.h create mode 100644 Parse/Internal/PFFileManager.m create mode 100644 Parse/Internal/PFGeoPointPrivate.h create mode 100644 Parse/Internal/PFHash.h create mode 100644 Parse/Internal/PFHash.m create mode 100644 Parse/Internal/PFInternalUtils.h create mode 100644 Parse/Internal/PFInternalUtils.m create mode 100644 Parse/Internal/PFJSONSerialization.h create mode 100644 Parse/Internal/PFJSONSerialization.m create mode 100644 Parse/Internal/PFKeychainStore.h create mode 100644 Parse/Internal/PFKeychainStore.m create mode 100644 Parse/Internal/PFLocationManager.h create mode 100644 Parse/Internal/PFLocationManager.m create mode 100644 Parse/Internal/PFLogger.h create mode 100644 Parse/Internal/PFLogger.m create mode 100644 Parse/Internal/PFLogging.h create mode 100644 Parse/Internal/PFMacros.h create mode 100644 Parse/Internal/PFMulticastDelegate.h create mode 100644 Parse/Internal/PFMulticastDelegate.m create mode 100644 Parse/Internal/PFNetworkCommand.h create mode 100644 Parse/Internal/PFPinningEventuallyQueue.h create mode 100644 Parse/Internal/PFPinningEventuallyQueue.m create mode 100644 Parse/Internal/PFReachability.h create mode 100644 Parse/Internal/PFReachability.m create mode 100644 Parse/Internal/PFTaskQueue.h create mode 100644 Parse/Internal/PFTaskQueue.m create mode 100644 Parse/Internal/PFWeakValue.h create mode 100644 Parse/Internal/PFWeakValue.m create mode 100644 Parse/Internal/ParseInternal.h create mode 100644 Parse/Internal/ParseManager.h create mode 100644 Parse/Internal/ParseManager.m create mode 100644 Parse/Internal/ParseModule.h create mode 100644 Parse/Internal/ParseModule.m create mode 100644 Parse/Internal/Parse_Private.h create mode 100644 Parse/Internal/Product/PFProduct+Private.h create mode 100644 Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.h create mode 100644 Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.m create mode 100644 Parse/Internal/PropertyInfo/PFPropertyInfo.h create mode 100644 Parse/Internal/PropertyInfo/PFPropertyInfo.m create mode 100644 Parse/Internal/PropertyInfo/PFPropertyInfo_Private.h create mode 100644 Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.h create mode 100644 Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.m create mode 100644 Parse/Internal/Purchase/Controller/PFPurchaseController.h create mode 100644 Parse/Internal/Purchase/Controller/PFPurchaseController.m create mode 100644 Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.h create mode 100644 Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.m create mode 100644 Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver_Private.h create mode 100644 Parse/Internal/Push/ChannelsController/PFPushChannelsController.h create mode 100644 Parse/Internal/Push/ChannelsController/PFPushChannelsController.m create mode 100644 Parse/Internal/Push/Controller/PFPushController.h create mode 100644 Parse/Internal/Push/Controller/PFPushController.m create mode 100644 Parse/Internal/Push/Manager/PFPushManager.h create mode 100644 Parse/Internal/Push/Manager/PFPushManager.m create mode 100644 Parse/Internal/Push/PFPushPrivate.h create mode 100644 Parse/Internal/Push/State/PFMutablePushState.h create mode 100644 Parse/Internal/Push/State/PFMutablePushState.m create mode 100644 Parse/Internal/Push/State/PFPushState.h create mode 100644 Parse/Internal/Push/State/PFPushState.m create mode 100644 Parse/Internal/Push/State/PFPushState_Private.h create mode 100644 Parse/Internal/Push/Utilites/PFPushUtilities.h create mode 100644 Parse/Internal/Push/Utilites/PFPushUtilities.m create mode 100644 Parse/Internal/Query/Controller/PFCachedQueryController.h create mode 100644 Parse/Internal/Query/Controller/PFCachedQueryController.m create mode 100644 Parse/Internal/Query/Controller/PFOfflineQueryController.h create mode 100644 Parse/Internal/Query/Controller/PFOfflineQueryController.m create mode 100644 Parse/Internal/Query/Controller/PFQueryController.h create mode 100644 Parse/Internal/Query/Controller/PFQueryController.m create mode 100644 Parse/Internal/Query/PFQueryPrivate.h create mode 100644 Parse/Internal/Query/State/PFMutableQueryState.h create mode 100644 Parse/Internal/Query/State/PFMutableQueryState.m create mode 100644 Parse/Internal/Query/State/PFQueryState.h create mode 100644 Parse/Internal/Query/State/PFQueryState.m create mode 100644 Parse/Internal/Query/State/PFQueryState_Private.h create mode 100644 Parse/Internal/Query/Utilities/PFQueryUtilities.h create mode 100644 Parse/Internal/Query/Utilities/PFQueryUtilities.m create mode 100644 Parse/Internal/Relation/PFRelationPrivate.h create mode 100644 Parse/Internal/Relation/State/PFMutableRelationState.h create mode 100644 Parse/Internal/Relation/State/PFMutableRelationState.m create mode 100644 Parse/Internal/Relation/State/PFRelationState.h create mode 100644 Parse/Internal/Relation/State/PFRelationState.m create mode 100644 Parse/Internal/Relation/State/PFRelationState_Private.h create mode 100644 Parse/Internal/Session/Controller/PFSessionController.h create mode 100644 Parse/Internal/Session/Controller/PFSessionController.m create mode 100644 Parse/Internal/Session/PFSession_Private.h create mode 100644 Parse/Internal/Session/Utilities/PFSessionUtilities.h create mode 100644 Parse/Internal/Session/Utilities/PFSessionUtilities.m create mode 100644 Parse/Internal/ThreadSafety/PFThreadsafety.h create mode 100644 Parse/Internal/ThreadSafety/PFThreadsafety.m create mode 100644 Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.h create mode 100644 Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.m create mode 100644 Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.h create mode 100644 Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.m create mode 100644 Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousUtils_Private.h create mode 100644 Parse/Internal/User/AuthenticationProviders/Providers/PFAuthenticationProvider.h create mode 100644 Parse/Internal/User/Coder/File/PFUserFileCodingLogic.h create mode 100644 Parse/Internal/User/Coder/File/PFUserFileCodingLogic.m create mode 100644 Parse/Internal/User/Constants/PFUserConstants.h create mode 100644 Parse/Internal/User/Constants/PFUserConstants.m create mode 100644 Parse/Internal/User/Controller/PFUserController.h create mode 100644 Parse/Internal/User/Controller/PFUserController.m create mode 100644 Parse/Internal/User/CurrentUserController/PFCurrentUserController.h create mode 100644 Parse/Internal/User/CurrentUserController/PFCurrentUserController.m create mode 100644 Parse/Internal/User/PFUserPrivate.h create mode 100644 Parse/Internal/User/State/PFMutableUserState.h create mode 100644 Parse/Internal/User/State/PFMutableUserState.m create mode 100644 Parse/Internal/User/State/PFUserState.h create mode 100644 Parse/Internal/User/State/PFUserState.m create mode 100644 Parse/Internal/User/State/PFUserState_Private.h create mode 100644 Parse/OSX/ParseOSX.h create mode 100644 Parse/PFACL.h create mode 100644 Parse/PFACL.m create mode 100644 Parse/PFAnalytics.h create mode 100644 Parse/PFAnalytics.m create mode 100644 Parse/PFAnonymousUtils.h create mode 100644 Parse/PFAnonymousUtils.m create mode 100644 Parse/PFCloud.h create mode 100644 Parse/PFCloud.m create mode 100644 Parse/PFConfig.h create mode 100644 Parse/PFConfig.m create mode 100644 Parse/PFConstants.h create mode 100644 Parse/PFConstants.m create mode 100644 Parse/PFFile.h create mode 100644 Parse/PFFile.m create mode 100644 Parse/PFGeoPoint.h create mode 100644 Parse/PFGeoPoint.m create mode 100644 Parse/PFInstallation.h create mode 100644 Parse/PFInstallation.m create mode 100644 Parse/PFNetworkActivityIndicatorManager.h create mode 100644 Parse/PFNetworkActivityIndicatorManager.m create mode 100644 Parse/PFNullability.h create mode 100644 Parse/PFObject+Subclass.h create mode 100644 Parse/PFObject.h create mode 100644 Parse/PFObject.m create mode 100644 Parse/PFProduct.h create mode 100644 Parse/PFProduct.m create mode 100644 Parse/PFPurchase.h create mode 100644 Parse/PFPurchase.m create mode 100644 Parse/PFPush.h create mode 100644 Parse/PFPush.m create mode 100644 Parse/PFQuery.h create mode 100644 Parse/PFQuery.m create mode 100644 Parse/PFRelation.h create mode 100644 Parse/PFRelation.m create mode 100644 Parse/PFRole.h create mode 100644 Parse/PFRole.m create mode 100644 Parse/PFSession.h create mode 100644 Parse/PFSession.m create mode 100644 Parse/PFSubclassing.h create mode 100644 Parse/PFUser.h create mode 100644 Parse/PFUser.m create mode 100644 Parse/Parse.h create mode 100644 Parse/Parse.m create mode 100644 Parse/Resources/Framework.plist create mode 100644 Parse/Resources/FrameworkOSX.plist create mode 100644 Parse/Resources/Localizable.strings create mode 100644 ParseStarterProject/.gitignore create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.pbxproj create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject-Swift.xcscheme create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject/AppDelegate.swift create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Base.lproj/MainMenu.xib create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Info.plist create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.pbxproj create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject.xcscheme create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.h create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.m create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/main.m create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/Resources/Info.plist create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/Credits.rtf create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/InfoPlist.strings create mode 100644 ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/MainMenu.xib create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.pbxproj create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseStarterProject-Swift.xcscheme create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/AppDelegate.swift create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/ViewController.swift create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Base.lproj/Main.storyboard create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/LaunchImage.launchimage/Contents.json create mode 100644 ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Info.plist create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.pbxproj create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/xcshareddata/xcschemes/ParseStarterProject.xcscheme create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.h create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.m create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.h create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.m create mode 100644 ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/main.m create mode 100644 ParseStarterProject/iOS/ParseStarterProject/Resources/Default-568h@2x.png create mode 100644 ParseStarterProject/iOS/ParseStarterProject/Resources/Default.png create mode 100644 ParseStarterProject/iOS/ParseStarterProject/Resources/Default@2x.png create mode 100644 ParseStarterProject/iOS/ParseStarterProject/Resources/Info.plist create mode 100644 ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/InfoPlist.strings create mode 100644 ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/MainWindow.xib create mode 100644 ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/ParseStarterProjectViewController.xib create mode 100644 README.md create mode 100644 Rakefile create mode 100755 Scripts/build_third_party.sh create mode 100755 Scripts/xctask/build_framework_task.rb create mode 100755 Scripts/xctask/build_task.rb create mode 100644 Tests/Other/Cache/TestCache.h create mode 100644 Tests/Other/Cache/TestCache.m create mode 100644 Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.h create mode 100644 Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.m create mode 100644 Tests/Other/FileManager/TestFileManager.h create mode 100644 Tests/Other/FileManager/TestFileManager.m create mode 100644 Tests/Other/LocationManager/CLLocationManager+TestAdditions.h create mode 100644 Tests/Other/LocationManager/CLLocationManager+TestAdditions.m create mode 100644 Tests/Other/NetworkMocking/PFMockURLProtocol.h create mode 100644 Tests/Other/NetworkMocking/PFMockURLProtocol.m create mode 100644 Tests/Other/NetworkMocking/PFMockURLResponse.h create mode 100644 Tests/Other/NetworkMocking/PFMockURLResponse.m create mode 100644 Tests/Other/OCMock/OCMock+Parse.h create mode 100644 Tests/Other/OCMock/OCMock+Parse.m create mode 100644 Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.h create mode 100644 Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.m create mode 100644 Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.h create mode 100644 Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.m create mode 100644 Tests/Other/StoreKitMocking/PFTestSKProduct.h create mode 100644 Tests/Other/StoreKitMocking/PFTestSKProduct.m create mode 100644 Tests/Other/StoreKitMocking/PFTestSKProductsRequest.h create mode 100644 Tests/Other/StoreKitMocking/PFTestSKProductsRequest.m create mode 100644 Tests/Other/StoreKitMocking/PFTestSKProductsResponse.h create mode 100644 Tests/Other/StoreKitMocking/PFTestSKProductsResponse.m create mode 100644 Tests/Other/Swift/ParseUnitTests-OSX-Bridging-Header.h create mode 100644 Tests/Other/Swift/ParseUnitTests-iOS-Bridging-Header.h create mode 100644 Tests/Other/Swift/SwiftSubclass.swift create mode 100644 Tests/Other/Swizzling/PFTestSwizzledMethod.h create mode 100644 Tests/Other/Swizzling/PFTestSwizzledMethod.m create mode 100644 Tests/Other/Swizzling/PFTestSwizzlingUtilities.h create mode 100644 Tests/Other/Swizzling/PFTestSwizzlingUtilities.m create mode 100644 Tests/Other/TestCases/TestCase/PFTestCase.h create mode 100644 Tests/Other/TestCases/TestCase/PFTestCase.m create mode 100644 Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.h create mode 100644 Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m create mode 100644 Tests/Resources/ParseUnitTests-OSX-Info.plist create mode 100644 Tests/Resources/ParseUnitTests-iOS-Info.plist create mode 100644 Tests/Unit/ACLStateTests.m create mode 100644 Tests/Unit/ACLUnitTests.m create mode 100644 Tests/Unit/AlertViewTests.m create mode 100644 Tests/Unit/AnalyticsCommandTests.m create mode 100644 Tests/Unit/AnalyticsControllerTests.m create mode 100644 Tests/Unit/AnalyticsUnitTests.m create mode 100644 Tests/Unit/AnalyticsUtilitiesTests.m create mode 100644 Tests/Unit/AnonymousAuthenticationProviderTests.m create mode 100644 Tests/Unit/AnonymousUtilsTests.m create mode 100644 Tests/Unit/BaseStateTests.m create mode 100644 Tests/Unit/BlockRetryerTests.m create mode 100644 Tests/Unit/CloudCodeControllerTests.m create mode 100644 Tests/Unit/CloudCommandTests.m create mode 100644 Tests/Unit/CloudUnitTests.m create mode 100644 Tests/Unit/CommandResultTests.m create mode 100644 Tests/Unit/CommandURLRequestConstructorTests.m create mode 100644 Tests/Unit/CommandUnitTests.m create mode 100644 Tests/Unit/ConfigCommandTests.m create mode 100644 Tests/Unit/ConfigControllerTests.m create mode 100644 Tests/Unit/ConfigUnitTests.m create mode 100644 Tests/Unit/CurrentConfigControllerTests.m create mode 100644 Tests/Unit/DateFormatterTests.m create mode 100644 Tests/Unit/DecoderTests.m create mode 100644 Tests/Unit/DefaultACLControllerTests.m create mode 100644 Tests/Unit/DeviceTests.m create mode 100644 Tests/Unit/ExtensionDataSharingMobileTests.m create mode 100644 Tests/Unit/ExtensionDataSharingTests.m create mode 100644 Tests/Unit/FieldOperationDecoderTests.m create mode 100644 Tests/Unit/FieldOperationTests.m create mode 100644 Tests/Unit/FileCommandTests.m create mode 100644 Tests/Unit/FileControllerTests.m create mode 100644 Tests/Unit/FileStateTests.m create mode 100644 Tests/Unit/FileUnitTests.m create mode 100644 Tests/Unit/GeoPointLocationTests.m create mode 100644 Tests/Unit/GeoPointUnitTests.m create mode 100644 Tests/Unit/HashTests.m create mode 100644 Tests/Unit/IncrementUnitTests.m create mode 100644 Tests/Unit/InstallationIdentifierUnitTests.m create mode 100644 Tests/Unit/InstallationUnitTests.m create mode 100644 Tests/Unit/KeyValueCacheTests.m create mode 100644 Tests/Unit/KeychainStoreTests.m create mode 100644 Tests/Unit/LocationManagerMobileTests.m create mode 100644 Tests/Unit/LocationManagerTests.m create mode 100644 Tests/Unit/ObjectBatchCommandTests.m create mode 100644 Tests/Unit/ObjectBatchControllerTests.m create mode 100644 Tests/Unit/ObjectCommandTests.m create mode 100644 Tests/Unit/ObjectEstimatedDataTests.m create mode 100644 Tests/Unit/ObjectFileCoderTests.m create mode 100644 Tests/Unit/ObjectFileCodingLogicTests.m create mode 100644 Tests/Unit/ObjectFilePersistenceControllerTests.m create mode 100644 Tests/Unit/ObjectLocalIdStoreTests.m create mode 100644 Tests/Unit/ObjectOfflineTests.m create mode 100644 Tests/Unit/ObjectPinTests.m create mode 100644 Tests/Unit/ObjectStateTests.m create mode 100644 Tests/Unit/ObjectSubclassPropertiesTests.m create mode 100644 Tests/Unit/ObjectSubclassTests.m create mode 100644 Tests/Unit/ObjectSubclassingControllerTests.m create mode 100644 Tests/Unit/ObjectUnitTests.m create mode 100644 Tests/Unit/ObjectUtilitiesTests.m create mode 100644 Tests/Unit/OfflineQueryControllerTests.m create mode 100644 Tests/Unit/OfflineQueryLogicUnitTests.m create mode 100644 Tests/Unit/OperationSetUnitTests.m create mode 100644 Tests/Unit/ParseModuleUnitTests.m create mode 100644 Tests/Unit/ParseSetupUnitTests.m create mode 100644 Tests/Unit/PinUnitTests.m create mode 100644 Tests/Unit/PinningObjectStoreTests.m create mode 100644 Tests/Unit/ProductTests.m create mode 100644 Tests/Unit/PropertyInfoTests.m create mode 100644 Tests/Unit/PurchaseControllerTests.m create mode 100644 Tests/Unit/PurchaseUnitTests.m create mode 100644 Tests/Unit/PushChannelsControllerTests.m create mode 100644 Tests/Unit/PushCommandTests.m create mode 100644 Tests/Unit/PushControllerTests.m create mode 100644 Tests/Unit/PushManagerTests.m create mode 100644 Tests/Unit/PushMobileTests.m create mode 100644 Tests/Unit/PushStateTests.m create mode 100644 Tests/Unit/PushUnitTests.m create mode 100644 Tests/Unit/QueryCachedControllerTests.m create mode 100644 Tests/Unit/QueryControllerUnitTests.m create mode 100644 Tests/Unit/QueryPredicateUnitTests.m create mode 100644 Tests/Unit/QueryStateUnitTests.m create mode 100644 Tests/Unit/QueryUnitTests.m create mode 100644 Tests/Unit/QueryUtilitiesTests.m create mode 100644 Tests/Unit/RelationStateTests.m create mode 100644 Tests/Unit/RelationUnitTests.m create mode 100644 Tests/Unit/RoleUnitTests.m create mode 100644 Tests/Unit/SQLiteDatabaseTest.m create mode 100644 Tests/Unit/SessionControllerTests.m create mode 100644 Tests/Unit/SessionUnitTests.m create mode 100644 Tests/Unit/SessionUtilitiesTests.m create mode 100644 Tests/Unit/URLSessionCommandRunnerTests.m create mode 100644 Tests/Unit/URLSessionDataTaskDelegateTests.m create mode 100644 Tests/Unit/URLSessionTests.m create mode 100644 Tests/Unit/URLSessionUploadTaskDelegateTests.m create mode 100644 Tests/Unit/UserCommandTests.m create mode 100644 Tests/Unit/UserControllerTests.m create mode 100644 Tests/Unit/UserFileCodingLogicTests.m create mode 100644 Tests/Unit/UserUnitTests.m create mode 100644 Tests/testServer.config create mode 160000 Vendor/Bolts-ObjC create mode 160000 Vendor/OCMock create mode 100644 third_party_licenses.txt diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..bdf01dc78 --- /dev/null +++ b/.clang-format @@ -0,0 +1,71 @@ +# Copyright (c) 2015-present, Parse, LLC. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +--- +Language: Cpp +BasedOnStyle: LLVM +AccessModifierOffset: -2 +AlignAfterOpenBracket: true +AlignEscapedNewlinesLeft: true +AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakTemplateDeclarations: false +AlwaysBreakBeforeMultilineStrings: false +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +BinPackParameters: true +BinPackArguments: true +ColumnLimit: 0 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +DerivePointerAlignment: true +ExperimentalAutoDetectBinPacking: true +IndentCaseLabels: true +IndentWrappedFunctionNames: true +IndentFunctionDeclarationAfterType: true +MaxEmptyLinesToKeep: 1 +KeepEmptyLinesAtTheStartOfBlocks: true +NamespaceIndentation: None +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakString: 1000 +PenaltyBreakFirstLessLess: 140 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 120 +PointerAlignment: Right +SpacesBeforeTrailingComments: 1 +Cpp11BracedListStyle: true +Standard: Cpp11 +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +BreakBeforeBraces: Attach +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInAngles: false +SpaceInEmptyParentheses: false +SpacesInCStyleCastParentheses: false +SpaceAfterCStyleCast: false +SpacesInContainerLiterals: true +SpaceBeforeAssignmentOperators: true +ContinuationIndentWidth: 4 +CommentPragmas: '^ IWYU pragma:' +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +SpaceBeforeParens: ControlStatements +DisableFormat: false +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..3ad462edd --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +.DS_Store + +*.pbxuser +*.perspective +*.perspectivev3 + +*.mode1v3 +*.mode2v3 + +*.xcodeproj/xcuserdata/*.xcuserdatad + +*.xccheckout +*.xcscmblueprint +*.xcuserdatad + +Pods +Podfile.lock + +DerivedData +build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..4f75a2e03 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "Vendor/Bolts-ObjC"] + path = Vendor/Bolts-ObjC + url = git@github.com:BoltsFramework/Bolts-iOS.git +[submodule "Vendor/OCMock"] + path = Vendor/OCMock + url = git@github.com:erikdoe/ocmock.git diff --git a/.slather.yml b/.slather.yml new file mode 100644 index 000000000..6bd36f69b --- /dev/null +++ b/.slather.yml @@ -0,0 +1,6 @@ +ci_service: travis_pro +coverage_service: coveralls +xcodeproj: Parse.xcodeproj +ignore: + - Tests/* + - Vendor/* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..922baa905 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +branches: + only: + - master +language: objective-c +osx_image: xcode6.4 +cache: bundler +env: + global: + - LC_CTYPE=en_US.UTF-8 + - LANG=en_US.UTF-8 + matrix: + - TEST_TYPE=ios + - TEST_TYPE=osx + - TEST_TYPE=deployment + - TEST_TYPE=starters + - TEST_TYPE=podspecs +before_install: + - bundle install +script: + - bundle exec rake test:$TEST_TYPE diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..9e8afb56e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing to Parse SDK for iOS/OS X +We want to make contributing to this project as easy and transparent as possible. + +## Code of Conduct +Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. + +## Our Development Process +Most of our work will be done in public directly on GitHub. There may be changes done through our internal source control, but it will be rare and only as needed. + +### `master` is unsafe +Our goal is to keep `master` stable, but there may be changes that your application may not be compatible with. We'll do our best to publicize any breaking changes, but try to use our specific releases in any production environment. + +### Pull Requests +We actively welcome your pull requests. When we get one, we'll run some Parse-specific integration tests on it first. From here, we'll need to get a core member to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +1. Fork the repo and create your branch from `master`. +4. Add unit tests for any new code you add. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +### Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Bugs +Although we try to keep developing on Parse easy, you still may run into some issues. General questions should be asked on [Google Groups][google-group], technical questions should be asked on [Stack Overflow][stack-overflow], and for everything else we'll be using GitHub issues. + +### Known Issues +We use GitHub issues to track public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. + +### Reporting New Issues +Not all issues are SDK issues. If you're unsure whether your bug is with the SDK or backend, you can test to see if it reproduces with our [REST API][rest-api] and [Parse API Console][parse-api-console]. If it does, you can report backend bugs [here][bug-reports]. + +Details are key. The more information you provide us the easier it'll be for us to debug and the faster you'll receive a fix. Some examples of useful tidbits: + +* A description. What did you expect to happen and what actually happened? Why do you think that was wrong? +* A simple unit test that fails. Refer [here][tests-dir] for examples of existing unit tests. See our [README](README.md#usage) for how to run unit tests. You can submit a pull request with your failing unit test so that our CI verifies that the test fails. +* What version does this reproduce on? What version did it last work on? +* [Stacktrace or GTFO][stacktrace-or-gtfo]. In all honesty, full stacktraces with line numbers make a happy developer. +* Anything else you find relevant. + + +### Security Bugs +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. + +## Style Guide +We're still working on providing a code style for your IDE and getting a linter on GitHub, but for now try to keep the following: + +* Most importantly, match the existing code style as much as possible. +* Try to keep lines under 120 characters, if possible. + +## License +By contributing to Parse iOS/OSX SDK, you agree that your contributions will be licensed under its license. + + [google-group]: https://groups.google.com/forum/#!forum/parse-developers + [stack-overflow]: http://stackoverflow.com/tags/parse.com + [bug-reports]: https://www.parse.com/help#report + [rest-api]: https://www.parse.com/docs/rest/guide + [parse-api-console]: http://blog.parse.com/announcements/introducing-the-parse-api-console/ + [stacktrace-or-gtfo]: http://i.imgur.com/jacoj.jpg + [tests-dir]: /Tests/Unit/ diff --git a/Configurations/BoltsSDK-OSX.xcconfig b/Configurations/BoltsSDK-OSX.xcconfig new file mode 100644 index 000000000..311a2ecd8 --- /dev/null +++ b/Configurations/BoltsSDK-OSX.xcconfig @@ -0,0 +1,10 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +SDKROOT = macosx diff --git a/Configurations/BoltsSDK-iOS.xcconfig b/Configurations/BoltsSDK-iOS.xcconfig new file mode 100644 index 000000000..996b9581e --- /dev/null +++ b/Configurations/BoltsSDK-iOS.xcconfig @@ -0,0 +1,10 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +SDKROOT = iphoneos diff --git a/Configurations/Parse-OSX.xcconfig b/Configurations/Parse-OSX.xcconfig new file mode 100644 index 000000000..562b9ee66 --- /dev/null +++ b/Configurations/Parse-OSX.xcconfig @@ -0,0 +1,21 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Shared/Platform/OSX.xcconfig" +#include "Shared/Product/Framework.xcconfig" + +PRODUCT_NAME = ParseOSX + +MACH_O_TYPE = mh_dylib +DEFINES_MODULE = YES +DYLIB_INSTALL_NAME_BASE = @rpath + +FRAMEWORK_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) $(VENDOR_DIR)/Bolts-ObjC/build/osx/ + +INFOPLIST_FILE = $(PARSE_DIR)/Parse/Resources/FrameworkOSX.plist diff --git a/Configurations/Parse-iOS.xcconfig b/Configurations/Parse-iOS.xcconfig new file mode 100644 index 000000000..926c162ee --- /dev/null +++ b/Configurations/Parse-iOS.xcconfig @@ -0,0 +1,20 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Shared/Platform/iOS.xcconfig" +#include "Shared/Product/Framework.xcconfig" + +PRODUCT_NAME = Parse + +MACH_O_TYPE = staticlib +DEFINES_MODULE = YES + +FRAMEWORK_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) $(VENDOR_DIR)/Bolts-ObjC/build/ios/ + +INFOPLIST_FILE = $(PARSE_DIR)/Parse/Resources/Framework.plist diff --git a/Configurations/ParseUnitTests-OSX.xcconfig b/Configurations/ParseUnitTests-OSX.xcconfig new file mode 100644 index 000000000..a9c1476d0 --- /dev/null +++ b/Configurations/ParseUnitTests-OSX.xcconfig @@ -0,0 +1,25 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Shared/Platform/OSX.xcconfig" +#include "Shared/Product/UnitTest.xcconfig" + +PRODUCT_NAME = ParseUnitTests-OSX +PRODUCT_MODULE_NAME = ParseUnitTests + +INFOPLIST_FILE = $(SRCROOT)/Tests/Resources/ParseUnitTests-OSX-Info.plist +LD_RUNPATH_SEARCH_PATHS = $(inherited) @loader_path/../Frameworks + +FRAMEWORK_SEARCH_PATHS = $(inherited) $(VENDOR_DIR)/Bolts-ObjC/build/osx $(BUILT_PRODUCTS_DIR)/../Release$(EFFECTIVE_PLATFORM_NAME) + +HEADER_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) +USER_HEADER_SEARCH_PATHS = $(inherited) $(PARSE_DIR)/Parse/Internal/** + +// Swift +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/Tests/Other/Swift/ParseUnitTests-OSX-Bridging-Header.h diff --git a/Configurations/ParseUnitTests-iOS.xcconfig b/Configurations/ParseUnitTests-iOS.xcconfig new file mode 100644 index 000000000..d64f3be56 --- /dev/null +++ b/Configurations/ParseUnitTests-iOS.xcconfig @@ -0,0 +1,25 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Shared/Platform/iOS.xcconfig" +#include "Shared/Product/UnitTest.xcconfig" + +PRODUCT_NAME = ParseUnitTests-iOS +PRODUCT_MODULE_NAME = ParseUnitTests + +INFOPLIST_FILE = $(SRCROOT)/Tests/Resources/ParseUnitTests-iOS-Info.plist + +FRAMEWORK_SEARCH_PATHS = $(inherited) $(VENDOR_DIR)/Bolts-ObjC/build/ios +LIBRARY_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) $(BUILT_PRODUCTS_DIR)/../Release$(EFFECTIVE_PLATFORM_NAME) + +HEADER_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) $(BUILT_PRODUCTS_DIR)/../Release$(EFFECTIVE_PLATFORM_NAME) +USER_HEADER_SEARCH_PATHS = $(inherited) $(PARSE_DIR)/Parse/Internal/** + +// Swift +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/Tests/Other/Swift/ParseUnitTests-iOS-Bridging-Header.h diff --git a/Configurations/Shared/Common.xcconfig b/Configurations/Shared/Common.xcconfig new file mode 100644 index 000000000..de2de31b1 --- /dev/null +++ b/Configurations/Shared/Common.xcconfig @@ -0,0 +1,21 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Warnings.xcconfig" + +// Language Settings +CLANG_ENABLE_OBJC_ARC = YES +GCC_C_LANGUAGE_STANDARD = gnu11 +CLANG_CXX_LANGUAGE_STANDARD = gnu++14 +CLANG_CXX_LIBRARY = libstdc++ + +// Search Paths +PARSE_DIR = $(PROJECT_DIR) +VENDOR_DIR = $(PARSE_DIR)/Vendor +ALWAYS_SEARCH_USER_PATHS = NO diff --git a/Configurations/Shared/Platform/OSX.xcconfig b/Configurations/Shared/Platform/OSX.xcconfig new file mode 100644 index 000000000..db20c6a81 --- /dev/null +++ b/Configurations/Shared/Platform/OSX.xcconfig @@ -0,0 +1,11 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +SDKROOT = macosx +MACOSX_DEPLOYMENT_TARGET = 10.9 diff --git a/Configurations/Shared/Platform/iOS.xcconfig b/Configurations/Shared/Platform/iOS.xcconfig new file mode 100644 index 000000000..5c5affbec --- /dev/null +++ b/Configurations/Shared/Platform/iOS.xcconfig @@ -0,0 +1,21 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +SDKROOT = iphoneos +IPHONEOS_DEPLOYMENT_TARGET = 7.0 + +GCC_THUMB_SUPPORT = NO + +ARCHS = $(ARCHS_STANDARD) armv7s +DSTROOT = /tmp/$(PRODUCT_NAME).dst + +CODE_SIGN_IDENTITY = +CODE_SIGNING_REQUIRED = NO + +TARGETED_DEVICE_FAMILY = 1,2 diff --git a/Configurations/Shared/Product/Application.xcconfig b/Configurations/Shared/Product/Application.xcconfig new file mode 100644 index 000000000..d5c4577f1 --- /dev/null +++ b/Configurations/Shared/Product/Application.xcconfig @@ -0,0 +1,13 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks + +ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon +ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage diff --git a/Configurations/Shared/Product/Framework.xcconfig b/Configurations/Shared/Product/Framework.xcconfig new file mode 100644 index 000000000..d70035cc7 --- /dev/null +++ b/Configurations/Shared/Product/Framework.xcconfig @@ -0,0 +1,16 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +ENABLE_NS_ASSERTIONS = NO +MTL_ENABLE_DEBUG_INFO = NO + +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 + +SKIP_INSTALL = YES diff --git a/Configurations/Shared/Product/UnitTest.xcconfig b/Configurations/Shared/Product/UnitTest.xcconfig new file mode 100644 index 000000000..ee42705f4 --- /dev/null +++ b/Configurations/Shared/Product/UnitTest.xcconfig @@ -0,0 +1,14 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +OTHER_LDFLAGS = $(inherited) -ObjC -framework XCTest +BUNDLE_LOADER = $(TEST_HOST) + +LD_RUNPATH_SEARCH_PATHS = $(inherited) @loader_path/Frameworks @executable_path/Frameworks +USER_HEADER_SEARCH_PATHS = $(value) $(PARSE_DIR)/Tests/** diff --git a/Configurations/Shared/Project/Debug.xcconfig b/Configurations/Shared/Project/Debug.xcconfig new file mode 100644 index 000000000..a06e91f24 --- /dev/null +++ b/Configurations/Shared/Project/Debug.xcconfig @@ -0,0 +1,16 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "../Common.xcconfig" + +GCC_OPTIMIZATION_LEVEL = 0 +SWIFT_OPTIMIZATION_LEVEL = -Onone + +GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 +ONLY_ACTIVE_ARCH = YES diff --git a/Configurations/Shared/Project/Release.xcconfig b/Configurations/Shared/Project/Release.xcconfig new file mode 100644 index 000000000..8570b87c0 --- /dev/null +++ b/Configurations/Shared/Project/Release.xcconfig @@ -0,0 +1,18 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "../Common.xcconfig" + +GCC_OPTIMIZATION_LEVEL = s +SWIFT_OPTIMIZATION_LEVEL = -O + +VALIDATE_PRODUCT = YES + +DEPLOYMENT_POSTPROCESSING = YES +STRIP_STYLE = debugging diff --git a/Configurations/Shared/Project/Test.xcconfig b/Configurations/Shared/Project/Test.xcconfig new file mode 100644 index 000000000..47d4a0cc0 --- /dev/null +++ b/Configurations/Shared/Project/Test.xcconfig @@ -0,0 +1,15 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +#include "Debug.xcconfig" + +GCC_GENERATE_TEST_COVERAGE_FILES = YES +GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES + +ENABLE_TESTABILITY = YES diff --git a/Configurations/Shared/Warnings.xcconfig b/Configurations/Shared/Warnings.xcconfig new file mode 100644 index 000000000..e5b8b77d9 --- /dev/null +++ b/Configurations/Shared/Warnings.xcconfig @@ -0,0 +1,43 @@ +// +// Copyright (c) 2015-present, Parse, LLC. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// + +ENABLE_STRICT_OBJC_MSGSEND = YES + +GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES +GCC_WARN_ABOUT_MISSING_NEWLINE = YES +GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES +GCC_WARN_CHECK_SWITCH_STATEMENTS = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES +GCC_WARN_UNKNOWN_PRAGMAS = YES +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_LABEL = YES +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES + +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_UNREACHABLE_CODE = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES + +// Errors +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..7d7bbc762 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' + +gem 'rake' +gem 'plist' +gem 'naturally', '~> 1.3.2' +gem 'slather' +gem 'xcpretty' +gem 'xcodeproj' +gem 'cocoapods' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..f916ec331 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,80 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (4.2.3) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + claide (0.9.1) + clamp (0.6.5) + cocoapods (0.38.2) + activesupport (>= 3.2.15) + claide (~> 0.9.1) + cocoapods-core (= 0.38.2) + cocoapods-downloader (~> 0.9.1) + cocoapods-plugins (~> 0.4.2) + cocoapods-stats (~> 0.5.3) + cocoapods-trunk (~> 0.6.1) + cocoapods-try (~> 0.4.5) + colored (~> 1.2) + escape (~> 0.0.4) + molinillo (~> 0.3.1) + nap (~> 0.8) + xcodeproj (~> 0.26.3) + cocoapods-core (0.38.2) + activesupport (>= 3.2.15) + fuzzy_match (~> 2.0.4) + nap (~> 0.8.0) + cocoapods-downloader (0.9.1) + cocoapods-plugins (0.4.2) + nap + cocoapods-stats (0.5.3) + nap (~> 0.8) + cocoapods-trunk (0.6.1) + nap (>= 0.8) + netrc (= 0.7.8) + cocoapods-try (0.4.5) + colored (1.2) + escape (0.0.4) + fuzzy_match (2.0.4) + i18n (0.7.0) + json (1.8.3) + mini_portile (0.6.2) + minitest (5.8.0) + molinillo (0.3.1) + nap (0.8.0) + naturally (1.3.2) + netrc (0.7.8) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) + plist (3.1.0) + rake (10.4.2) + slather (1.8.1) + clamp (~> 0.6) + nokogiri (~> 1.6.3) + xcodeproj (~> 0.26.2) + thread_safe (0.3.5) + tzinfo (1.2.2) + thread_safe (~> 0.1) + xcodeproj (0.26.3) + activesupport (>= 3) + claide (~> 0.9.1) + colored (~> 1.2) + xcpretty (0.1.10) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods + naturally (~> 1.3.2) + plist + rake + slather + xcodeproj + xcpretty + +BUNDLED WITH + 1.10.5 diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..d98b0e04d --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For Parse iOS/OSX SDK software + +Copyright (c) 2015-present, Parse, LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Parse nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PATENTS b/PATENTS new file mode 100644 index 000000000..c67b292cc --- /dev/null +++ b/PATENTS @@ -0,0 +1,33 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the Parse iOS/OSX SDK software distributed by Parse, LLC. + +Parse, LLC. ("Parse") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Parse’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Parse or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Parse or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Parse or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Parse that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. diff --git a/Parse-OSX.podspec b/Parse-OSX.podspec new file mode 100644 index 000000000..f57f43dff --- /dev/null +++ b/Parse-OSX.podspec @@ -0,0 +1,41 @@ +Pod::Spec.new do |s| + s.name = 'Parse-OSX' + s.version = '1.8.0' + s.license = { :type => 'Commercial', :text => "See https://www.parse.com/about/terms" } + s.homepage = 'https://www.parse.com/' + s.summary = 'Parse is a complete technology stack to power your app\'s backend.' + s.documentation_url = 'https://parse.com/docs/ios_guide' + s.authors = 'Parse' + + s.source = { :git => "https://github.com/ParsePlatform/Parse-SDK-iOS-OSX.git", :tag => s.version.to_s } + + s.platform = :osx + s.osx.deployment_target = '10.9' + s.requires_arc = true + + s.header_dir = 'ParseOSX' + s.module_name = 'ParseOSX' + + s.source_files = 'Parse/*.{h,m}', + 'Parse/OSX.{h,m}', + 'Parse/Internal/**/*.{h,m}' + s.public_header_files = 'Parse/*.h', 'Parse/OSX/*.h' + s.resources = 'Parse/Resources/Localizable.strings' + s.exclude_files = 'Parse/PFNetworkActivityIndicatorManager.{h,m}', + 'Parse/PFProduct.{h,m}', + 'Parse/PFPurchase.{h,m}', + 'Parse/Internal/PFAlertView.{h,m}', + 'Parse/Internal/Product/**/*.{h,m}', + 'Parse/Internal/Purchase/**/*.{h,m}' + + s.frameworks = 'ApplicationServices', + 'CFNetwork', + 'CoreGraphics', + 'CoreLocation', + 'QuartzCore', + 'Security', + 'SystemConfiguration' + s.libraries = 'z', 'sqlite3' + + s.dependency 'Bolts/Tasks', '>= 1.2.0' +end diff --git a/Parse.podspec b/Parse.podspec new file mode 100644 index 000000000..9721de0de --- /dev/null +++ b/Parse.podspec @@ -0,0 +1,33 @@ +Pod::Spec.new do |s| + s.name = 'Parse' + s.version = '1.8.0' + s.license = { :type => 'Commercial', :text => "See https://www.parse.com/about/terms" } + s.homepage = 'https://www.parse.com/' + s.summary = 'Parse is a complete technology stack to power your app\'s backend.' + s.authors = 'Parse' + + s.source = { :git => "https://github.com/ParsePlatform/Parse-SDK-iOS-OSX.git", :tag => s.version.to_s } + + s.platform = :ios + s.ios.deployment_target = '7.0' + s.requires_arc = true + + s.source_files = 'Parse/*.{h,m}', + 'Parse/Internal/**/*.{h,m}' + s.public_header_files = 'Parse/*.h' + s.resources = 'Parse/Resources/Localizable.strings' + + s.frameworks = 'AudioToolbox', + 'CFNetwork', + 'CoreGraphics', + 'CoreLocation', + 'QuartzCore', + 'Security', + 'StoreKit', + 'SystemConfiguration' + s.weak_frameworks = 'Accounts', + 'Social' + s.libraries = 'z', 'sqlite3' + + s.dependency 'Bolts/Tasks', '>= 1.2.0' +end diff --git a/Parse.xcodeproj/project.pbxproj b/Parse.xcodeproj/project.pbxproj new file mode 100644 index 000000000..b5d25c6d7 --- /dev/null +++ b/Parse.xcodeproj/project.pbxproj @@ -0,0 +1,4427 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 7CBC8DA116D594F800AEC66D /* PFTaskQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CF213BB16D41D980065CF1A /* PFTaskQueue.m */; }; + 8103FA38198FC190000BAE3F /* BFTask+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8103FA33198FC190000BAE3F /* BFTask+Private.h */; }; + 8103FA3A198FC190000BAE3F /* BFTask+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA34198FC190000BAE3F /* BFTask+Private.m */; }; + 8103FA3C198FC190000BAE3F /* PFCategoryLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8103FA35198FC190000BAE3F /* PFCategoryLoader.h */; }; + 8103FA3E198FC190000BAE3F /* PFCategoryLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA36198FC190000BAE3F /* PFCategoryLoader.m */; }; + 81068EBB1ADE462500A34D13 /* Parse_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81068EBA1ADE462500A34D13 /* Parse_Private.h */; }; + 81068EBC1ADE462500A34D13 /* Parse_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81068EBA1ADE462500A34D13 /* Parse_Private.h */; }; + 81068EF11AE0845D00A34D13 /* PFEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 81068EEF1AE0845D00A34D13 /* PFEncoder.h */; }; + 81068EF21AE0845D00A34D13 /* PFEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 81068EEF1AE0845D00A34D13 /* PFEncoder.h */; }; + 81068EF31AE0845D00A34D13 /* PFEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 81068EF01AE0845D00A34D13 /* PFEncoder.m */; }; + 81068EF41AE0845D00A34D13 /* PFEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 81068EF01AE0845D00A34D13 /* PFEncoder.m */; }; + 810749AE1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 810749AC1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h */; }; + 810749AF1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 810749AC1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h */; }; + 810749B01B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 810749AD1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m */; }; + 810749B11B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 810749AD1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m */; }; + 810B7D761A0291FF003C0909 /* PFMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 810B7D751A0291FF003C0909 /* PFMacros.h */; }; + 810B7D771A0291FF003C0909 /* PFMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 810B7D751A0291FF003C0909 /* PFMacros.h */; }; + 810ECA701B573853002944D4 /* PFRelationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 810ECA6F1B573853002944D4 /* PFRelationPrivate.h */; }; + 810ECA711B573853002944D4 /* PFRelationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 810ECA6F1B573853002944D4 /* PFRelationPrivate.h */; }; + 810ECC6F1B573C6B002944D4 /* SwiftSubclass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC6E1B573C6B002944D4 /* SwiftSubclass.swift */; }; + 810ECC701B573C6B002944D4 /* SwiftSubclass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC6E1B573C6B002944D4 /* SwiftSubclass.swift */; }; + 810ECC741B573CC5002944D4 /* OCMock+Parse.m in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC731B573CC5002944D4 /* OCMock+Parse.m */; }; + 810ECC751B573CC5002944D4 /* OCMock+Parse.m in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC731B573CC5002944D4 /* OCMock+Parse.m */; }; + 810ECC7D1B573D28002944D4 /* PFTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC791B573D28002944D4 /* PFTestCase.m */; }; + 810ECC7E1B573D28002944D4 /* PFTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC791B573D28002944D4 /* PFTestCase.m */; }; + 810ECC7F1B573D28002944D4 /* PFUnitTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC7C1B573D28002944D4 /* PFUnitTestCase.m */; }; + 810ECC801B573D28002944D4 /* PFUnitTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 810ECC7C1B573D28002944D4 /* PFUnitTestCase.m */; }; + 811214731B3E1CF10052741B /* PFObjectBatchController.h in Headers */ = {isa = PBXBuildFile; fileRef = 811214711B3E1CF10052741B /* PFObjectBatchController.h */; }; + 811214741B3E1CF10052741B /* PFObjectBatchController.h in Headers */ = {isa = PBXBuildFile; fileRef = 811214711B3E1CF10052741B /* PFObjectBatchController.h */; }; + 811214751B3E1CF10052741B /* PFObjectBatchController.m in Sources */ = {isa = PBXBuildFile; fileRef = 811214721B3E1CF10052741B /* PFObjectBatchController.m */; }; + 811214761B3E1CF10052741B /* PFObjectBatchController.m in Sources */ = {isa = PBXBuildFile; fileRef = 811214721B3E1CF10052741B /* PFObjectBatchController.m */; }; + 81146C7E1A785203001F8473 /* PFRESTObjectCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81146C7C1A785203001F8473 /* PFRESTObjectCommand.h */; }; + 81146C7F1A785203001F8473 /* PFRESTObjectCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81146C7C1A785203001F8473 /* PFRESTObjectCommand.h */; }; + 81146C801A785203001F8473 /* PFRESTObjectCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81146C7D1A785203001F8473 /* PFRESTObjectCommand.m */; }; + 81146C811A785203001F8473 /* PFRESTObjectCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81146C7D1A785203001F8473 /* PFRESTObjectCommand.m */; }; + 8119C9971A76E28F0085B516 /* PFNetworkCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 8119C9961A76E28F0085B516 /* PFNetworkCommand.h */; }; + 8119C9981A76E28F0085B516 /* PFNetworkCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 8119C9961A76E28F0085B516 /* PFNetworkCommand.h */; }; + 811AAF181B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 811AAF171B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m */; }; + 811AAF191B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 811AAF171B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m */; }; + 812145771AA4A4C1000B23F5 /* PFSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 812145751AA4A4C1000B23F5 /* PFSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 812145781AA4A4C1000B23F5 /* PFSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 812145751AA4A4C1000B23F5 /* PFSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 812145791AA4A4C1000B23F5 /* PFSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 812145761AA4A4C1000B23F5 /* PFSession.m */; }; + 8121457A1AA4A4C1000B23F5 /* PFSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 812145761AA4A4C1000B23F5 /* PFSession.m */; }; + 8121457D1AA4A808000B23F5 /* PFRESTSessionCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 8121457B1AA4A808000B23F5 /* PFRESTSessionCommand.h */; }; + 8121457E1AA4A808000B23F5 /* PFRESTSessionCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 8121457B1AA4A808000B23F5 /* PFRESTSessionCommand.h */; }; + 8121457F1AA4A808000B23F5 /* PFRESTSessionCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 8121457C1AA4A808000B23F5 /* PFRESTSessionCommand.m */; }; + 812145801AA4A808000B23F5 /* PFRESTSessionCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 8121457C1AA4A808000B23F5 /* PFRESTSessionCommand.m */; }; + 8124C8731B26B9E700758E00 /* PFPinningObjectStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8711B26B9E700758E00 /* PFPinningObjectStore.h */; }; + 8124C8741B26B9E700758E00 /* PFPinningObjectStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8711B26B9E700758E00 /* PFPinningObjectStore.h */; }; + 8124C8751B26B9E700758E00 /* PFPinningObjectStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8721B26B9E700758E00 /* PFPinningObjectStore.m */; }; + 8124C8761B26B9E700758E00 /* PFPinningObjectStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8721B26B9E700758E00 /* PFPinningObjectStore.m */; }; + 8124C8851B27588800758E00 /* PFPushChannelsController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8831B27588800758E00 /* PFPushChannelsController.h */; }; + 8124C8861B27588800758E00 /* PFPushChannelsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8841B27588800758E00 /* PFPushChannelsController.m */; }; + 8124C88A1B276B8800758E00 /* PFObjectFilePersistenceController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8881B276B8800758E00 /* PFObjectFilePersistenceController.h */; }; + 8124C88B1B276B8800758E00 /* PFObjectFilePersistenceController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8881B276B8800758E00 /* PFObjectFilePersistenceController.h */; }; + 8124C88C1B276B8800758E00 /* PFObjectFilePersistenceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8891B276B8800758E00 /* PFObjectFilePersistenceController.m */; }; + 8124C88D1B276B8800758E00 /* PFObjectFilePersistenceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8891B276B8800758E00 /* PFObjectFilePersistenceController.m */; }; + 8124C89F1B27BF0900758E00 /* PFSessionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C89D1B27BF0900758E00 /* PFSessionController.h */; }; + 8124C8A01B27BF0900758E00 /* PFSessionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C89D1B27BF0900758E00 /* PFSessionController.h */; }; + 8124C8A11B27BF0900758E00 /* PFSessionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C89E1B27BF0900758E00 /* PFSessionController.m */; }; + 8124C8A21B27BF0900758E00 /* PFSessionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C89E1B27BF0900758E00 /* PFSessionController.m */; }; + 8124C8AC1B27D5D600758E00 /* PFSessionUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8AA1B27D5D600758E00 /* PFSessionUtilities.h */; }; + 8124C8AD1B27D5D600758E00 /* PFSessionUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8AA1B27D5D600758E00 /* PFSessionUtilities.h */; }; + 8124C8AE1B27D5D600758E00 /* PFSessionUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8AB1B27D5D600758E00 /* PFSessionUtilities.m */; }; + 8124C8AF1B27D5D600758E00 /* PFSessionUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8AB1B27D5D600758E00 /* PFSessionUtilities.m */; }; + 812714881AE6F1270076AE8D /* ParseManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 812714861AE6F1270076AE8D /* ParseManager.h */; }; + 812714891AE6F1270076AE8D /* ParseManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 812714861AE6F1270076AE8D /* ParseManager.h */; }; + 8127148A1AE6F1270076AE8D /* ParseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 812714871AE6F1270076AE8D /* ParseManager.m */; }; + 8127148B1AE6F1270076AE8D /* ParseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 812714871AE6F1270076AE8D /* ParseManager.m */; }; + 812B02961B5DE3EE003846EE /* PFURLSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B02921B5DE3EE003846EE /* PFURLSession.h */; }; + 812B02971B5DE3EE003846EE /* PFURLSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B02921B5DE3EE003846EE /* PFURLSession.h */; }; + 812B02981B5DE3EE003846EE /* PFURLSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B02931B5DE3EE003846EE /* PFURLSession.m */; }; + 812B02991B5DE3EE003846EE /* PFURLSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B02931B5DE3EE003846EE /* PFURLSession.m */; }; + 812B02A81B5DE562003846EE /* PFCommandURLRequestConstructor.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B02A61B5DE562003846EE /* PFCommandURLRequestConstructor.h */; }; + 812B02A91B5DE562003846EE /* PFCommandURLRequestConstructor.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B02A61B5DE562003846EE /* PFCommandURLRequestConstructor.h */; }; + 812B02AA1B5DE562003846EE /* PFCommandURLRequestConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B02A71B5DE562003846EE /* PFCommandURLRequestConstructor.m */; }; + 812B02AB1B5DE562003846EE /* PFCommandURLRequestConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B02A71B5DE562003846EE /* PFCommandURLRequestConstructor.m */; }; + 812B63001B5F30D3009CEAA9 /* PFObjectFileCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B62FE1B5F30D3009CEAA9 /* PFObjectFileCoder.h */; }; + 812B63011B5F30D3009CEAA9 /* PFObjectFileCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B62FE1B5F30D3009CEAA9 /* PFObjectFileCoder.h */; }; + 812B63021B5F30D3009CEAA9 /* PFObjectFileCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B62FF1B5F30D3009CEAA9 /* PFObjectFileCoder.m */; }; + 812B63031B5F30D3009CEAA9 /* PFObjectFileCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B62FF1B5F30D3009CEAA9 /* PFObjectFileCoder.m */; }; + 812B7AB81AF2FA4800D15FF5 /* PFQueryController.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B7AB61AF2FA4800D15FF5 /* PFQueryController.h */; }; + 812B7AB91AF2FA4800D15FF5 /* PFQueryController.h in Headers */ = {isa = PBXBuildFile; fileRef = 812B7AB61AF2FA4800D15FF5 /* PFQueryController.h */; }; + 812B7ABA1AF2FA4800D15FF5 /* PFQueryController.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B7AB71AF2FA4800D15FF5 /* PFQueryController.m */; }; + 812B7ABB1AF2FA4800D15FF5 /* PFQueryController.m in Sources */ = {isa = PBXBuildFile; fileRef = 812B7AB71AF2FA4800D15FF5 /* PFQueryController.m */; }; + 812FC6201B0FF9FA0043C07F /* PFPurchaseController.h in Headers */ = {isa = PBXBuildFile; fileRef = 812FC61E1B0FF9FA0043C07F /* PFPurchaseController.h */; }; + 812FC6211B0FF9FA0043C07F /* PFPurchaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = 812FC61F1B0FF9FA0043C07F /* PFPurchaseController.m */; }; + 81308B6F1B5781F500FFFF44 /* PFTestSwizzledMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 81308B6C1B5781F500FFFF44 /* PFTestSwizzledMethod.m */; }; + 81308B701B5781F500FFFF44 /* PFTestSwizzledMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 81308B6C1B5781F500FFFF44 /* PFTestSwizzledMethod.m */; }; + 81308B711B5781F500FFFF44 /* PFTestSwizzlingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 81308B6E1B5781F500FFFF44 /* PFTestSwizzlingUtilities.m */; }; + 81308B721B5781F500FFFF44 /* PFTestSwizzlingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 81308B6E1B5781F500FFFF44 /* PFTestSwizzlingUtilities.m */; }; + 81329E8E1AE1E8840071EE3E /* PFReachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 81329E8C1AE1E8840071EE3E /* PFReachability.h */; }; + 81329E8F1AE1E8840071EE3E /* PFReachability.h in Headers */ = {isa = PBXBuildFile; fileRef = 81329E8C1AE1E8840071EE3E /* PFReachability.h */; }; + 81329E901AE1E8840071EE3E /* PFReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 81329E8D1AE1E8840071EE3E /* PFReachability.m */; }; + 81329E911AE1E8840071EE3E /* PFReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 81329E8D1AE1E8840071EE3E /* PFReachability.m */; }; + 8139B12E1A7BF630002BEF84 /* third_party_licenses.txt in Resources */ = {isa = PBXBuildFile; fileRef = 8139B12C1A7BF559002BEF84 /* third_party_licenses.txt */; }; + 8139B1301A7BF662002BEF84 /* third_party_licenses.txt in Resources */ = {isa = PBXBuildFile; fileRef = 8139B12C1A7BF559002BEF84 /* third_party_licenses.txt */; }; + 8139B1361A7C2E76002BEF84 /* PFRESTUserCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81AFE0E51A1FDB7900AB6CB3 /* PFRESTUserCommand.h */; }; + 813E769A1B7A9BD000FA3294 /* PFErrorUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 813E76981B7A9BD000FA3294 /* PFErrorUtilities.h */; }; + 813E769B1B7A9BD000FA3294 /* PFErrorUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 813E76981B7A9BD000FA3294 /* PFErrorUtilities.h */; }; + 813E769C1B7A9BD000FA3294 /* PFErrorUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 813E76991B7A9BD000FA3294 /* PFErrorUtilities.m */; }; + 813E769D1B7A9BD000FA3294 /* PFErrorUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 813E76991B7A9BD000FA3294 /* PFErrorUtilities.m */; }; + 8143E65D1AFC1BA5008C4E06 /* PFOfflineQueryController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8143E65B1AFC1BA5008C4E06 /* PFOfflineQueryController.h */; }; + 8143E65E1AFC1BA5008C4E06 /* PFOfflineQueryController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8143E65B1AFC1BA5008C4E06 /* PFOfflineQueryController.h */; }; + 8143E65F1AFC1BA5008C4E06 /* PFOfflineQueryController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8143E65C1AFC1BA5008C4E06 /* PFOfflineQueryController.m */; }; + 8143E6601AFC1BA5008C4E06 /* PFOfflineQueryController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8143E65C1AFC1BA5008C4E06 /* PFOfflineQueryController.m */; }; + 8143E6631AFC1C7D008C4E06 /* PFCachedQueryController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8143E6611AFC1C7D008C4E06 /* PFCachedQueryController.h */; }; + 8143E6641AFC1C7D008C4E06 /* PFCachedQueryController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8143E6611AFC1C7D008C4E06 /* PFCachedQueryController.h */; }; + 8143E6651AFC1C7D008C4E06 /* PFCachedQueryController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8143E6621AFC1C7D008C4E06 /* PFCachedQueryController.m */; }; + 8143E6661AFC1C7D008C4E06 /* PFCachedQueryController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8143E6621AFC1C7D008C4E06 /* PFCachedQueryController.m */; }; + 81443B331A27838500F3FD17 /* PFDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 81443B311A27838500F3FD17 /* PFDevice.h */; }; + 81443B341A27838500F3FD17 /* PFDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 81443B311A27838500F3FD17 /* PFDevice.h */; }; + 81443B351A27838500F3FD17 /* PFDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 81443B321A27838500F3FD17 /* PFDevice.m */; }; + 81443B361A27838500F3FD17 /* PFDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 81443B321A27838500F3FD17 /* PFDevice.m */; }; + 814881451B795C63008763BF /* PFKeyValueCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 814881421B795C63008763BF /* PFKeyValueCache.h */; }; + 814881461B795C63008763BF /* PFKeyValueCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 814881421B795C63008763BF /* PFKeyValueCache.h */; }; + 814881471B795C63008763BF /* PFKeyValueCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 814881431B795C63008763BF /* PFKeyValueCache.m */; }; + 814881481B795C63008763BF /* PFKeyValueCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 814881431B795C63008763BF /* PFKeyValueCache.m */; }; + 814881491B795C63008763BF /* PFKeyValueCache_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 814881441B795C63008763BF /* PFKeyValueCache_Private.h */; }; + 8148814A1B795C63008763BF /* PFKeyValueCache_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 814881441B795C63008763BF /* PFKeyValueCache_Private.h */; }; + 814881511B795CAC008763BF /* PFPropertyInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148814C1B795CAC008763BF /* PFPropertyInfo.h */; }; + 814881521B795CAC008763BF /* PFPropertyInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148814C1B795CAC008763BF /* PFPropertyInfo.h */; }; + 814881531B795CAC008763BF /* PFPropertyInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148814D1B795CAC008763BF /* PFPropertyInfo.m */; }; + 814881541B795CAC008763BF /* PFPropertyInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148814D1B795CAC008763BF /* PFPropertyInfo.m */; }; + 814881551B795CAC008763BF /* PFPropertyInfo_Runtime.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148814E1B795CAC008763BF /* PFPropertyInfo_Runtime.h */; }; + 814881561B795CAC008763BF /* PFPropertyInfo_Runtime.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148814E1B795CAC008763BF /* PFPropertyInfo_Runtime.h */; }; + 814881571B795CAC008763BF /* PFPropertyInfo_Runtime.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148814F1B795CAC008763BF /* PFPropertyInfo_Runtime.m */; }; + 814881581B795CAC008763BF /* PFPropertyInfo_Runtime.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148814F1B795CAC008763BF /* PFPropertyInfo_Runtime.m */; }; + 814881591B795CAC008763BF /* PFPropertyInfo_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 814881501B795CAC008763BF /* PFPropertyInfo_Private.h */; }; + 8148815A1B795CAC008763BF /* PFPropertyInfo_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 814881501B795CAC008763BF /* PFPropertyInfo_Private.h */; }; + 814881601B795CD4008763BF /* PFMultiProcessFileLock.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148815C1B795CD4008763BF /* PFMultiProcessFileLock.h */; }; + 814881611B795CD4008763BF /* PFMultiProcessFileLock.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148815C1B795CD4008763BF /* PFMultiProcessFileLock.h */; }; + 814881621B795CD4008763BF /* PFMultiProcessFileLock.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148815D1B795CD4008763BF /* PFMultiProcessFileLock.m */; }; + 814881631B795CD4008763BF /* PFMultiProcessFileLock.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148815D1B795CD4008763BF /* PFMultiProcessFileLock.m */; }; + 814881641B795CD4008763BF /* PFMultiProcessFileLockController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148815E1B795CD4008763BF /* PFMultiProcessFileLockController.h */; }; + 814881651B795CD4008763BF /* PFMultiProcessFileLockController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8148815E1B795CD4008763BF /* PFMultiProcessFileLockController.h */; }; + 814881661B795CD4008763BF /* PFMultiProcessFileLockController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148815F1B795CD4008763BF /* PFMultiProcessFileLockController.m */; }; + 814881671B795CD4008763BF /* PFMultiProcessFileLockController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8148815F1B795CD4008763BF /* PFMultiProcessFileLockController.m */; }; + 814916291B66D44500EFD14F /* ACLStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CC1B66D44500EFD14F /* ACLStateTests.m */; }; + 8149162A1B66D44500EFD14F /* ACLStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CC1B66D44500EFD14F /* ACLStateTests.m */; }; + 8149162B1B66D44500EFD14F /* ACLUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CD1B66D44500EFD14F /* ACLUnitTests.m */; }; + 8149162C1B66D44500EFD14F /* ACLUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CD1B66D44500EFD14F /* ACLUnitTests.m */; }; + 8149162F1B66D44500EFD14F /* AnalyticsCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CF1B66D44500EFD14F /* AnalyticsCommandTests.m */; }; + 814916301B66D44500EFD14F /* AnalyticsCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CF1B66D44500EFD14F /* AnalyticsCommandTests.m */; }; + 814916311B66D44500EFD14F /* AnalyticsControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D01B66D44500EFD14F /* AnalyticsControllerTests.m */; }; + 814916321B66D44500EFD14F /* AnalyticsControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D01B66D44500EFD14F /* AnalyticsControllerTests.m */; }; + 814916331B66D44500EFD14F /* AnalyticsUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D11B66D44500EFD14F /* AnalyticsUnitTests.m */; }; + 814916341B66D44500EFD14F /* AnalyticsUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D11B66D44500EFD14F /* AnalyticsUnitTests.m */; }; + 814916351B66D44500EFD14F /* AnalyticsUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D21B66D44500EFD14F /* AnalyticsUtilitiesTests.m */; }; + 814916361B66D44500EFD14F /* AnalyticsUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D21B66D44500EFD14F /* AnalyticsUtilitiesTests.m */; }; + 814916371B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D31B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m */; }; + 814916381B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D31B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m */; }; + 814916391B66D44500EFD14F /* AnonymousUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D41B66D44500EFD14F /* AnonymousUtilsTests.m */; }; + 8149163A1B66D44500EFD14F /* AnonymousUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D41B66D44500EFD14F /* AnonymousUtilsTests.m */; }; + 8149163B1B66D44500EFD14F /* BaseStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D51B66D44500EFD14F /* BaseStateTests.m */; }; + 8149163C1B66D44500EFD14F /* BaseStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D51B66D44500EFD14F /* BaseStateTests.m */; }; + 8149163D1B66D44500EFD14F /* BlockRetryerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D61B66D44500EFD14F /* BlockRetryerTests.m */; }; + 8149163E1B66D44500EFD14F /* BlockRetryerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D61B66D44500EFD14F /* BlockRetryerTests.m */; }; + 8149163F1B66D44500EFD14F /* CloudCodeControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D71B66D44500EFD14F /* CloudCodeControllerTests.m */; }; + 814916401B66D44500EFD14F /* CloudCodeControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D71B66D44500EFD14F /* CloudCodeControllerTests.m */; }; + 814916411B66D44600EFD14F /* CloudCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D81B66D44500EFD14F /* CloudCommandTests.m */; }; + 814916421B66D44600EFD14F /* CloudCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D81B66D44500EFD14F /* CloudCommandTests.m */; }; + 814916431B66D44600EFD14F /* CloudUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D91B66D44500EFD14F /* CloudUnitTests.m */; }; + 814916441B66D44600EFD14F /* CloudUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915D91B66D44500EFD14F /* CloudUnitTests.m */; }; + 814916451B66D44600EFD14F /* CommandResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DA1B66D44500EFD14F /* CommandResultTests.m */; }; + 814916461B66D44600EFD14F /* CommandResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DA1B66D44500EFD14F /* CommandResultTests.m */; }; + 814916471B66D44600EFD14F /* CommandUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DB1B66D44500EFD14F /* CommandUnitTests.m */; }; + 814916481B66D44600EFD14F /* CommandUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DB1B66D44500EFD14F /* CommandUnitTests.m */; }; + 814916491B66D44600EFD14F /* CommandURLRequestConstructorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DC1B66D44500EFD14F /* CommandURLRequestConstructorTests.m */; }; + 8149164A1B66D44600EFD14F /* CommandURLRequestConstructorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DC1B66D44500EFD14F /* CommandURLRequestConstructorTests.m */; }; + 8149164B1B66D44600EFD14F /* ConfigCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DD1B66D44500EFD14F /* ConfigCommandTests.m */; }; + 8149164C1B66D44600EFD14F /* ConfigCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DD1B66D44500EFD14F /* ConfigCommandTests.m */; }; + 8149164D1B66D44600EFD14F /* ConfigControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DE1B66D44500EFD14F /* ConfigControllerTests.m */; }; + 8149164E1B66D44600EFD14F /* ConfigControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DE1B66D44500EFD14F /* ConfigControllerTests.m */; }; + 8149164F1B66D44600EFD14F /* ConfigUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DF1B66D44500EFD14F /* ConfigUnitTests.m */; }; + 814916501B66D44600EFD14F /* ConfigUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915DF1B66D44500EFD14F /* ConfigUnitTests.m */; }; + 814916511B66D44600EFD14F /* CurrentConfigControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E01B66D44500EFD14F /* CurrentConfigControllerTests.m */; }; + 814916521B66D44600EFD14F /* CurrentConfigControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E01B66D44500EFD14F /* CurrentConfigControllerTests.m */; }; + 814916531B66D44600EFD14F /* DateFormatterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E11B66D44500EFD14F /* DateFormatterTests.m */; }; + 814916541B66D44600EFD14F /* DateFormatterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E11B66D44500EFD14F /* DateFormatterTests.m */; }; + 814916551B66D44600EFD14F /* DecoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E21B66D44500EFD14F /* DecoderTests.m */; }; + 814916561B66D44600EFD14F /* DecoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E21B66D44500EFD14F /* DecoderTests.m */; }; + 814916571B66D44600EFD14F /* DefaultACLControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E31B66D44500EFD14F /* DefaultACLControllerTests.m */; }; + 814916581B66D44600EFD14F /* DefaultACLControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E31B66D44500EFD14F /* DefaultACLControllerTests.m */; }; + 814916591B66D44600EFD14F /* DeviceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E41B66D44500EFD14F /* DeviceTests.m */; }; + 8149165A1B66D44600EFD14F /* DeviceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E41B66D44500EFD14F /* DeviceTests.m */; }; + 8149165F1B66D44600EFD14F /* FieldOperationDecoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E71B66D44500EFD14F /* FieldOperationDecoderTests.m */; }; + 814916601B66D44600EFD14F /* FieldOperationDecoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E71B66D44500EFD14F /* FieldOperationDecoderTests.m */; }; + 814916611B66D44600EFD14F /* FieldOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E81B66D44500EFD14F /* FieldOperationTests.m */; }; + 814916621B66D44600EFD14F /* FieldOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E81B66D44500EFD14F /* FieldOperationTests.m */; }; + 814916631B66D44600EFD14F /* FileCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E91B66D44500EFD14F /* FileCommandTests.m */; }; + 814916641B66D44600EFD14F /* FileCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E91B66D44500EFD14F /* FileCommandTests.m */; }; + 814916651B66D44600EFD14F /* FileControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EA1B66D44500EFD14F /* FileControllerTests.m */; }; + 814916661B66D44600EFD14F /* FileControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EA1B66D44500EFD14F /* FileControllerTests.m */; }; + 814916671B66D44600EFD14F /* FileStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EB1B66D44500EFD14F /* FileStateTests.m */; }; + 814916681B66D44600EFD14F /* FileStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EB1B66D44500EFD14F /* FileStateTests.m */; }; + 814916691B66D44600EFD14F /* FileUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EC1B66D44500EFD14F /* FileUnitTests.m */; }; + 8149166A1B66D44600EFD14F /* FileUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EC1B66D44500EFD14F /* FileUnitTests.m */; }; + 8149166B1B66D44600EFD14F /* GeoPointLocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915ED1B66D44500EFD14F /* GeoPointLocationTests.m */; }; + 8149166C1B66D44600EFD14F /* GeoPointLocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915ED1B66D44500EFD14F /* GeoPointLocationTests.m */; }; + 8149166D1B66D44600EFD14F /* GeoPointUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EE1B66D44500EFD14F /* GeoPointUnitTests.m */; }; + 8149166E1B66D44600EFD14F /* GeoPointUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EE1B66D44500EFD14F /* GeoPointUnitTests.m */; }; + 8149166F1B66D44600EFD14F /* IncrementUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EF1B66D44500EFD14F /* IncrementUnitTests.m */; }; + 814916701B66D44600EFD14F /* IncrementUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915EF1B66D44500EFD14F /* IncrementUnitTests.m */; }; + 814916711B66D44600EFD14F /* InstallationIdentifierUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F01B66D44500EFD14F /* InstallationIdentifierUnitTests.m */; }; + 814916721B66D44600EFD14F /* InstallationIdentifierUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F01B66D44500EFD14F /* InstallationIdentifierUnitTests.m */; }; + 814916731B66D44600EFD14F /* InstallationUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F11B66D44500EFD14F /* InstallationUnitTests.m */; }; + 814916741B66D44600EFD14F /* InstallationUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F11B66D44500EFD14F /* InstallationUnitTests.m */; }; + 814916751B66D44600EFD14F /* KeychainStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F21B66D44500EFD14F /* KeychainStoreTests.m */; }; + 814916761B66D44600EFD14F /* KeychainStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F21B66D44500EFD14F /* KeychainStoreTests.m */; }; + 814916771B66D44600EFD14F /* KeyValueCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F31B66D44500EFD14F /* KeyValueCacheTests.m */; }; + 814916781B66D44600EFD14F /* KeyValueCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F31B66D44500EFD14F /* KeyValueCacheTests.m */; }; + 814916791B66D44600EFD14F /* LocationManagerMobileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F41B66D44500EFD14F /* LocationManagerMobileTests.m */; }; + 8149167B1B66D44600EFD14F /* LocationManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F51B66D44500EFD14F /* LocationManagerTests.m */; }; + 8149167C1B66D44600EFD14F /* LocationManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F51B66D44500EFD14F /* LocationManagerTests.m */; }; + 8149167D1B66D44600EFD14F /* ObjectBatchCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F61B66D44500EFD14F /* ObjectBatchCommandTests.m */; }; + 8149167E1B66D44600EFD14F /* ObjectBatchCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F61B66D44500EFD14F /* ObjectBatchCommandTests.m */; }; + 8149167F1B66D44600EFD14F /* ObjectBatchControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F71B66D44500EFD14F /* ObjectBatchControllerTests.m */; }; + 814916801B66D44600EFD14F /* ObjectBatchControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F71B66D44500EFD14F /* ObjectBatchControllerTests.m */; }; + 814916811B66D44600EFD14F /* ObjectCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F81B66D44500EFD14F /* ObjectCommandTests.m */; }; + 814916821B66D44600EFD14F /* ObjectCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F81B66D44500EFD14F /* ObjectCommandTests.m */; }; + 814916831B66D44600EFD14F /* ObjectEstimatedDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F91B66D44500EFD14F /* ObjectEstimatedDataTests.m */; }; + 814916841B66D44600EFD14F /* ObjectEstimatedDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915F91B66D44500EFD14F /* ObjectEstimatedDataTests.m */; }; + 814916851B66D44600EFD14F /* ObjectFileCoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FA1B66D44500EFD14F /* ObjectFileCoderTests.m */; }; + 814916861B66D44600EFD14F /* ObjectFileCoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FA1B66D44500EFD14F /* ObjectFileCoderTests.m */; }; + 814916871B66D44600EFD14F /* ObjectFileCodingLogicTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FB1B66D44500EFD14F /* ObjectFileCodingLogicTests.m */; }; + 814916881B66D44600EFD14F /* ObjectFileCodingLogicTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FB1B66D44500EFD14F /* ObjectFileCodingLogicTests.m */; }; + 814916891B66D44600EFD14F /* ObjectLocalIdStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FC1B66D44500EFD14F /* ObjectLocalIdStoreTests.m */; }; + 8149168A1B66D44600EFD14F /* ObjectLocalIdStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FC1B66D44500EFD14F /* ObjectLocalIdStoreTests.m */; }; + 8149168B1B66D44600EFD14F /* ObjectOfflineTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FD1B66D44500EFD14F /* ObjectOfflineTests.m */; }; + 8149168C1B66D44600EFD14F /* ObjectOfflineTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FD1B66D44500EFD14F /* ObjectOfflineTests.m */; }; + 8149168D1B66D44600EFD14F /* ObjectPinTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FE1B66D44500EFD14F /* ObjectPinTests.m */; }; + 8149168E1B66D44600EFD14F /* ObjectPinTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FE1B66D44500EFD14F /* ObjectPinTests.m */; }; + 8149168F1B66D44600EFD14F /* ObjectStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FF1B66D44500EFD14F /* ObjectStateTests.m */; }; + 814916901B66D44600EFD14F /* ObjectStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915FF1B66D44500EFD14F /* ObjectStateTests.m */; }; + 814916911B66D44600EFD14F /* ObjectSubclassingControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916001B66D44500EFD14F /* ObjectSubclassingControllerTests.m */; }; + 814916921B66D44600EFD14F /* ObjectSubclassingControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916001B66D44500EFD14F /* ObjectSubclassingControllerTests.m */; }; + 814916931B66D44600EFD14F /* ObjectSubclassPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916011B66D44500EFD14F /* ObjectSubclassPropertiesTests.m */; }; + 814916941B66D44600EFD14F /* ObjectSubclassPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916011B66D44500EFD14F /* ObjectSubclassPropertiesTests.m */; }; + 814916951B66D44600EFD14F /* ObjectSubclassTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916021B66D44500EFD14F /* ObjectSubclassTests.m */; }; + 814916961B66D44600EFD14F /* ObjectSubclassTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916021B66D44500EFD14F /* ObjectSubclassTests.m */; }; + 814916971B66D44600EFD14F /* ObjectUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916031B66D44500EFD14F /* ObjectUnitTests.m */; }; + 814916981B66D44600EFD14F /* ObjectUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916031B66D44500EFD14F /* ObjectUnitTests.m */; }; + 814916991B66D44600EFD14F /* ObjectUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916041B66D44500EFD14F /* ObjectUtilitiesTests.m */; }; + 8149169A1B66D44600EFD14F /* ObjectUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916041B66D44500EFD14F /* ObjectUtilitiesTests.m */; }; + 8149169B1B66D44600EFD14F /* OfflineQueryControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916051B66D44500EFD14F /* OfflineQueryControllerTests.m */; }; + 8149169C1B66D44600EFD14F /* OfflineQueryControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916051B66D44500EFD14F /* OfflineQueryControllerTests.m */; }; + 8149169D1B66D44600EFD14F /* OfflineQueryLogicUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916061B66D44500EFD14F /* OfflineQueryLogicUnitTests.m */; }; + 8149169E1B66D44600EFD14F /* OfflineQueryLogicUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916061B66D44500EFD14F /* OfflineQueryLogicUnitTests.m */; }; + 8149169F1B66D44600EFD14F /* OperationSetUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916071B66D44500EFD14F /* OperationSetUnitTests.m */; }; + 814916A01B66D44600EFD14F /* OperationSetUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916071B66D44500EFD14F /* OperationSetUnitTests.m */; }; + 814916A11B66D44600EFD14F /* ParseModuleUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916081B66D44500EFD14F /* ParseModuleUnitTests.m */; }; + 814916A21B66D44600EFD14F /* ParseModuleUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916081B66D44500EFD14F /* ParseModuleUnitTests.m */; }; + 814916A31B66D44600EFD14F /* ParseSetupUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916091B66D44500EFD14F /* ParseSetupUnitTests.m */; }; + 814916A41B66D44600EFD14F /* ParseSetupUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916091B66D44500EFD14F /* ParseSetupUnitTests.m */; }; + 814916A51B66D44600EFD14F /* PinningObjectStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160A1B66D44500EFD14F /* PinningObjectStoreTests.m */; }; + 814916A61B66D44600EFD14F /* PinningObjectStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160A1B66D44500EFD14F /* PinningObjectStoreTests.m */; }; + 814916A71B66D44600EFD14F /* PinUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160B1B66D44500EFD14F /* PinUnitTests.m */; }; + 814916A81B66D44600EFD14F /* PinUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160B1B66D44500EFD14F /* PinUnitTests.m */; }; + 814916A91B66D44600EFD14F /* ProductTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160C1B66D44500EFD14F /* ProductTests.m */; }; + 814916AB1B66D44600EFD14F /* PropertyInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160D1B66D44500EFD14F /* PropertyInfoTests.m */; }; + 814916AC1B66D44600EFD14F /* PropertyInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160D1B66D44500EFD14F /* PropertyInfoTests.m */; }; + 814916AD1B66D44600EFD14F /* PurchaseControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160E1B66D44500EFD14F /* PurchaseControllerTests.m */; }; + 814916AF1B66D44600EFD14F /* PurchaseUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149160F1B66D44500EFD14F /* PurchaseUnitTests.m */; }; + 814916B11B66D44600EFD14F /* PushChannelsControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916101B66D44500EFD14F /* PushChannelsControllerTests.m */; }; + 814916B21B66D44600EFD14F /* PushChannelsControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916101B66D44500EFD14F /* PushChannelsControllerTests.m */; }; + 814916B31B66D44600EFD14F /* PushCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916111B66D44500EFD14F /* PushCommandTests.m */; }; + 814916B41B66D44600EFD14F /* PushCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916111B66D44500EFD14F /* PushCommandTests.m */; }; + 814916B51B66D44600EFD14F /* PushControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916121B66D44500EFD14F /* PushControllerTests.m */; }; + 814916B61B66D44600EFD14F /* PushControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916121B66D44500EFD14F /* PushControllerTests.m */; }; + 814916B71B66D44600EFD14F /* PushManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916131B66D44500EFD14F /* PushManagerTests.m */; }; + 814916B81B66D44600EFD14F /* PushManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916131B66D44500EFD14F /* PushManagerTests.m */; }; + 814916B91B66D44600EFD14F /* PushMobileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916141B66D44500EFD14F /* PushMobileTests.m */; }; + 814916BB1B66D44600EFD14F /* PushStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916151B66D44500EFD14F /* PushStateTests.m */; }; + 814916BC1B66D44600EFD14F /* PushStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916151B66D44500EFD14F /* PushStateTests.m */; }; + 814916BD1B66D44600EFD14F /* PushUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916161B66D44500EFD14F /* PushUnitTests.m */; }; + 814916BE1B66D44600EFD14F /* PushUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916161B66D44500EFD14F /* PushUnitTests.m */; }; + 814916BF1B66D44600EFD14F /* QueryCachedControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916171B66D44500EFD14F /* QueryCachedControllerTests.m */; }; + 814916C01B66D44600EFD14F /* QueryCachedControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916171B66D44500EFD14F /* QueryCachedControllerTests.m */; }; + 814916C11B66D44600EFD14F /* QueryControllerUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916181B66D44500EFD14F /* QueryControllerUnitTests.m */; }; + 814916C21B66D44600EFD14F /* QueryControllerUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916181B66D44500EFD14F /* QueryControllerUnitTests.m */; }; + 814916C31B66D44600EFD14F /* QueryPredicateUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916191B66D44500EFD14F /* QueryPredicateUnitTests.m */; }; + 814916C41B66D44600EFD14F /* QueryPredicateUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916191B66D44500EFD14F /* QueryPredicateUnitTests.m */; }; + 814916C51B66D44600EFD14F /* QueryStateUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161A1B66D44500EFD14F /* QueryStateUnitTests.m */; }; + 814916C61B66D44600EFD14F /* QueryStateUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161A1B66D44500EFD14F /* QueryStateUnitTests.m */; }; + 814916C71B66D44600EFD14F /* QueryUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161B1B66D44500EFD14F /* QueryUnitTests.m */; }; + 814916C81B66D44600EFD14F /* QueryUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161B1B66D44500EFD14F /* QueryUnitTests.m */; }; + 814916C91B66D44600EFD14F /* QueryUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161C1B66D44500EFD14F /* QueryUtilitiesTests.m */; }; + 814916CA1B66D44600EFD14F /* QueryUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161C1B66D44500EFD14F /* QueryUtilitiesTests.m */; }; + 814916CB1B66D44600EFD14F /* RelationStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161D1B66D44500EFD14F /* RelationStateTests.m */; }; + 814916CC1B66D44600EFD14F /* RelationStateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161D1B66D44500EFD14F /* RelationStateTests.m */; }; + 814916CD1B66D44600EFD14F /* RelationUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161E1B66D44500EFD14F /* RelationUnitTests.m */; }; + 814916CE1B66D44600EFD14F /* RelationUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161E1B66D44500EFD14F /* RelationUnitTests.m */; }; + 814916CF1B66D44600EFD14F /* RoleUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161F1B66D44500EFD14F /* RoleUnitTests.m */; }; + 814916D01B66D44600EFD14F /* RoleUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8149161F1B66D44500EFD14F /* RoleUnitTests.m */; }; + 814916D11B66D44600EFD14F /* SessionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916201B66D44500EFD14F /* SessionControllerTests.m */; }; + 814916D21B66D44600EFD14F /* SessionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916201B66D44500EFD14F /* SessionControllerTests.m */; }; + 814916D31B66D44600EFD14F /* SessionUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916211B66D44500EFD14F /* SessionUnitTests.m */; }; + 814916D41B66D44600EFD14F /* SessionUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916211B66D44500EFD14F /* SessionUnitTests.m */; }; + 814916D51B66D44600EFD14F /* SessionUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916221B66D44500EFD14F /* SessionUtilitiesTests.m */; }; + 814916D61B66D44600EFD14F /* SessionUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916221B66D44500EFD14F /* SessionUtilitiesTests.m */; }; + 814916D71B66D44600EFD14F /* SQLiteDatabaseTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916231B66D44500EFD14F /* SQLiteDatabaseTest.m */; }; + 814916D81B66D44600EFD14F /* SQLiteDatabaseTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916231B66D44500EFD14F /* SQLiteDatabaseTest.m */; }; + 814916D91B66D44600EFD14F /* URLSessionCommandRunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916241B66D44500EFD14F /* URLSessionCommandRunnerTests.m */; }; + 814916DA1B66D44600EFD14F /* URLSessionCommandRunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916241B66D44500EFD14F /* URLSessionCommandRunnerTests.m */; }; + 814916DB1B66D44600EFD14F /* UserCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916251B66D44500EFD14F /* UserCommandTests.m */; }; + 814916DC1B66D44600EFD14F /* UserCommandTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916251B66D44500EFD14F /* UserCommandTests.m */; }; + 814916DD1B66D44600EFD14F /* UserControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916261B66D44500EFD14F /* UserControllerTests.m */; }; + 814916DE1B66D44600EFD14F /* UserControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916261B66D44500EFD14F /* UserControllerTests.m */; }; + 814916DF1B66D44600EFD14F /* UserFileCodingLogicTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916271B66D44500EFD14F /* UserFileCodingLogicTests.m */; }; + 814916E01B66D44600EFD14F /* UserFileCodingLogicTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916271B66D44500EFD14F /* UserFileCodingLogicTests.m */; }; + 814916E11B66D44600EFD14F /* UserUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916281B66D44500EFD14F /* UserUnitTests.m */; }; + 814916E21B66D44600EFD14F /* UserUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814916281B66D44500EFD14F /* UserUnitTests.m */; }; + 81493AA41A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81493AA21A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h */; }; + 81493AA51A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81493AA21A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h */; }; + 81493AA61A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81493AA31A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m */; }; + 81493AA71A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81493AA31A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m */; }; + 814B64111A769EF500213055 /* PFLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 814B640E1A769EF500213055 /* PFLogger.h */; }; + 814B64121A769EF500213055 /* PFLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 814B640E1A769EF500213055 /* PFLogger.h */; }; + 814B64131A769EF500213055 /* PFLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 814B640F1A769EF500213055 /* PFLogger.m */; }; + 814B64141A769EF500213055 /* PFLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 814B640F1A769EF500213055 /* PFLogger.m */; }; + 814B64151A769EF500213055 /* PFLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 814B64101A769EF500213055 /* PFLogging.h */; }; + 814B64161A769EF500213055 /* PFLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 814B64101A769EF500213055 /* PFLogging.h */; }; + 814BCDF11B4DF63600007B7F /* PFUserState.h in Headers */ = {isa = PBXBuildFile; fileRef = 814BCDEF1B4DF63600007B7F /* PFUserState.h */; }; + 814BCDF21B4DF63600007B7F /* PFUserState.h in Headers */ = {isa = PBXBuildFile; fileRef = 814BCDEF1B4DF63600007B7F /* PFUserState.h */; }; + 814BCDF31B4DF63600007B7F /* PFUserState.m in Sources */ = {isa = PBXBuildFile; fileRef = 814BCDF01B4DF63600007B7F /* PFUserState.m */; }; + 814BCDF41B4DF63600007B7F /* PFUserState.m in Sources */ = {isa = PBXBuildFile; fileRef = 814BCDF01B4DF63600007B7F /* PFUserState.m */; }; + 814BCDF71B4DF66500007B7F /* PFMutableUserState.h in Headers */ = {isa = PBXBuildFile; fileRef = 814BCDF51B4DF66500007B7F /* PFMutableUserState.h */; }; + 814BCDF81B4DF66500007B7F /* PFMutableUserState.h in Headers */ = {isa = PBXBuildFile; fileRef = 814BCDF51B4DF66500007B7F /* PFMutableUserState.h */; }; + 814BCDF91B4DF66500007B7F /* PFMutableUserState.m in Sources */ = {isa = PBXBuildFile; fileRef = 814BCDF61B4DF66500007B7F /* PFMutableUserState.m */; }; + 814BCDFA1B4DF66500007B7F /* PFMutableUserState.m in Sources */ = {isa = PBXBuildFile; fileRef = 814BCDF61B4DF66500007B7F /* PFMutableUserState.m */; }; + 814BCDFC1B4DF7E800007B7F /* PFUserState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 814BCDFB1B4DF7E800007B7F /* PFUserState_Private.h */; }; + 814BCDFD1B4DF7E800007B7F /* PFUserState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 814BCDFB1B4DF7E800007B7F /* PFUserState_Private.h */; }; + 814CAD6D1A76ACB600EA4269 /* PFRESTUserCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81AFE0E61A1FDB7900AB6CB3 /* PFRESTUserCommand.m */; }; + 815619001A1F79AC0076504A /* PFDateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 815618FE1A1F79AC0076504A /* PFDateFormatter.h */; }; + 815619011A1F79AC0076504A /* PFDateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 815618FE1A1F79AC0076504A /* PFDateFormatter.h */; }; + 815619021A1F79AC0076504A /* PFDateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 815618FF1A1F79AC0076504A /* PFDateFormatter.m */; }; + 815619031A1F79AC0076504A /* PFDateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 815618FF1A1F79AC0076504A /* PFDateFormatter.m */; }; + 815868E21AF9818D009A5751 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8103FA44198FC267000BAE3F /* Bolts.framework */; }; + 815868E71AF98731009A5751 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8103FA44198FC267000BAE3F /* Bolts.framework */; settings = {ATTRIBUTES = (Required, ); }; }; + 815960A11ABCA3B30069EBCC /* PFFileManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8159609F1ABCA3B30069EBCC /* PFFileManager.h */; }; + 815960A21ABCA3B30069EBCC /* PFFileManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8159609F1ABCA3B30069EBCC /* PFFileManager.h */; }; + 815960A31ABCA3B30069EBCC /* PFFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 815960A01ABCA3B30069EBCC /* PFFileManager.m */; }; + 815960A41ABCA3B30069EBCC /* PFFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 815960A01ABCA3B30069EBCC /* PFFileManager.m */; }; + 815EE8F519F976D50076FE5D /* PFRESTCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE8EE19F976D50076FE5D /* PFRESTCommand.h */; }; + 815EE8F619F976D50076FE5D /* PFRESTCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE8EE19F976D50076FE5D /* PFRESTCommand.h */; }; + 815EE8F719F976D50076FE5D /* PFRESTCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE8EF19F976D50076FE5D /* PFRESTCommand.m */; }; + 815EE8F819F976D50076FE5D /* PFRESTCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE8EF19F976D50076FE5D /* PFRESTCommand.m */; }; + 815EE8F919F976D50076FE5D /* PFRESTCommand_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE8F019F976D50076FE5D /* PFRESTCommand_Private.h */; }; + 815EE8FA19F976D50076FE5D /* PFRESTCommand_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE8F019F976D50076FE5D /* PFRESTCommand_Private.h */; }; + 815EE91D19F987910076FE5D /* PFRESTCloudCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE91B19F987910076FE5D /* PFRESTCloudCommand.h */; }; + 815EE91E19F987910076FE5D /* PFRESTCloudCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE91B19F987910076FE5D /* PFRESTCloudCommand.h */; }; + 815EE91F19F987910076FE5D /* PFRESTCloudCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE91C19F987910076FE5D /* PFRESTCloudCommand.m */; }; + 815EE92019F987910076FE5D /* PFRESTCloudCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE91C19F987910076FE5D /* PFRESTCloudCommand.m */; }; + 815EE92319F989380076FE5D /* PFRESTConfigCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE92119F989380076FE5D /* PFRESTConfigCommand.h */; }; + 815EE92419F989390076FE5D /* PFRESTConfigCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE92119F989380076FE5D /* PFRESTConfigCommand.h */; }; + 815EE92519F989390076FE5D /* PFRESTConfigCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE92219F989380076FE5D /* PFRESTConfigCommand.m */; }; + 815EE92619F989390076FE5D /* PFRESTConfigCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE92219F989380076FE5D /* PFRESTConfigCommand.m */; }; + 815EE93C19FA56D20076FE5D /* PFHTTPURLRequestConstructor.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE93A19FA56D20076FE5D /* PFHTTPURLRequestConstructor.h */; }; + 815EE93D19FA56D20076FE5D /* PFHTTPURLRequestConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE93B19FA56D20076FE5D /* PFHTTPURLRequestConstructor.m */; }; + 815EE94019FA5A390076FE5D /* PFHTTPRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE93F19FA5A390076FE5D /* PFHTTPRequest.h */; }; + 815EE94119FA5A390076FE5D /* PFHTTPRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE93F19FA5A390076FE5D /* PFHTTPRequest.h */; }; + 815EE94219FA88FB0076FE5D /* PFHTTPURLRequestConstructor.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE93A19FA56D20076FE5D /* PFHTTPURLRequestConstructor.h */; }; + 815EE94319FA88FF0076FE5D /* PFHTTPURLRequestConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE93B19FA56D20076FE5D /* PFHTTPURLRequestConstructor.m */; }; + 815EE94619FAD12F0076FE5D /* PFRESTQueryCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE94419FAD12F0076FE5D /* PFRESTQueryCommand.h */; }; + 815EE94719FAD12F0076FE5D /* PFRESTQueryCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 815EE94419FAD12F0076FE5D /* PFRESTQueryCommand.h */; }; + 815EE94819FAD12F0076FE5D /* PFRESTQueryCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE94519FAD12F0076FE5D /* PFRESTQueryCommand.m */; }; + 815EE94919FAD12F0076FE5D /* PFRESTQueryCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 815EE94519FAD12F0076FE5D /* PFRESTQueryCommand.m */; }; + 8166FB9B1B4F2F08003841A2 /* PFUserConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FB991B4F2F08003841A2 /* PFUserConstants.h */; }; + 8166FB9C1B4F2F08003841A2 /* PFUserConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FB991B4F2F08003841A2 /* PFUserConstants.h */; }; + 8166FB9D1B4F2F08003841A2 /* PFUserConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FB9A1B4F2F08003841A2 /* PFUserConstants.m */; }; + 8166FB9E1B4F2F08003841A2 /* PFUserConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FB9A1B4F2F08003841A2 /* PFUserConstants.m */; }; + 8166FC581B503741003841A2 /* PFAnalytics_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC571B503741003841A2 /* PFAnalytics_Private.h */; }; + 8166FC591B503741003841A2 /* PFAnalytics_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC571B503741003841A2 /* PFAnalytics_Private.h */; }; + 8166FC5B1B50374B003841A2 /* PFConfig_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC5A1B50374B003841A2 /* PFConfig_Private.h */; }; + 8166FC5C1B50374B003841A2 /* PFConfig_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC5A1B50374B003841A2 /* PFConfig_Private.h */; }; + 8166FC5E1B503755003841A2 /* PFObjectPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC5D1B503755003841A2 /* PFObjectPrivate.h */; }; + 8166FC5F1B503755003841A2 /* PFObjectPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC5D1B503755003841A2 /* PFObjectPrivate.h */; }; + 8166FC631B50375D003841A2 /* PFOperationSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC611B50375D003841A2 /* PFOperationSet.h */; }; + 8166FC641B50375D003841A2 /* PFOperationSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC611B50375D003841A2 /* PFOperationSet.h */; }; + 8166FC651B50375D003841A2 /* PFOperationSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC621B50375D003841A2 /* PFOperationSet.m */; }; + 8166FC661B50375D003841A2 /* PFOperationSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC621B50375D003841A2 /* PFOperationSet.m */; }; + 8166FC6F1B50376D003841A2 /* PFOfflineObjectController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC691B50376D003841A2 /* PFOfflineObjectController.h */; }; + 8166FC701B50376D003841A2 /* PFOfflineObjectController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC691B50376D003841A2 /* PFOfflineObjectController.h */; }; + 8166FC711B50376D003841A2 /* PFOfflineObjectController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC6A1B50376D003841A2 /* PFOfflineObjectController.m */; }; + 8166FC721B50376D003841A2 /* PFOfflineObjectController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC6A1B50376D003841A2 /* PFOfflineObjectController.m */; }; + 8166FC731B50376D003841A2 /* PFObjectController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC6B1B50376D003841A2 /* PFObjectController.h */; }; + 8166FC741B50376D003841A2 /* PFObjectController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC6B1B50376D003841A2 /* PFObjectController.h */; }; + 8166FC751B50376D003841A2 /* PFObjectController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC6C1B50376D003841A2 /* PFObjectController.m */; }; + 8166FC761B50376D003841A2 /* PFObjectController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC6C1B50376D003841A2 /* PFObjectController.m */; }; + 8166FC771B50376D003841A2 /* PFObjectController_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC6D1B50376D003841A2 /* PFObjectController_Private.h */; }; + 8166FC781B50376D003841A2 /* PFObjectController_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC6D1B50376D003841A2 /* PFObjectController_Private.h */; }; + 8166FC791B50376D003841A2 /* PFObjectControlling.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC6E1B50376D003841A2 /* PFObjectControlling.h */; }; + 8166FC7A1B50376D003841A2 /* PFObjectControlling.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC6E1B50376D003841A2 /* PFObjectControlling.h */; }; + 8166FC7C1B503787003841A2 /* PFFile_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC7B1B503787003841A2 /* PFFile_Private.h */; }; + 8166FC7D1B503787003841A2 /* PFFile_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC7B1B503787003841A2 /* PFFile_Private.h */; }; + 8166FC831B503794003841A2 /* PFInstallationIdentifierStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC7F1B503794003841A2 /* PFInstallationIdentifierStore.h */; }; + 8166FC841B503794003841A2 /* PFInstallationIdentifierStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC7F1B503794003841A2 /* PFInstallationIdentifierStore.h */; }; + 8166FC851B503794003841A2 /* PFInstallationIdentifierStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC801B503794003841A2 /* PFInstallationIdentifierStore.m */; }; + 8166FC861B503794003841A2 /* PFInstallationIdentifierStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC801B503794003841A2 /* PFInstallationIdentifierStore.m */; }; + 8166FC871B503794003841A2 /* PFInstallationIdentifierStore_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC811B503794003841A2 /* PFInstallationIdentifierStore_Private.h */; }; + 8166FC881B503794003841A2 /* PFInstallationIdentifierStore_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC811B503794003841A2 /* PFInstallationIdentifierStore_Private.h */; }; + 8166FC891B503794003841A2 /* PFInstallationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC821B503794003841A2 /* PFInstallationPrivate.h */; }; + 8166FC8A1B503794003841A2 /* PFInstallationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC821B503794003841A2 /* PFInstallationPrivate.h */; }; + 8166FC901B5037F5003841A2 /* PFProduct+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC8C1B5037F4003841A2 /* PFProduct+Private.h */; }; + 8166FC911B5037F5003841A2 /* PFProductsRequestHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC8E1B5037F4003841A2 /* PFProductsRequestHandler.h */; }; + 8166FC921B5037F5003841A2 /* PFProductsRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FC8F1B5037F5003841A2 /* PFProductsRequestHandler.m */; }; + 8166FC941B503809003841A2 /* PFPushPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC931B503809003841A2 /* PFPushPrivate.h */; }; + 8166FC951B503809003841A2 /* PFPushPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC931B503809003841A2 /* PFPushPrivate.h */; }; + 8166FC971B50381B003841A2 /* PFQueryPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC961B50381B003841A2 /* PFQueryPrivate.h */; }; + 8166FC981B50381B003841A2 /* PFQueryPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC961B50381B003841A2 /* PFQueryPrivate.h */; }; + 8166FC9A1B503830003841A2 /* PFSession_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC991B503830003841A2 /* PFSession_Private.h */; }; + 8166FC9B1B503830003841A2 /* PFSession_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC991B503830003841A2 /* PFSession_Private.h */; }; + 8166FC9D1B503847003841A2 /* PFUserPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC9C1B503847003841A2 /* PFUserPrivate.h */; }; + 8166FC9E1B503847003841A2 /* PFUserPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FC9C1B503847003841A2 /* PFUserPrivate.h */; }; + 8166FCB01B503886003841A2 /* PFOfflineQueryLogic.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCA11B503886003841A2 /* PFOfflineQueryLogic.h */; }; + 8166FCB11B503886003841A2 /* PFOfflineQueryLogic.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCA11B503886003841A2 /* PFOfflineQueryLogic.h */; }; + 8166FCB21B503886003841A2 /* PFOfflineQueryLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCA21B503886003841A2 /* PFOfflineQueryLogic.m */; }; + 8166FCB31B503886003841A2 /* PFOfflineQueryLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCA21B503886003841A2 /* PFOfflineQueryLogic.m */; }; + 8166FCB41B503886003841A2 /* PFOfflineStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCA41B503886003841A2 /* PFOfflineStore.h */; }; + 8166FCB51B503886003841A2 /* PFOfflineStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCA41B503886003841A2 /* PFOfflineStore.h */; }; + 8166FCB61B503886003841A2 /* PFOfflineStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCA51B503886003841A2 /* PFOfflineStore.m */; }; + 8166FCB71B503886003841A2 /* PFOfflineStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCA51B503886003841A2 /* PFOfflineStore.m */; }; + 8166FCB81B503886003841A2 /* PFPin.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCA71B503886003841A2 /* PFPin.h */; }; + 8166FCB91B503886003841A2 /* PFPin.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCA71B503886003841A2 /* PFPin.h */; }; + 8166FCBA1B503886003841A2 /* PFPin.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCA81B503886003841A2 /* PFPin.m */; }; + 8166FCBB1B503886003841A2 /* PFPin.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCA81B503886003841A2 /* PFPin.m */; }; + 8166FCBC1B503886003841A2 /* PFSQLiteDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCAA1B503886003841A2 /* PFSQLiteDatabase.h */; }; + 8166FCBD1B503886003841A2 /* PFSQLiteDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCAA1B503886003841A2 /* PFSQLiteDatabase.h */; }; + 8166FCBE1B503886003841A2 /* PFSQLiteDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCAB1B503886003841A2 /* PFSQLiteDatabase.m */; }; + 8166FCBF1B503886003841A2 /* PFSQLiteDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCAB1B503886003841A2 /* PFSQLiteDatabase.m */; }; + 8166FCC01B503886003841A2 /* PFSQLiteDatabaseResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCAC1B503886003841A2 /* PFSQLiteDatabaseResult.h */; }; + 8166FCC11B503886003841A2 /* PFSQLiteDatabaseResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCAC1B503886003841A2 /* PFSQLiteDatabaseResult.h */; }; + 8166FCC21B503886003841A2 /* PFSQLiteDatabaseResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCAD1B503886003841A2 /* PFSQLiteDatabaseResult.m */; }; + 8166FCC31B503886003841A2 /* PFSQLiteDatabaseResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCAD1B503886003841A2 /* PFSQLiteDatabaseResult.m */; }; + 8166FCC41B503886003841A2 /* PFSQLiteStatement.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCAE1B503886003841A2 /* PFSQLiteStatement.h */; }; + 8166FCC51B503886003841A2 /* PFSQLiteStatement.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCAE1B503886003841A2 /* PFSQLiteStatement.h */; }; + 8166FCC61B503886003841A2 /* PFSQLiteStatement.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCAF1B503886003841A2 /* PFSQLiteStatement.m */; }; + 8166FCC71B503886003841A2 /* PFSQLiteStatement.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCAF1B503886003841A2 /* PFSQLiteStatement.m */; }; + 8166FCCC1B5038B7003841A2 /* PFPaymentTransactionObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCC91B5038B7003841A2 /* PFPaymentTransactionObserver.h */; }; + 8166FCCD1B5038B7003841A2 /* PFPaymentTransactionObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCCA1B5038B7003841A2 /* PFPaymentTransactionObserver.m */; }; + 8166FCCE1B5038B7003841A2 /* PFPaymentTransactionObserver_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCCB1B5038B7003841A2 /* PFPaymentTransactionObserver_Private.h */; }; + 8166FCD91B503914003841A2 /* PFUserAuthenticationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD11B503914003841A2 /* PFUserAuthenticationController.h */; }; + 8166FCDA1B503914003841A2 /* PFUserAuthenticationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD11B503914003841A2 /* PFUserAuthenticationController.h */; }; + 8166FCDB1B503914003841A2 /* PFUserAuthenticationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCD21B503914003841A2 /* PFUserAuthenticationController.m */; }; + 8166FCDC1B503914003841A2 /* PFUserAuthenticationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCD21B503914003841A2 /* PFUserAuthenticationController.m */; }; + 8166FCDD1B503914003841A2 /* PFAnonymousAuthenticationProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD51B503914003841A2 /* PFAnonymousAuthenticationProvider.h */; }; + 8166FCDE1B503914003841A2 /* PFAnonymousAuthenticationProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD51B503914003841A2 /* PFAnonymousAuthenticationProvider.h */; }; + 8166FCDF1B503914003841A2 /* PFAnonymousAuthenticationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCD61B503914003841A2 /* PFAnonymousAuthenticationProvider.m */; }; + 8166FCE01B503914003841A2 /* PFAnonymousAuthenticationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCD61B503914003841A2 /* PFAnonymousAuthenticationProvider.m */; }; + 8166FCE11B503914003841A2 /* PFAnonymousUtils_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD71B503914003841A2 /* PFAnonymousUtils_Private.h */; }; + 8166FCE21B503914003841A2 /* PFAnonymousUtils_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD71B503914003841A2 /* PFAnonymousUtils_Private.h */; }; + 8166FCE31B503914003841A2 /* PFAuthenticationProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD81B503914003841A2 /* PFAuthenticationProvider.h */; }; + 8166FCE41B503914003841A2 /* PFAuthenticationProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCD81B503914003841A2 /* PFAuthenticationProvider.h */; }; + 8166FCE81B504083003841A2 /* PFPushManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCE61B504083003841A2 /* PFPushManager.h */; }; + 8166FCE91B504083003841A2 /* PFPushManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8166FCE61B504083003841A2 /* PFPushManager.h */; }; + 8166FCEA1B504083003841A2 /* PFPushManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCE71B504083003841A2 /* PFPushManager.m */; }; + 8166FCEB1B504083003841A2 /* PFPushManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8166FCE71B504083003841A2 /* PFPushManager.m */; }; + 8169701C19BE94BB00EC1D1F /* PFDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 919311D619AE5EB20008FF12 /* PFDecoder.m */; }; + 816AC9BA1A3F48250031D94C /* PFApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = 816AC9B81A3F48250031D94C /* PFApplication.h */; }; + 816AC9BB1A3F48250031D94C /* PFApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 816AC9B91A3F48250031D94C /* PFApplication.m */; }; + 816F44741A8E8933009CDB32 /* Parse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81C3821C19CCA89E0066284A /* Parse.framework */; }; + 816F44761A8E8933009CDB32 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 498C29FE1551DC450034BB80 /* StoreKit.framework */; }; + 816F44771A8E8933009CDB32 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 6393F38B15D3018400C4F78D /* libsqlite3.dylib */; }; + 816F44781A8E8933009CDB32 /* Accounts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63CA84EA1612660F002E09F8 /* Accounts.framework */; }; + 816F44791A8E8933009CDB32 /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63CBA36B1612829C0062C84A /* Social.framework */; }; + 816F447C1A8E8933009CDB32 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8103FA42198FC25B000BAE3F /* Bolts.framework */; }; + 816F97111A93FAC400CADE60 /* PFNullability.h in Headers */ = {isa = PBXBuildFile; fileRef = 816F97101A93FAC400CADE60 /* PFNullability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 816F97121A93FAC400CADE60 /* PFNullability.h in Headers */ = {isa = PBXBuildFile; fileRef = 816F97101A93FAC400CADE60 /* PFNullability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8171E99F19AE091000EAE6C1 /* PFFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 81DEF07E199C42A300D86A21 /* PFFile.m */; }; + 8171E9BA19AE37F000EAE6C1 /* PFThreadsafety.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D049919A3B84500BEE20F /* PFThreadsafety.h */; }; + 8171E9BB19AE37F500EAE6C1 /* PFThreadsafety.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D049A19A3B84500BEE20F /* PFThreadsafety.m */; }; + 818AAA6F19D36B1C00FC1B3C /* Parse.h in Headers */ = {isa = PBXBuildFile; fileRef = 09EEA12D1434FB1F00E3A3FA /* Parse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7019D36B1C00FC1B3C /* PFACL.h in Headers */ = {isa = PBXBuildFile; fileRef = 64C47802147336C70092082F /* PFACL.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7119D36B1C00FC1B3C /* PFAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = 9739513816B9D28E0010B884 /* PFAnalytics.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7219D36B1C00FC1B3C /* PFAnonymousUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 638CBBB415191435004F54E4 /* PFAnonymousUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7319D36B1C00FC1B3C /* PFCloud.h in Headers */ = {isa = PBXBuildFile; fileRef = 805D3D9F15E31241007E8D10 /* PFCloud.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7419D36B1C00FC1B3C /* PFConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EB6632198A7FA600851598 /* PFConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7519D36B1C00FC1B3C /* PFConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABEB13D791770095FEFA /* PFConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7619D36B1C00FC1B3C /* PFFile.h in Headers */ = {isa = PBXBuildFile; fileRef = 81DEF07D199C42A300D86A21 /* PFFile.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7719D36B1C00FC1B3C /* PFGeoPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 09B119F614880776002B5594 /* PFGeoPoint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7819D36B1C00FC1B3C /* PFInstallation.h in Headers */ = {isa = PBXBuildFile; fileRef = 44B78E11157D21B000A5E97F /* PFInstallation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7919D36B1C00FC1B3C /* PFObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABED13D791770095FEFA /* PFObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7A19D36B1C00FC1B3C /* PFProduct.h in Headers */ = {isa = PBXBuildFile; fileRef = 499E425515B6409000A2C28E /* PFProduct.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7B19D36B1C00FC1B3C /* PFPurchase.h in Headers */ = {isa = PBXBuildFile; fileRef = 49FDE2EC158C138F00126F64 /* PFPurchase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7C19D36B1C00FC1B3C /* PFPush.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABF113D791770095FEFA /* PFPush.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7D19D36B1C00FC1B3C /* PFQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABF313D791770095FEFA /* PFQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7E19D36B1C00FC1B3C /* PFRelation.h in Headers */ = {isa = PBXBuildFile; fileRef = 8083B859155DAB1B0023EEFA /* PFRelation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA7F19D36B1C00FC1B3C /* PFRole.h in Headers */ = {isa = PBXBuildFile; fileRef = 63723F6D1565A085007A1A73 /* PFRole.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA8119D36B1C00FC1B3C /* PFUser.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABF513D791770095FEFA /* PFUser.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA8219D36B1C00FC1B3C /* PFObject+Subclass.h in Headers */ = {isa = PBXBuildFile; fileRef = E9BBE98E16D18E5800CD7B52 /* PFObject+Subclass.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA8319D36B1C00FC1B3C /* PFSubclassing.h in Headers */ = {isa = PBXBuildFile; fileRef = E9E81E8316EEF93E001D034F /* PFSubclassing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818AAA8419D36B1C00FC1B3C /* PFNetworkActivityIndicatorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 81DEF089199D555800D86A21 /* PFNetworkActivityIndicatorManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 818D586A1B5D9F4B00813989 /* PFURLSessionCommandRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D58681B5D9F4B00813989 /* PFURLSessionCommandRunner.h */; }; + 818D586B1B5D9F4B00813989 /* PFURLSessionCommandRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D58681B5D9F4B00813989 /* PFURLSessionCommandRunner.h */; }; + 818D586C1B5D9F4B00813989 /* PFURLSessionCommandRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D58691B5D9F4B00813989 /* PFURLSessionCommandRunner.m */; }; + 818D586D1B5D9F4B00813989 /* PFURLSessionCommandRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D58691B5D9F4B00813989 /* PFURLSessionCommandRunner.m */; }; + 818D586F1B5DA43800813989 /* PFCommandRunning.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D586E1B5DA43800813989 /* PFCommandRunning.m */; }; + 818D58701B5DA43800813989 /* PFCommandRunning.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D586E1B5DA43800813989 /* PFCommandRunning.m */; }; + 818D58731B5DAAFE00813989 /* PFCommandRunningConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D58711B5DAAFE00813989 /* PFCommandRunningConstants.h */; }; + 818D58741B5DAAFE00813989 /* PFCommandRunningConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D58711B5DAAFE00813989 /* PFCommandRunningConstants.h */; }; + 818D58751B5DAAFE00813989 /* PFCommandRunningConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D58721B5DAAFE00813989 /* PFCommandRunningConstants.m */; }; + 818D58761B5DAAFE00813989 /* PFCommandRunningConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D58721B5DAAFE00813989 /* PFCommandRunningConstants.m */; }; + 818D6F141B3C8D1900F94C82 /* PFObjectLocalIdStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D6F121B3C8D1900F94C82 /* PFObjectLocalIdStore.h */; }; + 818D6F151B3C8D1900F94C82 /* PFObjectLocalIdStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D6F121B3C8D1900F94C82 /* PFObjectLocalIdStore.h */; }; + 818D6F161B3C8D1900F94C82 /* PFObjectLocalIdStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D6F131B3C8D1900F94C82 /* PFObjectLocalIdStore.m */; }; + 818D6F171B3C8D1900F94C82 /* PFObjectLocalIdStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D6F131B3C8D1900F94C82 /* PFObjectLocalIdStore.m */; }; + 818D6F201B3DCB5A00F94C82 /* PFObjectEstimatedData.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D6F1E1B3DCB5A00F94C82 /* PFObjectEstimatedData.h */; }; + 818D6F211B3DCB5A00F94C82 /* PFObjectEstimatedData.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D6F1E1B3DCB5A00F94C82 /* PFObjectEstimatedData.h */; }; + 818D6F221B3DCB5A00F94C82 /* PFObjectEstimatedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D6F1F1B3DCB5A00F94C82 /* PFObjectEstimatedData.m */; }; + 818D6F231B3DCB5A00F94C82 /* PFObjectEstimatedData.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D6F1F1B3DCB5A00F94C82 /* PFObjectEstimatedData.m */; }; + 81951F161ACB90DA00E142EB /* PFJSONSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 81951F141ACB90DA00E142EB /* PFJSONSerialization.h */; }; + 81951F171ACB90DA00E142EB /* PFJSONSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 81951F141ACB90DA00E142EB /* PFJSONSerialization.h */; }; + 81951F181ACB90DA00E142EB /* PFJSONSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 81951F151ACB90DA00E142EB /* PFJSONSerialization.m */; }; + 81951F191ACB90DA00E142EB /* PFJSONSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 81951F151ACB90DA00E142EB /* PFJSONSerialization.m */; }; + 8196D55B1B0AB64B000465A1 /* PFAnalyticsController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8196D5591B0AB64B000465A1 /* PFAnalyticsController.h */; }; + 8196D55C1B0AB64B000465A1 /* PFAnalyticsController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8196D5591B0AB64B000465A1 /* PFAnalyticsController.h */; }; + 8196D55D1B0AB64B000465A1 /* PFAnalyticsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8196D55A1B0AB64B000465A1 /* PFAnalyticsController.m */; }; + 8196D55E1B0AB64B000465A1 /* PFAnalyticsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8196D55A1B0AB64B000465A1 /* PFAnalyticsController.m */; }; + 8196D5611B0AB661000465A1 /* PFAnalyticsUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 8196D55F1B0AB661000465A1 /* PFAnalyticsUtilities.h */; }; + 8196D5621B0AB661000465A1 /* PFAnalyticsUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 8196D55F1B0AB661000465A1 /* PFAnalyticsUtilities.h */; }; + 8196D5631B0AB661000465A1 /* PFAnalyticsUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8196D5601B0AB661000465A1 /* PFAnalyticsUtilities.m */; }; + 8196D5641B0AB661000465A1 /* PFAnalyticsUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8196D5601B0AB661000465A1 /* PFAnalyticsUtilities.m */; }; + 8196D58D1B0BD23B000465A1 /* PFCoreManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8196D58B1B0BD23B000465A1 /* PFCoreManager.h */; }; + 8196D58E1B0BD23B000465A1 /* PFCoreManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8196D58B1B0BD23B000465A1 /* PFCoreManager.h */; }; + 8196D58F1B0BD23B000465A1 /* PFCoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8196D58C1B0BD23B000465A1 /* PFCoreManager.m */; }; + 8196D5901B0BD23B000465A1 /* PFCoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8196D58C1B0BD23B000465A1 /* PFCoreManager.m */; }; + 81986CA51A412277007B8860 /* PFApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 816AC9B91A3F48250031D94C /* PFApplication.m */; }; + 819A4B081A67330200D01241 /* PFHash.h in Headers */ = {isa = PBXBuildFile; fileRef = 819A4B061A67330200D01241 /* PFHash.h */; }; + 819A4B091A67330200D01241 /* PFHash.h in Headers */ = {isa = PBXBuildFile; fileRef = 819A4B061A67330200D01241 /* PFHash.h */; }; + 819A4B0A1A67330200D01241 /* PFHash.m in Sources */ = {isa = PBXBuildFile; fileRef = 819A4B071A67330200D01241 /* PFHash.m */; }; + 819A4B0B1A67330200D01241 /* PFHash.m in Sources */ = {isa = PBXBuildFile; fileRef = 819A4B071A67330200D01241 /* PFHash.m */; }; + 81A016271B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A016261B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m */; }; + 81A016281B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A016261B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m */; }; + 81A2458D1B1E99C6006A6953 /* PFFieldOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A2458B1B1E99C6006A6953 /* PFFieldOperation.h */; }; + 81A2458E1B1E99C6006A6953 /* PFFieldOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A2458B1B1E99C6006A6953 /* PFFieldOperation.h */; }; + 81A2458F1B1E99C6006A6953 /* PFFieldOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A2458C1B1E99C6006A6953 /* PFFieldOperation.m */; }; + 81A245901B1E99C6006A6953 /* PFFieldOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A2458C1B1E99C6006A6953 /* PFFieldOperation.m */; }; + 81A245931B1E99EA006A6953 /* PFFieldOperationDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A245911B1E99EA006A6953 /* PFFieldOperationDecoder.h */; }; + 81A245941B1E99EA006A6953 /* PFFieldOperationDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A245911B1E99EA006A6953 /* PFFieldOperationDecoder.h */; }; + 81A245951B1E99EA006A6953 /* PFFieldOperationDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A245921B1E99EA006A6953 /* PFFieldOperationDecoder.m */; }; + 81A245961B1E99EA006A6953 /* PFFieldOperationDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A245921B1E99EA006A6953 /* PFFieldOperationDecoder.m */; }; + 81A245F21B1FB188006A6953 /* PFDataProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A245F11B1FB188006A6953 /* PFDataProvider.h */; }; + 81A245F31B1FB188006A6953 /* PFDataProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A245F11B1FB188006A6953 /* PFDataProvider.h */; }; + 81A715A41B423A4100A504FC /* PFObjectUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A715A21B423A4100A504FC /* PFObjectUtilities.h */; }; + 81A715A51B423A4100A504FC /* PFObjectUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 81A715A21B423A4100A504FC /* PFObjectUtilities.h */; }; + 81A715A61B423A4100A504FC /* PFObjectUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A715A31B423A4100A504FC /* PFObjectUtilities.m */; }; + 81A715A71B423A4100A504FC /* PFObjectUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A715A31B423A4100A504FC /* PFObjectUtilities.m */; }; + 81ABC0FE1B5427EC00BA9009 /* PFUserController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81ABC0FC1B5427EC00BA9009 /* PFUserController.h */; }; + 81ABC0FF1B5427EC00BA9009 /* PFUserController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81ABC0FC1B5427EC00BA9009 /* PFUserController.h */; }; + 81ABC1001B5427EC00BA9009 /* PFUserController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81ABC0FD1B5427EC00BA9009 /* PFUserController.m */; }; + 81ABC1011B5427EC00BA9009 /* PFUserController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81ABC0FD1B5427EC00BA9009 /* PFUserController.m */; }; + 81AFA6711B0ECD4E000763C0 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81AFA6701B0ECD4E000763C0 /* libOCMock.a */; }; + 81AFA6761B0ECF90000763C0 /* OCMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81AFA6751B0ECF90000763C0 /* OCMock.framework */; }; + 81AFA6771B0ECF97000763C0 /* OCMock.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 81AFA6751B0ECF90000763C0 /* OCMock.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 81AFE0E71A1FDB7900AB6CB3 /* PFRESTUserCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81AFE0E51A1FDB7900AB6CB3 /* PFRESTUserCommand.h */; }; + 81AFE0E91A1FDB7D00AB6CB3 /* PFRESTUserCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81AFE0E61A1FDB7900AB6CB3 /* PFRESTUserCommand.m */; }; + 81B3F2011AC5DA7600A92677 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 6393F38B15D3018400C4F78D /* libsqlite3.dylib */; }; + 81B3F2021AC5DAA400A92677 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 970110191630B1FE00AB761E /* Cocoa.framework */; }; + 81B3F2541AC9D4E100A92677 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 81B3F2531AC9D4E100A92677 /* Localizable.strings */; }; + 81B3F2551AC9D4E100A92677 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 81B3F2531AC9D4E100A92677 /* Localizable.strings */; }; + 81BB6E211B0E7A1A00465C38 /* PFBase64Encoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BB6E1F1B0E7A1A00465C38 /* PFBase64Encoder.h */; }; + 81BB6E221B0E7A1A00465C38 /* PFBase64Encoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BB6E1F1B0E7A1A00465C38 /* PFBase64Encoder.h */; }; + 81BB6E231B0E7A1A00465C38 /* PFBase64Encoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BB6E201B0E7A1A00465C38 /* PFBase64Encoder.m */; }; + 81BB6E241B0E7A1A00465C38 /* PFBase64Encoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BB6E201B0E7A1A00465C38 /* PFBase64Encoder.m */; }; + 81BBE12F19FFCB3700622646 /* PFURLConstructor.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BBE12D19FFCB3700622646 /* PFURLConstructor.h */; }; + 81BBE13019FFCB3700622646 /* PFURLConstructor.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BBE12D19FFCB3700622646 /* PFURLConstructor.h */; }; + 81BBE13119FFCB3700622646 /* PFURLConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BBE12E19FFCB3700622646 /* PFURLConstructor.m */; }; + 81BBE13219FFCB3700622646 /* PFURLConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BBE12E19FFCB3700622646 /* PFURLConstructor.m */; }; + 81BBE1351A0062B800622646 /* PFRESTAnalyticsCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BBE1331A0062B800622646 /* PFRESTAnalyticsCommand.h */; }; + 81BBE1361A0062B800622646 /* PFRESTAnalyticsCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BBE1331A0062B800622646 /* PFRESTAnalyticsCommand.h */; }; + 81BBE1371A0062B800622646 /* PFRESTAnalyticsCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BBE1341A0062B800622646 /* PFRESTAnalyticsCommand.m */; }; + 81BBE1381A0062B800622646 /* PFRESTAnalyticsCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BBE1341A0062B800622646 /* PFRESTAnalyticsCommand.m */; }; + 81BCB4C41B744626006659CB /* PFURLSessionDataTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4BD1B744626006659CB /* PFURLSessionDataTaskDelegate.h */; }; + 81BCB4C51B744626006659CB /* PFURLSessionDataTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4BD1B744626006659CB /* PFURLSessionDataTaskDelegate.h */; }; + 81BCB4C61B744626006659CB /* PFURLSessionDataTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BCB4BE1B744626006659CB /* PFURLSessionDataTaskDelegate.m */; }; + 81BCB4C71B744626006659CB /* PFURLSessionDataTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BCB4BE1B744626006659CB /* PFURLSessionDataTaskDelegate.m */; }; + 81BCB4C81B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4BF1B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h */; }; + 81BCB4C91B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4BF1B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h */; }; + 81BCB4CA1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4C01B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h */; }; + 81BCB4CB1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4C01B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h */; }; + 81BCB4CC1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BCB4C11B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m */; }; + 81BCB4CD1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BCB4C11B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m */; }; + 81BCB4CE1B744626006659CB /* PFURLSessionUploadTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4C21B744626006659CB /* PFURLSessionUploadTaskDelegate.h */; }; + 81BCB4CF1B744626006659CB /* PFURLSessionUploadTaskDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BCB4C21B744626006659CB /* PFURLSessionUploadTaskDelegate.h */; }; + 81BCB4D01B744626006659CB /* PFURLSessionUploadTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BCB4C31B744626006659CB /* PFURLSessionUploadTaskDelegate.m */; }; + 81BCB4D11B744626006659CB /* PFURLSessionUploadTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BCB4C31B744626006659CB /* PFURLSessionUploadTaskDelegate.m */; }; + 81BF4AB61B0BF3E500A3D75B /* PFConfigController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BF4AB41B0BF3E500A3D75B /* PFConfigController.h */; }; + 81BF4AB71B0BF3E500A3D75B /* PFConfigController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BF4AB41B0BF3E500A3D75B /* PFConfigController.h */; }; + 81BF4AB81B0BF3E500A3D75B /* PFConfigController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BF4AB51B0BF3E500A3D75B /* PFConfigController.m */; }; + 81BF4AB91B0BF3E500A3D75B /* PFConfigController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BF4AB51B0BF3E500A3D75B /* PFConfigController.m */; }; + 81BF4ABC1B0BF64B00A3D75B /* PFCurrentConfigController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BF4ABA1B0BF64B00A3D75B /* PFCurrentConfigController.h */; }; + 81BF4ABD1B0BF64B00A3D75B /* PFCurrentConfigController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BF4ABA1B0BF64B00A3D75B /* PFCurrentConfigController.h */; }; + 81BF4ABE1B0BF64B00A3D75B /* PFCurrentConfigController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BF4ABB1B0BF64B00A3D75B /* PFCurrentConfigController.m */; }; + 81BF4ABF1B0BF64B00A3D75B /* PFCurrentConfigController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81BF4ABB1B0BF64B00A3D75B /* PFCurrentConfigController.m */; }; + 81C09F891AF97EA70043B49C /* ParseOSX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97010FAC1630B18F00AB761E /* ParseOSX.framework */; }; + 81C09F8D1AF9816D0043B49C /* Bolts.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 8103FA44198FC267000BAE3F /* Bolts.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 81C1EE491AE1EF960031C438 /* PFWeakValue.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C1EE471AE1EF960031C438 /* PFWeakValue.h */; }; + 81C1EE4A1AE1EF960031C438 /* PFWeakValue.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C1EE481AE1EF960031C438 /* PFWeakValue.m */; }; + 81C1EE4B1AE1EFA10031C438 /* PFWeakValue.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C1EE481AE1EF960031C438 /* PFWeakValue.m */; }; + 81C3823E19CCAD090066284A /* PFConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABEC13D791770095FEFA /* PFConstants.m */; }; + 81C3823F19CCAD2C0066284A /* Parse.m in Sources */ = {isa = PBXBuildFile; fileRef = 09EEA12E1434FB1F00E3A3FA /* Parse.m */; }; + 81C3824019CCAD2C0066284A /* PFACL.m in Sources */ = {isa = PBXBuildFile; fileRef = 64C47803147336C70092082F /* PFACL.m */; }; + 81C3824119CCAD2C0066284A /* PFAnalytics.m in Sources */ = {isa = PBXBuildFile; fileRef = 9739513916B9D28E0010B884 /* PFAnalytics.m */; }; + 81C3824219CCAD2C0066284A /* PFAnonymousUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 638CBBB515191435004F54E4 /* PFAnonymousUtils.m */; }; + 81C3824319CCAD2C0066284A /* PFCloud.m in Sources */ = {isa = PBXBuildFile; fileRef = 805D3DA015E31241007E8D10 /* PFCloud.m */; }; + 81C3824419CCAD2C0066284A /* PFConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 81EB6633198A7FA600851598 /* PFConfig.m */; }; + 81C3824519CCAD2C0066284A /* PFFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 81DEF07E199C42A300D86A21 /* PFFile.m */; }; + 81C3824619CCAD2C0066284A /* PFGeoPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 09B119F714880776002B5594 /* PFGeoPoint.m */; }; + 81C3824719CCAD2C0066284A /* PFInstallation.m in Sources */ = {isa = PBXBuildFile; fileRef = 44B78E12157D21B000A5E97F /* PFInstallation.m */; }; + 81C3824819CCAD2C0066284A /* PFObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABEE13D791770095FEFA /* PFObject.m */; }; + 81C3824919CCAD2C0066284A /* PFProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = 499E425615B6409000A2C28E /* PFProduct.m */; }; + 81C3824A19CCAD2C0066284A /* PFPurchase.m in Sources */ = {isa = PBXBuildFile; fileRef = 49FDE2ED158C138F00126F64 /* PFPurchase.m */; }; + 81C3824B19CCAD2C0066284A /* PFPush.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABF213D791770095FEFA /* PFPush.m */; }; + 81C3824C19CCAD2C0066284A /* PFQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABF413D791770095FEFA /* PFQuery.m */; }; + 81C3824D19CCAD2C0066284A /* PFRelation.m in Sources */ = {isa = PBXBuildFile; fileRef = 8083B85A155DAB1B0023EEFA /* PFRelation.m */; }; + 81C3824E19CCAD2C0066284A /* PFRole.m in Sources */ = {isa = PBXBuildFile; fileRef = 63723F6E1565A085007A1A73 /* PFRole.m */; }; + 81C3825019CCAD2C0066284A /* PFUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABF613D791770095FEFA /* PFUser.m */; }; + 81C3825119CCAD2C0066284A /* PFNetworkActivityIndicatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 81DEF08A199D555800D86A21 /* PFNetworkActivityIndicatorManager.m */; }; + 81C3825519CCAD4D0066284A /* PFCommandResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CF87D38162FC8FB00FF5C22 /* PFCommandResult.m */; }; + 81C3826819CCAD790066284A /* PFAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = 8101A14719ACDA97008BB503 /* PFAlertView.m */; }; + 81C3826919CCAD7F0066284A /* BFTask+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA34198FC190000BAE3F /* BFTask+Private.m */; }; + 81C3826A19CCAD7F0066284A /* PFCategoryLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8103FA36198FC190000BAE3F /* PFCategoryLoader.m */; }; + 81C3826B19CCAD850066284A /* PFThreadsafety.m in Sources */ = {isa = PBXBuildFile; fileRef = 818D049A19A3B84500BEE20F /* PFThreadsafety.m */; }; + 81C3826C19CCADA00066284A /* ParseModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 81DDB90C199A3EC200B50F35 /* ParseModule.m */; }; + 81C3826E19CCADA00066284A /* PFBlockRetryer.m in Sources */ = {isa = PBXBuildFile; fileRef = 097952A214CE462B00E6E88C /* PFBlockRetryer.m */; }; + 81C3826F19CCADA00066284A /* PFCommandCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C1FDDCB14E1B1BD00A77007 /* PFCommandCache.m */; }; + 81C3827019CCADA00066284A /* PFDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 919311D619AE5EB20008FF12 /* PFDecoder.m */; }; + 81C3827319CCADA00066284A /* PFInternalUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 09809FB21434F98C00EC3E74 /* PFInternalUtils.m */; }; + 81C3827419CCADA00066284A /* PFKeychainStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D0EE9819B0A2060000AE75 /* PFKeychainStore.m */; }; + 81C3827819CCADA00066284A /* PFMulticastDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6390EB1C151EDDA40001B779 /* PFMulticastDelegate.m */; }; + 81C3827E19CCADA00066284A /* PFTaskQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CF213BB16D41D980065CF1A /* PFTaskQueue.m */; }; + 81C3828019CCADA00066284A /* PFLocationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E18AE51623835600B17A67 /* PFLocationManager.m */; }; + 81C6BDEE1B4DB16500553A83 /* PFInstallationConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C6BDEC1B4DB16500553A83 /* PFInstallationConstants.h */; }; + 81C6BDEF1B4DB16500553A83 /* PFInstallationConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C6BDEC1B4DB16500553A83 /* PFInstallationConstants.h */; }; + 81C6BDF01B4DB16500553A83 /* PFInstallationConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C6BDED1B4DB16500553A83 /* PFInstallationConstants.m */; }; + 81C6BDF11B4DB16500553A83 /* PFInstallationConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C6BDED1B4DB16500553A83 /* PFInstallationConstants.m */; }; + 81C6BDF41B4DD32700553A83 /* PFCurrentObjectControlling.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C6BDF31B4DD32700553A83 /* PFCurrentObjectControlling.h */; }; + 81C6BDF51B4DD32700553A83 /* PFCurrentObjectControlling.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C6BDF31B4DD32700553A83 /* PFCurrentObjectControlling.h */; }; + 81C76EE81B4B201E0031C2FD /* PFObjectConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C76EE71B4B201E0031C2FD /* PFObjectConstants.h */; }; + 81C76EE91B4B201E0031C2FD /* PFObjectConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C76EE71B4B201E0031C2FD /* PFObjectConstants.h */; }; + 81C76EEB1B4B218C0031C2FD /* PFObjectConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C76EEA1B4B218C0031C2FD /* PFObjectConstants.m */; }; + 81C76EEC1B4B218C0031C2FD /* PFObjectConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C76EEA1B4B218C0031C2FD /* PFObjectConstants.m */; }; + 81C7F48B1AF4110B007B5418 /* PFQueryUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4891AF4110B007B5418 /* PFQueryUtilities.h */; }; + 81C7F48C1AF4110B007B5418 /* PFQueryUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4891AF4110B007B5418 /* PFQueryUtilities.h */; }; + 81C7F48D1AF4110B007B5418 /* PFQueryUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F48A1AF4110B007B5418 /* PFQueryUtilities.m */; }; + 81C7F48E1AF4110B007B5418 /* PFQueryUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F48A1AF4110B007B5418 /* PFQueryUtilities.m */; }; + 81C7F4991AF42187007B5418 /* PFFileState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4971AF42187007B5418 /* PFFileState.h */; }; + 81C7F49A1AF42187007B5418 /* PFFileState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4971AF42187007B5418 /* PFFileState.h */; }; + 81C7F49B1AF42187007B5418 /* PFFileState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4981AF42187007B5418 /* PFFileState.m */; }; + 81C7F49C1AF42187007B5418 /* PFFileState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4981AF42187007B5418 /* PFFileState.m */; }; + 81C7F49E1AF421FF007B5418 /* PFFileState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F49D1AF421FF007B5418 /* PFFileState_Private.h */; }; + 81C7F49F1AF421FF007B5418 /* PFFileState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F49D1AF421FF007B5418 /* PFFileState_Private.h */; }; + 81C7F4A21AF4220A007B5418 /* PFMutableFileState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4A01AF4220A007B5418 /* PFMutableFileState.h */; }; + 81C7F4A31AF4220A007B5418 /* PFMutableFileState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4A01AF4220A007B5418 /* PFMutableFileState.h */; }; + 81C7F4A41AF4220A007B5418 /* PFMutableFileState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4A11AF4220A007B5418 /* PFMutableFileState.m */; }; + 81C7F4A51AF4220A007B5418 /* PFMutableFileState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4A11AF4220A007B5418 /* PFMutableFileState.m */; }; + 81C7F4AC1AF42BD9007B5418 /* PFMutableQueryState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4A71AF42BD9007B5418 /* PFMutableQueryState.h */; }; + 81C7F4AD1AF42BD9007B5418 /* PFMutableQueryState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4A71AF42BD9007B5418 /* PFMutableQueryState.h */; }; + 81C7F4AE1AF42BD9007B5418 /* PFMutableQueryState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4A81AF42BD9007B5418 /* PFMutableQueryState.m */; }; + 81C7F4AF1AF42BD9007B5418 /* PFMutableQueryState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4A81AF42BD9007B5418 /* PFMutableQueryState.m */; }; + 81C7F4B01AF42BD9007B5418 /* PFQueryState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4A91AF42BD9007B5418 /* PFQueryState.h */; }; + 81C7F4B11AF42BD9007B5418 /* PFQueryState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4A91AF42BD9007B5418 /* PFQueryState.h */; }; + 81C7F4B21AF42BD9007B5418 /* PFQueryState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4AA1AF42BD9007B5418 /* PFQueryState.m */; }; + 81C7F4B31AF42BD9007B5418 /* PFQueryState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C7F4AA1AF42BD9007B5418 /* PFQueryState.m */; }; + 81C7F4B41AF42BD9007B5418 /* PFQueryState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4AB1AF42BD9007B5418 /* PFQueryState_Private.h */; }; + 81C7F4B51AF42BD9007B5418 /* PFQueryState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C7F4AB1AF42BD9007B5418 /* PFQueryState_Private.h */; }; + 81C9C9F719FEA89200D514C5 /* PFRESTPushCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C9C9F519FEA89200D514C5 /* PFRESTPushCommand.h */; }; + 81C9C9F819FEA89200D514C5 /* PFRESTPushCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C9C9F519FEA89200D514C5 /* PFRESTPushCommand.h */; }; + 81C9C9F919FEA89200D514C5 /* PFRESTPushCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C9C9F619FEA89200D514C5 /* PFRESTPushCommand.m */; }; + 81C9C9FA19FEA89200D514C5 /* PFRESTPushCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C9C9F619FEA89200D514C5 /* PFRESTPushCommand.m */; }; + 81C9CA0619FECF5F00D514C5 /* PFRESTFileCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C9CA0419FECF5F00D514C5 /* PFRESTFileCommand.h */; }; + 81C9CA0719FECF5F00D514C5 /* PFRESTFileCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C9CA0419FECF5F00D514C5 /* PFRESTFileCommand.h */; }; + 81C9CA0819FECF5F00D514C5 /* PFRESTFileCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C9CA0519FECF5F00D514C5 /* PFRESTFileCommand.m */; }; + 81C9CA0919FECF5F00D514C5 /* PFRESTFileCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81C9CA0519FECF5F00D514C5 /* PFRESTFileCommand.m */; }; + 81CB7F6F1B166FE500DC601D /* PFObjectState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F6D1B166FE500DC601D /* PFObjectState.h */; }; + 81CB7F701B166FE500DC601D /* PFObjectState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F6D1B166FE500DC601D /* PFObjectState.h */; }; + 81CB7F711B166FE500DC601D /* PFObjectState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F6E1B166FE500DC601D /* PFObjectState.m */; }; + 81CB7F721B166FE500DC601D /* PFObjectState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F6E1B166FE500DC601D /* PFObjectState.m */; }; + 81CB7F751B166FF500DC601D /* PFMutableObjectState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F731B166FF500DC601D /* PFMutableObjectState.h */; }; + 81CB7F761B166FF500DC601D /* PFMutableObjectState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F731B166FF500DC601D /* PFMutableObjectState.h */; }; + 81CB7F771B166FF500DC601D /* PFMutableObjectState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F741B166FF500DC601D /* PFMutableObjectState.m */; }; + 81CB7F781B166FF500DC601D /* PFMutableObjectState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F741B166FF500DC601D /* PFMutableObjectState.m */; }; + 81CB7F7A1B16710D00DC601D /* PFObjectState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F791B16710D00DC601D /* PFObjectState_Private.h */; }; + 81CB7F7B1B16710D00DC601D /* PFObjectState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F791B16710D00DC601D /* PFObjectState_Private.h */; }; + 81CB7F8E1B1795C000DC601D /* PFPushState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F8C1B1795C000DC601D /* PFPushState.h */; }; + 81CB7F901B1795C000DC601D /* PFPushState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F8D1B1795C000DC601D /* PFPushState.m */; }; + 81CB7F941B1795CF00DC601D /* PFMutablePushState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F921B1795CF00DC601D /* PFMutablePushState.h */; }; + 81CB7F961B1795CF00DC601D /* PFMutablePushState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F931B1795CF00DC601D /* PFMutablePushState.m */; }; + 81CB7F991B17970400DC601D /* PFPushState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F981B17970400DC601D /* PFPushState_Private.h */; }; + 81CB7FA01B1800E400DC601D /* PFPushController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F9E1B1800E400DC601D /* PFPushController.h */; }; + 81CB7FA11B1800E400DC601D /* PFPushController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F9F1B1800E400DC601D /* PFPushController.m */; }; + 81CD66541B4DA5A70042FC0B /* PFCurrentInstallationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CD66521B4DA5A70042FC0B /* PFCurrentInstallationController.h */; }; + 81CD66551B4DA5A70042FC0B /* PFCurrentInstallationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CD66521B4DA5A70042FC0B /* PFCurrentInstallationController.h */; }; + 81CD66561B4DA5A70042FC0B /* PFCurrentInstallationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CD66531B4DA5A70042FC0B /* PFCurrentInstallationController.m */; }; + 81CD66571B4DA5A70042FC0B /* PFCurrentInstallationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CD66531B4DA5A70042FC0B /* PFCurrentInstallationController.m */; }; + 81CD665A1B4DA5BA0042FC0B /* PFInstallationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CD66581B4DA5BA0042FC0B /* PFInstallationController.h */; }; + 81CD665B1B4DA5BA0042FC0B /* PFInstallationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CD66581B4DA5BA0042FC0B /* PFInstallationController.h */; }; + 81CD665C1B4DA5BA0042FC0B /* PFInstallationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CD66591B4DA5BA0042FC0B /* PFInstallationController.m */; }; + 81CD665D1B4DA5BA0042FC0B /* PFInstallationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CD66591B4DA5BA0042FC0B /* PFInstallationController.m */; }; + 81D0EE9A19B0A2060000AE75 /* PFKeychainStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 81D0EE9719B0A2060000AE75 /* PFKeychainStore.h */; }; + 81D0EE9C19B0A2060000AE75 /* PFKeychainStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D0EE9819B0A2060000AE75 /* PFKeychainStore.m */; }; + 81D843C91B012FBA007CEBCB /* PFCloudCodeController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81D843C71B012FBA007CEBCB /* PFCloudCodeController.h */; }; + 81D843CA1B012FBA007CEBCB /* PFCloudCodeController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81D843C71B012FBA007CEBCB /* PFCloudCodeController.h */; }; + 81D843CB1B012FBA007CEBCB /* PFCloudCodeController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D843C81B012FBA007CEBCB /* PFCloudCodeController.m */; }; + 81D843CC1B012FBA007CEBCB /* PFCloudCodeController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D843C81B012FBA007CEBCB /* PFCloudCodeController.m */; }; + 81D8E7601B7323ED004B014C /* HashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D8E75F1B7323ED004B014C /* HashTests.m */; }; + 81D8E7611B7323ED004B014C /* HashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81D8E75F1B7323ED004B014C /* HashTests.m */; }; + 81DDB912199A551A00B50F35 /* ParseModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 81DDB90C199A3EC200B50F35 /* ParseModule.m */; }; + 81E0335A1B573F3E00B25168 /* PFMockURLProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E033571B573F3E00B25168 /* PFMockURLProtocol.m */; }; + 81E0335B1B573F3E00B25168 /* PFMockURLProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E033571B573F3E00B25168 /* PFMockURLProtocol.m */; }; + 81E0335C1B573F3E00B25168 /* PFMockURLResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E033591B573F3E00B25168 /* PFMockURLResponse.m */; }; + 81E0335D1B573F3E00B25168 /* PFMockURLResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E033591B573F3E00B25168 /* PFMockURLResponse.m */; }; + 81E0336E1B573FC500B25168 /* PFTestSKPaymentQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E033651B573FC500B25168 /* PFTestSKPaymentQueue.m */; }; + 81E0336F1B573FC500B25168 /* PFTestSKPaymentTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E033671B573FC500B25168 /* PFTestSKPaymentTransaction.m */; }; + 81E033701B573FC500B25168 /* PFTestSKProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E033691B573FC500B25168 /* PFTestSKProduct.m */; }; + 81E033711B573FC500B25168 /* PFTestSKProductsRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E0336B1B573FC500B25168 /* PFTestSKProductsRequest.m */; }; + 81E033721B573FC500B25168 /* PFTestSKProductsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E0336D1B573FC500B25168 /* PFTestSKProductsResponse.m */; }; + 81E0337E1B57441F00B25168 /* CLLocationManager+TestAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E0337D1B57441F00B25168 /* CLLocationManager+TestAdditions.m */; }; + 81E0337F1B57441F00B25168 /* CLLocationManager+TestAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E0337D1B57441F00B25168 /* CLLocationManager+TestAdditions.m */; }; + 81E7A21C1B602560006CB680 /* PFUserFileCodingLogic.h in Headers */ = {isa = PBXBuildFile; fileRef = 81E7A21A1B602560006CB680 /* PFUserFileCodingLogic.h */; }; + 81E7A21D1B602560006CB680 /* PFUserFileCodingLogic.h in Headers */ = {isa = PBXBuildFile; fileRef = 81E7A21A1B602560006CB680 /* PFUserFileCodingLogic.h */; }; + 81E7A21E1B602560006CB680 /* PFUserFileCodingLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E7A21B1B602560006CB680 /* PFUserFileCodingLogic.m */; }; + 81E7A21F1B602560006CB680 /* PFUserFileCodingLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E7A21B1B602560006CB680 /* PFUserFileCodingLogic.m */; }; + 81E7A2251B6042BD006CB680 /* PFObjectFileCodingLogic.h in Headers */ = {isa = PBXBuildFile; fileRef = 81E7A2231B6042BD006CB680 /* PFObjectFileCodingLogic.h */; }; + 81E7A2261B6042BD006CB680 /* PFObjectFileCodingLogic.h in Headers */ = {isa = PBXBuildFile; fileRef = 81E7A2231B6042BD006CB680 /* PFObjectFileCodingLogic.h */; }; + 81E7A2271B6042BD006CB680 /* PFObjectFileCodingLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E7A2241B6042BD006CB680 /* PFObjectFileCodingLogic.m */; }; + 81E7A2281B6042BD006CB680 /* PFObjectFileCodingLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E7A2241B6042BD006CB680 /* PFObjectFileCodingLogic.m */; }; + 81EB595E1AF46434001EA1FC /* PFFileController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EB595C1AF46434001EA1FC /* PFFileController.h */; }; + 81EB595F1AF46434001EA1FC /* PFFileController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EB595C1AF46434001EA1FC /* PFFileController.h */; }; + 81EB59601AF46434001EA1FC /* PFFileController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81EB595D1AF46434001EA1FC /* PFFileController.m */; }; + 81EB59611AF46434001EA1FC /* PFFileController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81EB595D1AF46434001EA1FC /* PFFileController.m */; }; + 81EB6635198A7FA600851598 /* PFConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EB6632198A7FA600851598 /* PFConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81EB6637198A7FA600851598 /* PFConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 81EB6633198A7FA600851598 /* PFConfig.m */; }; + 81EBF33F1B33E7A800991947 /* PFInstallation.h in Headers */ = {isa = PBXBuildFile; fileRef = 44B78E11157D21B000A5E97F /* PFInstallation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81EBF3401B33E7B100991947 /* PFPush.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABF113D791770095FEFA /* PFPush.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81EBF3411B33E7C600991947 /* PFPush.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABF213D791770095FEFA /* PFPush.m */; }; + 81EBF3441B33E7D400991947 /* PFPushController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F9F1B1800E400DC601D /* PFPushController.m */; }; + 81EBF3451B33E7D800991947 /* PFMutablePushState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F931B1795CF00DC601D /* PFMutablePushState.m */; }; + 81EBF3461B33E7DE00991947 /* PFPushChannelsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8124C8841B27588800758E00 /* PFPushChannelsController.m */; }; + 81EBF3471B33E7E500991947 /* PFPushState.m in Sources */ = {isa = PBXBuildFile; fileRef = 81CB7F8D1B1795C000DC601D /* PFPushState.m */; }; + 81EBF3481B33E7EB00991947 /* PFInstallation.m in Sources */ = {isa = PBXBuildFile; fileRef = 44B78E12157D21B000A5E97F /* PFInstallation.m */; }; + 81EDD4D21B59A6EC002F69C0 /* PFCommandRunning.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EDD4D11B59A6EC002F69C0 /* PFCommandRunning.h */; }; + 81EDD4D31B59A6EC002F69C0 /* PFCommandRunning.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EDD4D11B59A6EC002F69C0 /* PFCommandRunning.h */; }; + 81EEE1B01B446D600087AC4D /* PFCurrentUserController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EEE1AE1B446D600087AC4D /* PFCurrentUserController.h */; }; + 81EEE1B11B446D600087AC4D /* PFCurrentUserController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EEE1AE1B446D600087AC4D /* PFCurrentUserController.h */; }; + 81EEE1B21B446D600087AC4D /* PFCurrentUserController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81EEE1AF1B446D600087AC4D /* PFCurrentUserController.m */; }; + 81EEE1B31B446D600087AC4D /* PFCurrentUserController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81EEE1AF1B446D600087AC4D /* PFCurrentUserController.m */; }; + 81F0E88E19E6F7D600812A88 /* Parse.h in Headers */ = {isa = PBXBuildFile; fileRef = 09EEA12D1434FB1F00E3A3FA /* Parse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E88F19E6F7DB00812A88 /* ParseOSX.h in Headers */ = {isa = PBXBuildFile; fileRef = 971AC55C1716405A00A4EB71 /* ParseOSX.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89019E6F83E00812A88 /* PFACL.h in Headers */ = {isa = PBXBuildFile; fileRef = 64C47802147336C70092082F /* PFACL.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89119E6F83E00812A88 /* PFAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = 9739513816B9D28E0010B884 /* PFAnalytics.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89219E6F83E00812A88 /* PFAnonymousUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 638CBBB415191435004F54E4 /* PFAnonymousUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89319E6F83E00812A88 /* PFCloud.h in Headers */ = {isa = PBXBuildFile; fileRef = 805D3D9F15E31241007E8D10 /* PFCloud.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89419E6F83E00812A88 /* PFConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABEB13D791770095FEFA /* PFConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89519E6F83E00812A88 /* PFFile.h in Headers */ = {isa = PBXBuildFile; fileRef = 81DEF07D199C42A300D86A21 /* PFFile.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89619E6F83E00812A88 /* PFGeoPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 09B119F614880776002B5594 /* PFGeoPoint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89719E6F83E00812A88 /* PFObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABED13D791770095FEFA /* PFObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89819E6F83E00812A88 /* PFQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABF313D791770095FEFA /* PFQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89919E6F83E00812A88 /* PFRelation.h in Headers */ = {isa = PBXBuildFile; fileRef = 8083B859155DAB1B0023EEFA /* PFRelation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89A19E6F83E00812A88 /* PFRole.h in Headers */ = {isa = PBXBuildFile; fileRef = 63723F6D1565A085007A1A73 /* PFRole.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89B19E6F83E00812A88 /* PFUser.h in Headers */ = {isa = PBXBuildFile; fileRef = 0925ABF513D791770095FEFA /* PFUser.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89C19E6F83E00812A88 /* PFObject+Subclass.h in Headers */ = {isa = PBXBuildFile; fileRef = E9BBE98E16D18E5800CD7B52 /* PFObject+Subclass.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 81F0E89D19E6F83E00812A88 /* PFSubclassing.h in Headers */ = {isa = PBXBuildFile; fileRef = E9E81E8316EEF93E001D034F /* PFSubclassing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 91115EF91A097AF30092D1C9 /* PFEventuallyPin.h in Headers */ = {isa = PBXBuildFile; fileRef = 91115EF71A097AF30092D1C9 /* PFEventuallyPin.h */; }; + 91115EFA1A097AF30092D1C9 /* PFEventuallyPin.m in Sources */ = {isa = PBXBuildFile; fileRef = 91115EF81A097AF30092D1C9 /* PFEventuallyPin.m */; }; + 91CDB94C1A32E5C900FF830F /* PFEventuallyQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 91DF24911A09BA7600CFC7D4 /* PFEventuallyQueue.m */; }; + 91CDB94D1A32E5C900FF830F /* PFPinningEventuallyQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 91DF24951A09BAF100CFC7D4 /* PFPinningEventuallyQueue.m */; }; + 91CDB94E1A32E5E800FF830F /* PFEventuallyPin.m in Sources */ = {isa = PBXBuildFile; fileRef = 91115EF81A097AF30092D1C9 /* PFEventuallyPin.m */; }; + 91DF24921A09BA7600CFC7D4 /* PFEventuallyQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 91DF24901A09BA7600CFC7D4 /* PFEventuallyQueue.h */; }; + 91DF24931A09BA7600CFC7D4 /* PFEventuallyQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 91DF24911A09BA7600CFC7D4 /* PFEventuallyQueue.m */; }; + 91DF24961A09BAF100CFC7D4 /* PFPinningEventuallyQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 91DF24941A09BAF100CFC7D4 /* PFPinningEventuallyQueue.h */; }; + 91DF24971A09BAF100CFC7D4 /* PFPinningEventuallyQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 91DF24951A09BAF100CFC7D4 /* PFPinningEventuallyQueue.m */; }; + 91DF24991A0B0FF200CFC7D4 /* PFEventuallyQueue_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 91DF24981A0B0FF200CFC7D4 /* PFEventuallyQueue_Private.h */; }; + 970110681630B44200AB761E /* PFBlockRetryer.m in Sources */ = {isa = PBXBuildFile; fileRef = 097952A214CE462B00E6E88C /* PFBlockRetryer.m */; }; + 970110691630B44200AB761E /* PFCommandCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C1FDDCB14E1B1BD00A77007 /* PFCommandCache.m */; }; + 9701106E1630B44200AB761E /* PFInternalUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 09809FB21434F98C00EC3E74 /* PFInternalUtils.m */; }; + 970110721630B44200AB761E /* PFMulticastDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6390EB1C151EDDA40001B779 /* PFMulticastDelegate.m */; }; + 970110791630B44200AB761E /* PFLocationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 97E18AE51623835600B17A67 /* PFLocationManager.m */; }; + 9701107B1630B45800AB761E /* Parse.m in Sources */ = {isa = PBXBuildFile; fileRef = 09EEA12E1434FB1F00E3A3FA /* Parse.m */; }; + 9701107C1630B45800AB761E /* PFACL.m in Sources */ = {isa = PBXBuildFile; fileRef = 64C47803147336C70092082F /* PFACL.m */; }; + 9701107D1630B45800AB761E /* PFAnonymousUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 638CBBB515191435004F54E4 /* PFAnonymousUtils.m */; }; + 9701107E1630B45800AB761E /* PFCloud.m in Sources */ = {isa = PBXBuildFile; fileRef = 805D3DA015E31241007E8D10 /* PFCloud.m */; }; + 9701107F1630B45800AB761E /* PFConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABEC13D791770095FEFA /* PFConstants.m */; }; + 970110821630B45800AB761E /* PFGeoPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 09B119F714880776002B5594 /* PFGeoPoint.m */; }; + 970110841630B45800AB761E /* PFObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABEE13D791770095FEFA /* PFObject.m */; }; + 970110881630B45800AB761E /* PFQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABF413D791770095FEFA /* PFQuery.m */; }; + 970110891630B45800AB761E /* PFRelation.m in Sources */ = {isa = PBXBuildFile; fileRef = 8083B85A155DAB1B0023EEFA /* PFRelation.m */; }; + 9701108A1630B45800AB761E /* PFRole.m in Sources */ = {isa = PBXBuildFile; fileRef = 63723F6E1565A085007A1A73 /* PFRole.m */; }; + 9701108C1630B45800AB761E /* PFUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 0925ABF613D791770095FEFA /* PFUser.m */; }; + 974268CA1651ED4E00F2BC57 /* PFCommandResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CF87D38162FC8FB00FF5C22 /* PFCommandResult.m */; }; + 97DE045116321428007154E8 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97DE045016321428007154E8 /* CoreLocation.framework */; }; + 97DE045A16321492007154E8 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97DE045916321492007154E8 /* Security.framework */; }; + 97DE045C163214C0007154E8 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97DE045B163214C0007154E8 /* SystemConfiguration.framework */; }; + 97EB055516F7CCE400E09147 /* PFAnalytics.m in Sources */ = {isa = PBXBuildFile; fileRef = 9739513916B9D28E0010B884 /* PFAnalytics.m */; }; + F50C66331B33A708001941A6 /* PFPushUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = F50C66311B33A708001941A6 /* PFPushUtilities.h */; }; + F50C66341B33A708001941A6 /* PFPushUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = F50C66321B33A708001941A6 /* PFPushUtilities.m */; }; + F50C667C1B34B231001941A6 /* PFPushUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = F50C66321B33A708001941A6 /* PFPushUtilities.m */; }; + F510509F1B6AA4CE00749060 /* ExtensionDataSharingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E61B66D44500EFD14F /* ExtensionDataSharingTests.m */; }; + F51050A01B6AA4D100749060 /* ExtensionDataSharingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E61B66D44500EFD14F /* ExtensionDataSharingTests.m */; }; + F51050A11B6AA4D600749060 /* ExtensionDataSharingMobileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E51B66D44500EFD14F /* ExtensionDataSharingMobileTests.m */; }; + F51534FF1B571E9100C49F56 /* PFACLPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534F61B571E9100C49F56 /* PFACLPrivate.h */; }; + F51535001B571E9100C49F56 /* PFACLState.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534F81B571E9100C49F56 /* PFACLState.h */; }; + F51535011B571E9100C49F56 /* PFACLState.m in Sources */ = {isa = PBXBuildFile; fileRef = F51534F91B571E9100C49F56 /* PFACLState.m */; }; + F51535021B571E9100C49F56 /* PFACLState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534FA1B571E9100C49F56 /* PFACLState_Private.h */; }; + F51535031B571E9100C49F56 /* PFMutableACLState.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534FB1B571E9100C49F56 /* PFMutableACLState.h */; }; + F51535041B571E9100C49F56 /* PFMutableACLState.m in Sources */ = {isa = PBXBuildFile; fileRef = F51534FC1B571E9100C49F56 /* PFMutableACLState.m */; }; + F51535051B57240900C49F56 /* PFACLPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534F61B571E9100C49F56 /* PFACLPrivate.h */; }; + F51535061B57240900C49F56 /* PFACLState.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534F81B571E9100C49F56 /* PFACLState.h */; }; + F51535071B57240900C49F56 /* PFACLState.m in Sources */ = {isa = PBXBuildFile; fileRef = F51534F91B571E9100C49F56 /* PFACLState.m */; }; + F51535081B57240900C49F56 /* PFACLState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534FA1B571E9100C49F56 /* PFACLState_Private.h */; }; + F51535091B57240900C49F56 /* PFMutableACLState.h in Headers */ = {isa = PBXBuildFile; fileRef = F51534FB1B571E9100C49F56 /* PFMutableACLState.h */; }; + F515350A1B57240900C49F56 /* PFMutableACLState.m in Sources */ = {isa = PBXBuildFile; fileRef = F51534FC1B571E9100C49F56 /* PFMutableACLState.m */; }; + F51535591B57573700C49F56 /* PFDefaultACLController.h in Headers */ = {isa = PBXBuildFile; fileRef = F51535571B57573700C49F56 /* PFDefaultACLController.h */; }; + F515355A1B57573700C49F56 /* PFDefaultACLController.h in Headers */ = {isa = PBXBuildFile; fileRef = F51535571B57573700C49F56 /* PFDefaultACLController.h */; }; + F515355B1B57573700C49F56 /* PFDefaultACLController.m in Sources */ = {isa = PBXBuildFile; fileRef = F51535581B57573700C49F56 /* PFDefaultACLController.m */; }; + F515355C1B57573700C49F56 /* PFDefaultACLController.m in Sources */ = {isa = PBXBuildFile; fileRef = F51535581B57573700C49F56 /* PFDefaultACLController.m */; }; + F51D06341B792CF10044539E /* PFSQLiteDatabaseController.h in Headers */ = {isa = PBXBuildFile; fileRef = F51D06321B792CF10044539E /* PFSQLiteDatabaseController.h */; }; + F51D06351B792CF10044539E /* PFSQLiteDatabaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = F51D06331B792CF10044539E /* PFSQLiteDatabaseController.m */; }; + F51D06371B793A110044539E /* PFSQLiteDatabase_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F51D06361B793A110044539E /* PFSQLiteDatabase_Private.h */; }; + F51D06381B793A110044539E /* PFSQLiteDatabase_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F51D06361B793A110044539E /* PFSQLiteDatabase_Private.h */; }; + F5556A181B66F47900410837 /* PFURLSession_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F5556A171B66F47900410837 /* PFURLSession_Private.h */; }; + F5556A191B66F47900410837 /* PFURLSession_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F5556A171B66F47900410837 /* PFURLSession_Private.h */; }; + F55C740C1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F55C740B1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h */; }; + F55C740D1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F55C740B1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h */; }; + F5732DE11B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5732DE01B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m */; }; + F5732DE21B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5732DE01B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m */; }; + F586B3511B1E3BD70082E3BD /* PFBaseState.m in Sources */ = {isa = PBXBuildFile; fileRef = F586B34F1B1E3BD70082E3BD /* PFBaseState.m */; }; + F586B3521B1E3BE90082E3BD /* PFBaseState.m in Sources */ = {isa = PBXBuildFile; fileRef = F586B34F1B1E3BD70082E3BD /* PFBaseState.m */; }; + F589894B1B7427FF008A566B /* AlertViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915CE1B66D44500EFD14F /* AlertViewTests.m */; }; + F590194A1B7992E000F763EF /* PFSQLiteDatabaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = F51D06331B792CF10044539E /* PFSQLiteDatabaseController.m */; }; + F590194B1B7992E700F763EF /* PFSQLiteDatabaseController.h in Headers */ = {isa = PBXBuildFile; fileRef = F51D06321B792CF10044539E /* PFSQLiteDatabaseController.h */; }; + F5ADB9C71B6C503E002A819E /* TestFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F5ADB9C61B6C503E002A819E /* TestFileManager.m */; }; + F5ADB9C81B6C503E002A819E /* TestFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F5ADB9C61B6C503E002A819E /* TestFileManager.m */; }; + F5ADB9CB1B6C5047002A819E /* TestCache.m in Sources */ = {isa = PBXBuildFile; fileRef = F5ADB9CA1B6C5047002A819E /* TestCache.m */; }; + F5ADB9CC1B6C5047002A819E /* TestCache.m in Sources */ = {isa = PBXBuildFile; fileRef = F5ADB9CA1B6C5047002A819E /* TestCache.m */; }; + F5B0B2DE1B449EEF00F3EBC4 /* PFCommandCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7C1FDDCA14E1B1BD00A77007 /* PFCommandCache.h */; }; + F5B0B2DF1B449EEF00F3EBC4 /* PFCommandCache_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 913B9F2C1A311FF40040247C /* PFCommandCache_Private.h */; }; + F5B0B2E01B449EEF00F3EBC4 /* PFCommandResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7C9455DE15B8793F0037A86D /* PFCommandResult.h */; }; + F5B0B2EB1B449EEF00F3EBC4 /* PFAlertView.h in Headers */ = {isa = PBXBuildFile; fileRef = 8101A14619ACDA97008BB503 /* PFAlertView.h */; }; + F5B0B2EC1B449F1D00F3EBC4 /* BFTask+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8103FA33198FC190000BAE3F /* BFTask+Private.h */; }; + F5B0B2ED1B449F1D00F3EBC4 /* PFCategoryLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8103FA35198FC190000BAE3F /* PFCategoryLoader.h */; }; + F5B0B2EE1B449F1D00F3EBC4 /* PFThreadsafety.h in Headers */ = {isa = PBXBuildFile; fileRef = 818D049919A3B84500BEE20F /* PFThreadsafety.h */; }; + F5B0B2EF1B449F1D00F3EBC4 /* PFRelationState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F5E8DE231B2912BC00EEA594 /* PFRelationState_Private.h */; }; + F5B0B2F01B449F1D00F3EBC4 /* ParseInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 09EEA1351435143500E3A3FA /* ParseInternal.h */; }; + F5B0B2F11B449F1D00F3EBC4 /* PFCoreDataProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8811B27542A00758E00 /* PFCoreDataProvider.h */; }; + F5B0B2F21B449F1D00F3EBC4 /* ParseModule.h in Headers */ = {isa = PBXBuildFile; fileRef = 81DDB90B199A3EC200B50F35 /* ParseModule.h */; }; + F5B0B2F31B449F1D00F3EBC4 /* PFAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 81E2D5AF19DDAAB5009053A1 /* PFAssert.h */; }; + F5B0B2F61B449F1D00F3EBC4 /* PFBlockRetryer.h in Headers */ = {isa = PBXBuildFile; fileRef = 097952A114CE462B00E6E88C /* PFBlockRetryer.h */; }; + F5B0B2F81B449F1D00F3EBC4 /* PFDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 919311D519AE5EB20008FF12 /* PFDecoder.h */; }; + F5B0B2FA1B449F1D00F3EBC4 /* PFGeoPointPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 09B119FB1488429D002B5594 /* PFGeoPointPrivate.h */; }; + F5B0B2FC1B449F1D00F3EBC4 /* PFInternalUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 09809FB11434F98C00EC3E74 /* PFInternalUtils.h */; }; + F5B0B2FD1B449F1D00F3EBC4 /* PFKeychainStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 81D0EE9719B0A2060000AE75 /* PFKeychainStore.h */; }; + F5B0B2FF1B449F1D00F3EBC4 /* PFMulticastDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6390EB1B151EDDA40001B779 /* PFMulticastDelegate.h */; }; + F5B0B30A1B449F1D00F3EBC4 /* PFTaskQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 7CF213BA16D41D980065CF1A /* PFTaskQueue.h */; }; + F5B0B30C1B449F1D00F3EBC4 /* PFLocationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 97E18AE41623835600B17A67 /* PFLocationManager.h */; }; + F5B0B30D1B449F1D00F3EBC4 /* PFAsyncTaskQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C8F2BE1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.h */; }; + F5B0B30E1B449F1D00F3EBC4 /* PFBaseState.h in Headers */ = {isa = PBXBuildFile; fileRef = F586B34E1B1E3BD70082E3BD /* PFBaseState.h */; }; + F5B0B3131B44A05100F3EBC4 /* PFPaymentTransactionObserver_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F5B0B3121B44A05100F3EBC4 /* PFPaymentTransactionObserver_Private.h */; }; + F5B0B3151B44A21100F3EBC4 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5B0B3141B44A21100F3EBC4 /* SystemConfiguration.framework */; }; + F5B0B3161B44A22300F3EBC4 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0942999C139C613700DFA018 /* SystemConfiguration.framework */; }; + F5B0B3171B44A2CA00F3EBC4 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 498C29FE1551DC450034BB80 /* StoreKit.framework */; }; + F5B0B3191B44A33100F3EBC4 /* PFCommandCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 7C1FDDCA14E1B1BD00A77007 /* PFCommandCache.h */; }; + F5B0B31A1B44A33100F3EBC4 /* PFCommandCache_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 913B9F2C1A311FF40040247C /* PFCommandCache_Private.h */; }; + F5B0B31B1B44A33100F3EBC4 /* PFCommandResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7C9455DE15B8793F0037A86D /* PFCommandResult.h */; }; + F5B0B31C1B44A33100F3EBC4 /* PFEventuallyPin.h in Headers */ = {isa = PBXBuildFile; fileRef = 91115EF71A097AF30092D1C9 /* PFEventuallyPin.h */; }; + F5B0B31D1B44A33100F3EBC4 /* PFEventuallyQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 91DF24901A09BA7600CFC7D4 /* PFEventuallyQueue.h */; }; + F5B0B31E1B44A33100F3EBC4 /* PFEventuallyQueue_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 91DF24981A0B0FF200CFC7D4 /* PFEventuallyQueue_Private.h */; }; + F5B0B31F1B44A33100F3EBC4 /* PFPinningEventuallyQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 91DF24941A09BAF100CFC7D4 /* PFPinningEventuallyQueue.h */; }; + F5B0B3221B44A33100F3EBC4 /* ParseInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 09EEA1351435143500E3A3FA /* ParseInternal.h */; }; + F5B0B3231B44A33100F3EBC4 /* ParseModule.h in Headers */ = {isa = PBXBuildFile; fileRef = 81DDB90B199A3EC200B50F35 /* ParseModule.h */; }; + F5B0B3251B44A33100F3EBC4 /* PFApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = 816AC9B81A3F48250031D94C /* PFApplication.h */; }; + F5B0B3261B44A33100F3EBC4 /* PFAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 81E2D5AF19DDAAB5009053A1 /* PFAssert.h */; }; + F5B0B3271B44A33100F3EBC4 /* PFAsyncTaskQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C8F2BE1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.h */; }; + F5B0B3281B44A33100F3EBC4 /* PFBaseState.h in Headers */ = {isa = PBXBuildFile; fileRef = F586B34E1B1E3BD70082E3BD /* PFBaseState.h */; }; + F5B0B3291B44A33100F3EBC4 /* PFBlockRetryer.h in Headers */ = {isa = PBXBuildFile; fileRef = 097952A114CE462B00E6E88C /* PFBlockRetryer.h */; }; + F5B0B32B1B44A33100F3EBC4 /* PFCoreDataProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8811B27542A00758E00 /* PFCoreDataProvider.h */; }; + F5B0B32C1B44A33100F3EBC4 /* PFDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 919311D519AE5EB20008FF12 /* PFDecoder.h */; }; + F5B0B32E1B44A33100F3EBC4 /* PFGeoPointPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 09B119FB1488429D002B5594 /* PFGeoPointPrivate.h */; }; + F5B0B3301B44A33100F3EBC4 /* PFInternalUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 09809FB11434F98C00EC3E74 /* PFInternalUtils.h */; }; + F5B0B3331B44A33100F3EBC4 /* PFLocationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 97E18AE41623835600B17A67 /* PFLocationManager.h */; }; + F5B0B3341B44A33100F3EBC4 /* PFMulticastDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6390EB1B151EDDA40001B779 /* PFMulticastDelegate.h */; }; + F5B0B3431B44A33200F3EBC4 /* PFTaskQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 7CF213BA16D41D980065CF1A /* PFTaskQueue.h */; }; + F5B0B3451B44A33200F3EBC4 /* PFWeakValue.h in Headers */ = {isa = PBXBuildFile; fileRef = 81C1EE471AE1EF960031C438 /* PFWeakValue.h */; }; + F5B0B3461B44A33200F3EBC4 /* PFPushState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F8C1B1795C000DC601D /* PFPushState.h */; }; + F5B0B3471B44A33200F3EBC4 /* PFPushState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F981B17970400DC601D /* PFPushState_Private.h */; }; + F5B0B3481B44A33200F3EBC4 /* PFMutablePushState.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F921B1795CF00DC601D /* PFMutablePushState.h */; }; + F5B0B3491B44A33200F3EBC4 /* PFPushController.h in Headers */ = {isa = PBXBuildFile; fileRef = 81CB7F9E1B1800E400DC601D /* PFPushController.h */; }; + F5B0B34A1B44A33200F3EBC4 /* PFPushChannelsController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8124C8831B27588800758E00 /* PFPushChannelsController.h */; }; + F5B0B34B1B44A33200F3EBC4 /* PFPushUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = F50C66311B33A708001941A6 /* PFPushUtilities.h */; }; + F5B0B34C1B44A33200F3EBC4 /* PFRelationState_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = F5E8DE231B2912BC00EEA594 /* PFRelationState_Private.h */; }; + F5C42CC71B34C22100C720D8 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 095ACE9913C69BF700566243 /* AudioToolbox.framework */; }; + F5C42CD41B34F68C00C720D8 /* PFObjectSubclassingController.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C42CD21B34F68C00C720D8 /* PFObjectSubclassingController.h */; }; + F5C42CD51B34F68C00C720D8 /* PFObjectSubclassingController.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C42CD21B34F68C00C720D8 /* PFObjectSubclassingController.h */; }; + F5C42CD61B34F68C00C720D8 /* PFObjectSubclassingController.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C42CD31B34F68C00C720D8 /* PFObjectSubclassingController.m */; }; + F5C42CD71B34F68C00C720D8 /* PFObjectSubclassingController.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C42CD31B34F68C00C720D8 /* PFObjectSubclassingController.m */; }; + F5C42CDA1B38761B00C720D8 /* PFObjectSubclassInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C42CD81B38761B00C720D8 /* PFObjectSubclassInfo.h */; }; + F5C42CDB1B38761B00C720D8 /* PFObjectSubclassInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C42CD81B38761B00C720D8 /* PFObjectSubclassInfo.h */; }; + F5C42CDC1B38761B00C720D8 /* PFObjectSubclassInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C42CD91B38761B00C720D8 /* PFObjectSubclassInfo.m */; }; + F5C42CDD1B38761B00C720D8 /* PFObjectSubclassInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C42CD91B38761B00C720D8 /* PFObjectSubclassInfo.m */; }; + F5C8F2C01B1F7E7800CD98E7 /* PFAsyncTaskQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C8F2BF1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.m */; }; + F5C8F2C11B1F7E7900CD98E7 /* PFAsyncTaskQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C8F2BF1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.m */; }; + F5E381311B68832000A3B9F2 /* URLSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5556A141B66F36000410837 /* URLSessionTests.m */; }; + F5E381321B68832100A3B9F2 /* URLSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5556A141B66F36000410837 /* URLSessionTests.m */; }; + F5E381341B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5E381331B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m */; }; + F5E381351B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5E381331B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m */; }; + F5E8DE191B29100000EEA594 /* PFRelationState.h in Headers */ = {isa = PBXBuildFile; fileRef = F5E8DE171B290FFF00EEA594 /* PFRelationState.h */; }; + F5E8DE1A1B29100000EEA594 /* PFRelationState.h in Headers */ = {isa = PBXBuildFile; fileRef = F5E8DE171B290FFF00EEA594 /* PFRelationState.h */; }; + F5E8DE1B1B29100000EEA594 /* PFRelationState.m in Sources */ = {isa = PBXBuildFile; fileRef = F5E8DE181B290FFF00EEA594 /* PFRelationState.m */; }; + F5E8DE1C1B29100000EEA594 /* PFRelationState.m in Sources */ = {isa = PBXBuildFile; fileRef = F5E8DE181B290FFF00EEA594 /* PFRelationState.m */; }; + F5E8DE1F1B29112000EEA594 /* PFMutableRelationState.h in Headers */ = {isa = PBXBuildFile; fileRef = F5E8DE1D1B29112000EEA594 /* PFMutableRelationState.h */; }; + F5E8DE201B29112000EEA594 /* PFMutableRelationState.h in Headers */ = {isa = PBXBuildFile; fileRef = F5E8DE1D1B29112000EEA594 /* PFMutableRelationState.h */; }; + F5E8DE211B29112000EEA594 /* PFMutableRelationState.m in Sources */ = {isa = PBXBuildFile; fileRef = F5E8DE1E1B29112000EEA594 /* PFMutableRelationState.m */; }; + F5E8DE221B29112000EEA594 /* PFMutableRelationState.m in Sources */ = {isa = PBXBuildFile; fileRef = F5E8DE1E1B29112000EEA594 /* PFMutableRelationState.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 81493A991A0D3CE3008D5504 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 09D33641139C54930098E916 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 81493A931A0D3492008D5504; + remoteInfo = BoltsSDK; + }; + F569F07E1B14DB3C00296F73 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 09D33641139C54930098E916 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F569F07A1B14DB1E00296F73; + remoteInfo = "BoltsSDK-iOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 81C09F8C1AF9815F0043B49C /* Copy Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 81C09F8D1AF9816D0043B49C /* Bolts.framework in Copy Frameworks */, + 81AFA6771B0ECF97000763C0 /* OCMock.framework in Copy Frameworks */, + ); + name = "Copy Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0925ABEB13D791770095FEFA /* PFConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFConstants.h; sourceTree = ""; }; + 0925ABEC13D791770095FEFA /* PFConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFConstants.m; sourceTree = ""; }; + 0925ABED13D791770095FEFA /* PFObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObject.h; sourceTree = ""; }; + 0925ABEE13D791770095FEFA /* PFObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObject.m; sourceTree = ""; }; + 0925ABF113D791770095FEFA /* PFPush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPush.h; sourceTree = ""; }; + 0925ABF213D791770095FEFA /* PFPush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPush.m; sourceTree = ""; }; + 0925ABF313D791770095FEFA /* PFQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFQuery.h; sourceTree = ""; }; + 0925ABF413D791770095FEFA /* PFQuery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFQuery.m; sourceTree = ""; }; + 0925ABF513D791770095FEFA /* PFUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUser.h; sourceTree = ""; }; + 0925ABF613D791770095FEFA /* PFUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFUser.m; sourceTree = ""; }; + 09429995139C60A700DFA018 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 0942999A139C612100DFA018 /* libz.1.1.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.1.1.3.dylib; path = usr/lib/libz.1.1.3.dylib; sourceTree = SDKROOT; }; + 0942999C139C613700DFA018 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 095ACE9913C69BF700566243 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 097952A114CE462B00E6E88C /* PFBlockRetryer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFBlockRetryer.h; sourceTree = ""; }; + 097952A214CE462B00E6E88C /* PFBlockRetryer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFBlockRetryer.m; sourceTree = ""; }; + 09809FB11434F98C00EC3E74 /* PFInternalUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PFInternalUtils.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 09809FB21434F98C00EC3E74 /* PFInternalUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PFInternalUtils.m; sourceTree = ""; }; + 09809FB91434F98C00EC3E74 /* Framework.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Framework.plist; sourceTree = ""; }; + 09B119F614880776002B5594 /* PFGeoPoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFGeoPoint.h; sourceTree = ""; }; + 09B119F714880776002B5594 /* PFGeoPoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFGeoPoint.m; sourceTree = ""; }; + 09B119FB1488429D002B5594 /* PFGeoPointPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFGeoPointPrivate.h; sourceTree = ""; }; + 09BEF2D913D39E23001BBCDB /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 09D3365B139C54940098E916 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 09EEA12D1434FB1F00E3A3FA /* Parse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = Parse.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 09EEA12E1434FB1F00E3A3FA /* Parse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Parse.m; sourceTree = ""; }; + 09EEA1351435143500E3A3FA /* ParseInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParseInternal.h; sourceTree = ""; }; + 2FE3E9E9147B383200445083 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 44B78E11157D21B000A5E97F /* PFInstallation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFInstallation.h; sourceTree = ""; }; + 44B78E12157D21B000A5E97F /* PFInstallation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFInstallation.m; sourceTree = ""; }; + 498C29FE1551DC450034BB80 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 499E425515B6409000A2C28E /* PFProduct.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFProduct.h; sourceTree = ""; }; + 499E425615B6409000A2C28E /* PFProduct.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFProduct.m; sourceTree = ""; }; + 49FDE2EC158C138F00126F64 /* PFPurchase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPurchase.h; sourceTree = ""; }; + 49FDE2ED158C138F00126F64 /* PFPurchase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPurchase.m; sourceTree = ""; }; + 63723F6D1565A085007A1A73 /* PFRole.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRole.h; sourceTree = ""; }; + 63723F6E1565A085007A1A73 /* PFRole.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRole.m; sourceTree = ""; }; + 638CBBB415191435004F54E4 /* PFAnonymousUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAnonymousUtils.h; sourceTree = ""; }; + 638CBBB515191435004F54E4 /* PFAnonymousUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFAnonymousUtils.m; sourceTree = ""; }; + 6390EB1B151EDDA40001B779 /* PFMulticastDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMulticastDelegate.h; sourceTree = ""; }; + 6390EB1C151EDDA40001B779 /* PFMulticastDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMulticastDelegate.m; sourceTree = ""; }; + 6393F38B15D3018400C4F78D /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; + 63CA84EA1612660F002E09F8 /* Accounts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accounts.framework; path = System/Library/Frameworks/Accounts.framework; sourceTree = SDKROOT; }; + 63CBA36B1612829C0062C84A /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; + 64C47802147336C70092082F /* PFACL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFACL.h; sourceTree = ""; }; + 64C47803147336C70092082F /* PFACL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFACL.m; sourceTree = ""; }; + 7C1FDDCA14E1B1BD00A77007 /* PFCommandCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCommandCache.h; sourceTree = ""; }; + 7C1FDDCB14E1B1BD00A77007 /* PFCommandCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCommandCache.m; sourceTree = ""; }; + 7C9455DE15B8793F0037A86D /* PFCommandResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PFCommandResult.h; sourceTree = ""; }; + 7CF213BA16D41D980065CF1A /* PFTaskQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTaskQueue.h; sourceTree = ""; }; + 7CF213BB16D41D980065CF1A /* PFTaskQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTaskQueue.m; sourceTree = ""; }; + 7CF87D38162FC8FB00FF5C22 /* PFCommandResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCommandResult.m; sourceTree = ""; }; + 805D3D9F15E31241007E8D10 /* PFCloud.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCloud.h; sourceTree = ""; }; + 805D3DA015E31241007E8D10 /* PFCloud.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PFCloud.m; sourceTree = ""; }; + 8083B859155DAB1B0023EEFA /* PFRelation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRelation.h; sourceTree = ""; }; + 8083B85A155DAB1B0023EEFA /* PFRelation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRelation.m; sourceTree = ""; }; + 8101A14619ACDA97008BB503 /* PFAlertView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAlertView.h; sourceTree = ""; }; + 8101A14719ACDA97008BB503 /* PFAlertView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFAlertView.m; sourceTree = ""; }; + 8103FA33198FC190000BAE3F /* BFTask+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "BFTask+Private.h"; sourceTree = ""; }; + 8103FA34198FC190000BAE3F /* BFTask+Private.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "BFTask+Private.m"; sourceTree = ""; }; + 8103FA35198FC190000BAE3F /* PFCategoryLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCategoryLoader.h; sourceTree = ""; }; + 8103FA36198FC190000BAE3F /* PFCategoryLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCategoryLoader.m; sourceTree = ""; }; + 8103FA42198FC25B000BAE3F /* Bolts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Bolts.framework; path = "Vendor/Bolts-ObjC/build/ios/Bolts.framework"; sourceTree = ""; }; + 8103FA44198FC267000BAE3F /* Bolts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Bolts.framework; path = "Vendor/Bolts-ObjC/build/osx/Bolts.framework"; sourceTree = ""; }; + 81068EBA1ADE462500A34D13 /* Parse_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Parse_Private.h; sourceTree = ""; }; + 81068EEF1AE0845D00A34D13 /* PFEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFEncoder.h; sourceTree = ""; }; + 81068EF01AE0845D00A34D13 /* PFEncoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFEncoder.m; sourceTree = ""; }; + 810749AC1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionFileDownloadTaskDelegate.h; sourceTree = ""; }; + 810749AD1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFURLSessionFileDownloadTaskDelegate.m; sourceTree = ""; }; + 810B7D751A0291FF003C0909 /* PFMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMacros.h; sourceTree = ""; }; + 810ECA6F1B573853002944D4 /* PFRelationPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRelationPrivate.h; sourceTree = ""; }; + 810ECC611B573B96002944D4 /* ParseUnitTests-iOS-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "ParseUnitTests-iOS-Info.plist"; sourceTree = ""; }; + 810ECC621B573B96002944D4 /* ParseUnitTests-OSX-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "ParseUnitTests-OSX-Info.plist"; sourceTree = ""; }; + 810ECC6C1B573C6B002944D4 /* ParseUnitTests-iOS-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ParseUnitTests-iOS-Bridging-Header.h"; sourceTree = ""; }; + 810ECC6D1B573C6B002944D4 /* ParseUnitTests-OSX-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ParseUnitTests-OSX-Bridging-Header.h"; sourceTree = ""; }; + 810ECC6E1B573C6B002944D4 /* SwiftSubclass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSubclass.swift; sourceTree = ""; }; + 810ECC721B573CC5002944D4 /* OCMock+Parse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMock+Parse.h"; sourceTree = ""; }; + 810ECC731B573CC5002944D4 /* OCMock+Parse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OCMock+Parse.m"; sourceTree = ""; }; + 810ECC781B573D28002944D4 /* PFTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestCase.h; sourceTree = ""; }; + 810ECC791B573D28002944D4 /* PFTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestCase.m; sourceTree = ""; }; + 810ECC7B1B573D28002944D4 /* PFUnitTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUnitTestCase.h; sourceTree = ""; }; + 810ECC7C1B573D28002944D4 /* PFUnitTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFUnitTestCase.m; sourceTree = ""; }; + 811214711B3E1CF10052741B /* PFObjectBatchController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectBatchController.h; sourceTree = ""; }; + 811214721B3E1CF10052741B /* PFObjectBatchController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectBatchController.m; sourceTree = ""; }; + 81146C7C1A785203001F8473 /* PFRESTObjectCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTObjectCommand.h; sourceTree = ""; }; + 81146C7D1A785203001F8473 /* PFRESTObjectCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTObjectCommand.m; sourceTree = ""; }; + 8119C9961A76E28F0085B516 /* PFNetworkCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFNetworkCommand.h; sourceTree = ""; }; + 811AAF171B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectFilePersistenceControllerTests.m; sourceTree = ""; }; + 812145751AA4A4C1000B23F5 /* PFSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSession.h; sourceTree = ""; }; + 812145761AA4A4C1000B23F5 /* PFSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFSession.m; sourceTree = ""; }; + 8121457B1AA4A808000B23F5 /* PFRESTSessionCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTSessionCommand.h; sourceTree = ""; }; + 8121457C1AA4A808000B23F5 /* PFRESTSessionCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTSessionCommand.m; sourceTree = ""; }; + 8124C8711B26B9E700758E00 /* PFPinningObjectStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPinningObjectStore.h; sourceTree = ""; }; + 8124C8721B26B9E700758E00 /* PFPinningObjectStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPinningObjectStore.m; sourceTree = ""; }; + 8124C8811B27542A00758E00 /* PFCoreDataProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PFCoreDataProvider.h; sourceTree = ""; }; + 8124C8831B27588800758E00 /* PFPushChannelsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushChannelsController.h; sourceTree = ""; }; + 8124C8841B27588800758E00 /* PFPushChannelsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPushChannelsController.m; sourceTree = ""; }; + 8124C8881B276B8800758E00 /* PFObjectFilePersistenceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectFilePersistenceController.h; sourceTree = ""; }; + 8124C8891B276B8800758E00 /* PFObjectFilePersistenceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectFilePersistenceController.m; sourceTree = ""; }; + 8124C89D1B27BF0900758E00 /* PFSessionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSessionController.h; sourceTree = ""; }; + 8124C89E1B27BF0900758E00 /* PFSessionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFSessionController.m; sourceTree = ""; }; + 8124C8AA1B27D5D600758E00 /* PFSessionUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSessionUtilities.h; sourceTree = ""; }; + 8124C8AB1B27D5D600758E00 /* PFSessionUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFSessionUtilities.m; sourceTree = ""; }; + 812714861AE6F1270076AE8D /* ParseManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParseManager.h; sourceTree = ""; }; + 812714871AE6F1270076AE8D /* ParseManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ParseManager.m; sourceTree = ""; }; + 812B02921B5DE3EE003846EE /* PFURLSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSession.h; sourceTree = ""; }; + 812B02931B5DE3EE003846EE /* PFURLSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFURLSession.m; sourceTree = ""; }; + 812B02A61B5DE562003846EE /* PFCommandURLRequestConstructor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCommandURLRequestConstructor.h; sourceTree = ""; }; + 812B02A71B5DE562003846EE /* PFCommandURLRequestConstructor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCommandURLRequestConstructor.m; sourceTree = ""; }; + 812B62FE1B5F30D3009CEAA9 /* PFObjectFileCoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectFileCoder.h; sourceTree = ""; }; + 812B62FF1B5F30D3009CEAA9 /* PFObjectFileCoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectFileCoder.m; sourceTree = ""; }; + 812B7AB61AF2FA4800D15FF5 /* PFQueryController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFQueryController.h; sourceTree = ""; }; + 812B7AB71AF2FA4800D15FF5 /* PFQueryController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFQueryController.m; sourceTree = ""; }; + 812FC61E1B0FF9FA0043C07F /* PFPurchaseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPurchaseController.h; sourceTree = ""; }; + 812FC61F1B0FF9FA0043C07F /* PFPurchaseController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPurchaseController.m; sourceTree = ""; }; + 81308B6B1B5781F500FFFF44 /* PFTestSwizzledMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestSwizzledMethod.h; sourceTree = ""; }; + 81308B6C1B5781F500FFFF44 /* PFTestSwizzledMethod.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestSwizzledMethod.m; sourceTree = ""; }; + 81308B6D1B5781F500FFFF44 /* PFTestSwizzlingUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestSwizzlingUtilities.h; sourceTree = ""; }; + 81308B6E1B5781F500FFFF44 /* PFTestSwizzlingUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestSwizzlingUtilities.m; sourceTree = ""; }; + 81329E8C1AE1E8840071EE3E /* PFReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFReachability.h; sourceTree = ""; }; + 81329E8D1AE1E8840071EE3E /* PFReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFReachability.m; sourceTree = ""; }; + 8139B12C1A7BF559002BEF84 /* third_party_licenses.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = third_party_licenses.txt; sourceTree = SOURCE_ROOT; }; + 813E76981B7A9BD000FA3294 /* PFErrorUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFErrorUtilities.h; sourceTree = ""; }; + 813E76991B7A9BD000FA3294 /* PFErrorUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFErrorUtilities.m; sourceTree = ""; }; + 8143E65B1AFC1BA5008C4E06 /* PFOfflineQueryController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFOfflineQueryController.h; sourceTree = ""; }; + 8143E65C1AFC1BA5008C4E06 /* PFOfflineQueryController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFOfflineQueryController.m; sourceTree = ""; }; + 8143E6611AFC1C7D008C4E06 /* PFCachedQueryController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCachedQueryController.h; sourceTree = ""; }; + 8143E6621AFC1C7D008C4E06 /* PFCachedQueryController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCachedQueryController.m; sourceTree = ""; }; + 81443B311A27838500F3FD17 /* PFDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFDevice.h; sourceTree = ""; }; + 81443B321A27838500F3FD17 /* PFDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFDevice.m; sourceTree = ""; }; + 814881421B795C63008763BF /* PFKeyValueCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFKeyValueCache.h; sourceTree = ""; }; + 814881431B795C63008763BF /* PFKeyValueCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFKeyValueCache.m; sourceTree = ""; }; + 814881441B795C63008763BF /* PFKeyValueCache_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFKeyValueCache_Private.h; sourceTree = ""; }; + 8148814C1B795CAC008763BF /* PFPropertyInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPropertyInfo.h; sourceTree = ""; }; + 8148814D1B795CAC008763BF /* PFPropertyInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPropertyInfo.m; sourceTree = ""; }; + 8148814E1B795CAC008763BF /* PFPropertyInfo_Runtime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPropertyInfo_Runtime.h; sourceTree = ""; }; + 8148814F1B795CAC008763BF /* PFPropertyInfo_Runtime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPropertyInfo_Runtime.m; sourceTree = ""; }; + 814881501B795CAC008763BF /* PFPropertyInfo_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPropertyInfo_Private.h; sourceTree = ""; }; + 8148815C1B795CD4008763BF /* PFMultiProcessFileLock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMultiProcessFileLock.h; sourceTree = ""; }; + 8148815D1B795CD4008763BF /* PFMultiProcessFileLock.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMultiProcessFileLock.m; sourceTree = ""; }; + 8148815E1B795CD4008763BF /* PFMultiProcessFileLockController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMultiProcessFileLockController.h; sourceTree = ""; }; + 8148815F1B795CD4008763BF /* PFMultiProcessFileLockController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMultiProcessFileLockController.m; sourceTree = ""; }; + 814915CC1B66D44500EFD14F /* ACLStateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ACLStateTests.m; sourceTree = ""; }; + 814915CD1B66D44500EFD14F /* ACLUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ACLUnitTests.m; sourceTree = ""; }; + 814915CE1B66D44500EFD14F /* AlertViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AlertViewTests.m; sourceTree = ""; }; + 814915CF1B66D44500EFD14F /* AnalyticsCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnalyticsCommandTests.m; sourceTree = ""; }; + 814915D01B66D44500EFD14F /* AnalyticsControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnalyticsControllerTests.m; sourceTree = ""; }; + 814915D11B66D44500EFD14F /* AnalyticsUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnalyticsUnitTests.m; sourceTree = ""; }; + 814915D21B66D44500EFD14F /* AnalyticsUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnalyticsUtilitiesTests.m; sourceTree = ""; }; + 814915D31B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnonymousAuthenticationProviderTests.m; sourceTree = ""; }; + 814915D41B66D44500EFD14F /* AnonymousUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnonymousUtilsTests.m; sourceTree = ""; }; + 814915D51B66D44500EFD14F /* BaseStateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BaseStateTests.m; sourceTree = ""; }; + 814915D61B66D44500EFD14F /* BlockRetryerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlockRetryerTests.m; sourceTree = ""; }; + 814915D71B66D44500EFD14F /* CloudCodeControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CloudCodeControllerTests.m; sourceTree = ""; }; + 814915D81B66D44500EFD14F /* CloudCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CloudCommandTests.m; sourceTree = ""; }; + 814915D91B66D44500EFD14F /* CloudUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CloudUnitTests.m; sourceTree = ""; }; + 814915DA1B66D44500EFD14F /* CommandResultTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommandResultTests.m; sourceTree = ""; }; + 814915DB1B66D44500EFD14F /* CommandUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommandUnitTests.m; sourceTree = ""; }; + 814915DC1B66D44500EFD14F /* CommandURLRequestConstructorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommandURLRequestConstructorTests.m; sourceTree = ""; }; + 814915DD1B66D44500EFD14F /* ConfigCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConfigCommandTests.m; sourceTree = ""; }; + 814915DE1B66D44500EFD14F /* ConfigControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConfigControllerTests.m; sourceTree = ""; }; + 814915DF1B66D44500EFD14F /* ConfigUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConfigUnitTests.m; sourceTree = ""; }; + 814915E01B66D44500EFD14F /* CurrentConfigControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CurrentConfigControllerTests.m; sourceTree = ""; }; + 814915E11B66D44500EFD14F /* DateFormatterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DateFormatterTests.m; sourceTree = ""; }; + 814915E21B66D44500EFD14F /* DecoderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DecoderTests.m; sourceTree = ""; }; + 814915E31B66D44500EFD14F /* DefaultACLControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DefaultACLControllerTests.m; sourceTree = ""; }; + 814915E41B66D44500EFD14F /* DeviceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DeviceTests.m; sourceTree = ""; }; + 814915E51B66D44500EFD14F /* ExtensionDataSharingMobileTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExtensionDataSharingMobileTests.m; sourceTree = ""; }; + 814915E61B66D44500EFD14F /* ExtensionDataSharingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExtensionDataSharingTests.m; sourceTree = ""; }; + 814915E71B66D44500EFD14F /* FieldOperationDecoderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FieldOperationDecoderTests.m; sourceTree = ""; }; + 814915E81B66D44500EFD14F /* FieldOperationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FieldOperationTests.m; sourceTree = ""; }; + 814915E91B66D44500EFD14F /* FileCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileCommandTests.m; sourceTree = ""; }; + 814915EA1B66D44500EFD14F /* FileControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileControllerTests.m; sourceTree = ""; }; + 814915EB1B66D44500EFD14F /* FileStateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileStateTests.m; sourceTree = ""; }; + 814915EC1B66D44500EFD14F /* FileUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileUnitTests.m; sourceTree = ""; }; + 814915ED1B66D44500EFD14F /* GeoPointLocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeoPointLocationTests.m; sourceTree = ""; }; + 814915EE1B66D44500EFD14F /* GeoPointUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeoPointUnitTests.m; sourceTree = ""; }; + 814915EF1B66D44500EFD14F /* IncrementUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IncrementUnitTests.m; sourceTree = ""; }; + 814915F01B66D44500EFD14F /* InstallationIdentifierUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstallationIdentifierUnitTests.m; sourceTree = ""; }; + 814915F11B66D44500EFD14F /* InstallationUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstallationUnitTests.m; sourceTree = ""; }; + 814915F21B66D44500EFD14F /* KeychainStoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KeychainStoreTests.m; sourceTree = ""; }; + 814915F31B66D44500EFD14F /* KeyValueCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KeyValueCacheTests.m; sourceTree = ""; }; + 814915F41B66D44500EFD14F /* LocationManagerMobileTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocationManagerMobileTests.m; sourceTree = ""; }; + 814915F51B66D44500EFD14F /* LocationManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocationManagerTests.m; sourceTree = ""; }; + 814915F61B66D44500EFD14F /* ObjectBatchCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectBatchCommandTests.m; sourceTree = ""; }; + 814915F71B66D44500EFD14F /* ObjectBatchControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectBatchControllerTests.m; sourceTree = ""; }; + 814915F81B66D44500EFD14F /* ObjectCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectCommandTests.m; sourceTree = ""; }; + 814915F91B66D44500EFD14F /* ObjectEstimatedDataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectEstimatedDataTests.m; sourceTree = ""; }; + 814915FA1B66D44500EFD14F /* ObjectFileCoderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectFileCoderTests.m; sourceTree = ""; }; + 814915FB1B66D44500EFD14F /* ObjectFileCodingLogicTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectFileCodingLogicTests.m; sourceTree = ""; }; + 814915FC1B66D44500EFD14F /* ObjectLocalIdStoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectLocalIdStoreTests.m; sourceTree = ""; }; + 814915FD1B66D44500EFD14F /* ObjectOfflineTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectOfflineTests.m; sourceTree = ""; }; + 814915FE1B66D44500EFD14F /* ObjectPinTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectPinTests.m; sourceTree = ""; }; + 814915FF1B66D44500EFD14F /* ObjectStateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectStateTests.m; sourceTree = ""; }; + 814916001B66D44500EFD14F /* ObjectSubclassingControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectSubclassingControllerTests.m; sourceTree = ""; }; + 814916011B66D44500EFD14F /* ObjectSubclassPropertiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectSubclassPropertiesTests.m; sourceTree = ""; }; + 814916021B66D44500EFD14F /* ObjectSubclassTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectSubclassTests.m; sourceTree = ""; }; + 814916031B66D44500EFD14F /* ObjectUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectUnitTests.m; sourceTree = ""; }; + 814916041B66D44500EFD14F /* ObjectUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjectUtilitiesTests.m; sourceTree = ""; }; + 814916051B66D44500EFD14F /* OfflineQueryControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OfflineQueryControllerTests.m; sourceTree = ""; }; + 814916061B66D44500EFD14F /* OfflineQueryLogicUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OfflineQueryLogicUnitTests.m; sourceTree = ""; }; + 814916071B66D44500EFD14F /* OperationSetUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OperationSetUnitTests.m; sourceTree = ""; }; + 814916081B66D44500EFD14F /* ParseModuleUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ParseModuleUnitTests.m; sourceTree = ""; }; + 814916091B66D44500EFD14F /* ParseSetupUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ParseSetupUnitTests.m; sourceTree = ""; }; + 8149160A1B66D44500EFD14F /* PinningObjectStoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PinningObjectStoreTests.m; sourceTree = ""; }; + 8149160B1B66D44500EFD14F /* PinUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PinUnitTests.m; sourceTree = ""; }; + 8149160C1B66D44500EFD14F /* ProductTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProductTests.m; sourceTree = ""; }; + 8149160D1B66D44500EFD14F /* PropertyInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PropertyInfoTests.m; sourceTree = ""; }; + 8149160E1B66D44500EFD14F /* PurchaseControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PurchaseControllerTests.m; sourceTree = ""; }; + 8149160F1B66D44500EFD14F /* PurchaseUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PurchaseUnitTests.m; sourceTree = ""; }; + 814916101B66D44500EFD14F /* PushChannelsControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushChannelsControllerTests.m; sourceTree = ""; }; + 814916111B66D44500EFD14F /* PushCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushCommandTests.m; sourceTree = ""; }; + 814916121B66D44500EFD14F /* PushControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushControllerTests.m; sourceTree = ""; }; + 814916131B66D44500EFD14F /* PushManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushManagerTests.m; sourceTree = ""; }; + 814916141B66D44500EFD14F /* PushMobileTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushMobileTests.m; sourceTree = ""; }; + 814916151B66D44500EFD14F /* PushStateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushStateTests.m; sourceTree = ""; }; + 814916161B66D44500EFD14F /* PushUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushUnitTests.m; sourceTree = ""; }; + 814916171B66D44500EFD14F /* QueryCachedControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryCachedControllerTests.m; sourceTree = ""; }; + 814916181B66D44500EFD14F /* QueryControllerUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryControllerUnitTests.m; sourceTree = ""; }; + 814916191B66D44500EFD14F /* QueryPredicateUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryPredicateUnitTests.m; sourceTree = ""; }; + 8149161A1B66D44500EFD14F /* QueryStateUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryStateUnitTests.m; sourceTree = ""; }; + 8149161B1B66D44500EFD14F /* QueryUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryUnitTests.m; sourceTree = ""; }; + 8149161C1B66D44500EFD14F /* QueryUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QueryUtilitiesTests.m; sourceTree = ""; }; + 8149161D1B66D44500EFD14F /* RelationStateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RelationStateTests.m; sourceTree = ""; }; + 8149161E1B66D44500EFD14F /* RelationUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RelationUnitTests.m; sourceTree = ""; }; + 8149161F1B66D44500EFD14F /* RoleUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoleUnitTests.m; sourceTree = ""; }; + 814916201B66D44500EFD14F /* SessionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SessionControllerTests.m; sourceTree = ""; }; + 814916211B66D44500EFD14F /* SessionUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SessionUnitTests.m; sourceTree = ""; }; + 814916221B66D44500EFD14F /* SessionUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SessionUtilitiesTests.m; sourceTree = ""; }; + 814916231B66D44500EFD14F /* SQLiteDatabaseTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQLiteDatabaseTest.m; sourceTree = ""; }; + 814916241B66D44500EFD14F /* URLSessionCommandRunnerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLSessionCommandRunnerTests.m; sourceTree = ""; }; + 814916251B66D44500EFD14F /* UserCommandTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserCommandTests.m; sourceTree = ""; }; + 814916261B66D44500EFD14F /* UserControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserControllerTests.m; sourceTree = ""; }; + 814916271B66D44500EFD14F /* UserFileCodingLogicTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserFileCodingLogicTests.m; sourceTree = ""; }; + 814916281B66D44500EFD14F /* UserUnitTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserUnitTests.m; sourceTree = ""; }; + 81493AA21A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTObjectBatchCommand.h; sourceTree = ""; }; + 81493AA31A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTObjectBatchCommand.m; sourceTree = ""; }; + 814B640E1A769EF500213055 /* PFLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PFLogger.h; path = Parse/Internal/PFLogger.h; sourceTree = SOURCE_ROOT; }; + 814B640F1A769EF500213055 /* PFLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PFLogger.m; path = Parse/Internal/PFLogger.m; sourceTree = SOURCE_ROOT; }; + 814B64101A769EF500213055 /* PFLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PFLogging.h; path = Parse/Internal/PFLogging.h; sourceTree = SOURCE_ROOT; }; + 814BCDEF1B4DF63600007B7F /* PFUserState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUserState.h; sourceTree = ""; }; + 814BCDF01B4DF63600007B7F /* PFUserState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFUserState.m; sourceTree = ""; }; + 814BCDF51B4DF66500007B7F /* PFMutableUserState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMutableUserState.h; sourceTree = ""; }; + 814BCDF61B4DF66500007B7F /* PFMutableUserState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMutableUserState.m; sourceTree = ""; }; + 814BCDFB1B4DF7E800007B7F /* PFUserState_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUserState_Private.h; sourceTree = ""; }; + 814C3AB01B6975DE00E307BB /* Test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Test.xcconfig; sourceTree = ""; }; + 815618FE1A1F79AC0076504A /* PFDateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFDateFormatter.h; sourceTree = ""; }; + 815618FF1A1F79AC0076504A /* PFDateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFDateFormatter.m; sourceTree = ""; }; + 8159609F1ABCA3B30069EBCC /* PFFileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFileManager.h; sourceTree = ""; }; + 815960A01ABCA3B30069EBCC /* PFFileManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFileManager.m; sourceTree = ""; }; + 815EE8EE19F976D50076FE5D /* PFRESTCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTCommand.h; sourceTree = ""; }; + 815EE8EF19F976D50076FE5D /* PFRESTCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTCommand.m; sourceTree = ""; }; + 815EE8F019F976D50076FE5D /* PFRESTCommand_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTCommand_Private.h; sourceTree = ""; }; + 815EE91B19F987910076FE5D /* PFRESTCloudCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTCloudCommand.h; sourceTree = ""; }; + 815EE91C19F987910076FE5D /* PFRESTCloudCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTCloudCommand.m; sourceTree = ""; }; + 815EE92119F989380076FE5D /* PFRESTConfigCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTConfigCommand.h; sourceTree = ""; }; + 815EE92219F989380076FE5D /* PFRESTConfigCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTConfigCommand.m; sourceTree = ""; }; + 815EE93A19FA56D20076FE5D /* PFHTTPURLRequestConstructor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFHTTPURLRequestConstructor.h; sourceTree = ""; }; + 815EE93B19FA56D20076FE5D /* PFHTTPURLRequestConstructor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFHTTPURLRequestConstructor.m; sourceTree = ""; }; + 815EE93F19FA5A390076FE5D /* PFHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFHTTPRequest.h; sourceTree = ""; }; + 815EE94419FAD12F0076FE5D /* PFRESTQueryCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTQueryCommand.h; sourceTree = ""; }; + 815EE94519FAD12F0076FE5D /* PFRESTQueryCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTQueryCommand.m; sourceTree = ""; }; + 8166FB991B4F2F08003841A2 /* PFUserConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUserConstants.h; sourceTree = ""; }; + 8166FB9A1B4F2F08003841A2 /* PFUserConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFUserConstants.m; sourceTree = ""; }; + 8166FC571B503741003841A2 /* PFAnalytics_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAnalytics_Private.h; sourceTree = ""; }; + 8166FC5A1B50374B003841A2 /* PFConfig_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFConfig_Private.h; sourceTree = ""; }; + 8166FC5D1B503755003841A2 /* PFObjectPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectPrivate.h; sourceTree = ""; }; + 8166FC611B50375D003841A2 /* PFOperationSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFOperationSet.h; sourceTree = ""; }; + 8166FC621B50375D003841A2 /* PFOperationSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFOperationSet.m; sourceTree = ""; }; + 8166FC691B50376D003841A2 /* PFOfflineObjectController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFOfflineObjectController.h; sourceTree = ""; }; + 8166FC6A1B50376D003841A2 /* PFOfflineObjectController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFOfflineObjectController.m; sourceTree = ""; }; + 8166FC6B1B50376D003841A2 /* PFObjectController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectController.h; sourceTree = ""; }; + 8166FC6C1B50376D003841A2 /* PFObjectController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectController.m; sourceTree = ""; }; + 8166FC6D1B50376D003841A2 /* PFObjectController_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectController_Private.h; sourceTree = ""; }; + 8166FC6E1B50376D003841A2 /* PFObjectControlling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectControlling.h; sourceTree = ""; }; + 8166FC7B1B503787003841A2 /* PFFile_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFile_Private.h; sourceTree = ""; }; + 8166FC7F1B503794003841A2 /* PFInstallationIdentifierStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFInstallationIdentifierStore.h; sourceTree = ""; }; + 8166FC801B503794003841A2 /* PFInstallationIdentifierStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFInstallationIdentifierStore.m; sourceTree = ""; }; + 8166FC811B503794003841A2 /* PFInstallationIdentifierStore_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFInstallationIdentifierStore_Private.h; sourceTree = ""; }; + 8166FC821B503794003841A2 /* PFInstallationPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFInstallationPrivate.h; sourceTree = ""; }; + 8166FC8C1B5037F4003841A2 /* PFProduct+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PFProduct+Private.h"; sourceTree = ""; }; + 8166FC8E1B5037F4003841A2 /* PFProductsRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFProductsRequestHandler.h; sourceTree = ""; }; + 8166FC8F1B5037F5003841A2 /* PFProductsRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFProductsRequestHandler.m; sourceTree = ""; }; + 8166FC931B503809003841A2 /* PFPushPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushPrivate.h; sourceTree = ""; }; + 8166FC961B50381B003841A2 /* PFQueryPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFQueryPrivate.h; sourceTree = ""; }; + 8166FC991B503830003841A2 /* PFSession_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSession_Private.h; sourceTree = ""; }; + 8166FC9C1B503847003841A2 /* PFUserPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUserPrivate.h; sourceTree = ""; }; + 8166FCA11B503886003841A2 /* PFOfflineQueryLogic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFOfflineQueryLogic.h; sourceTree = ""; }; + 8166FCA21B503886003841A2 /* PFOfflineQueryLogic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFOfflineQueryLogic.m; sourceTree = ""; }; + 8166FCA41B503886003841A2 /* PFOfflineStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFOfflineStore.h; sourceTree = ""; }; + 8166FCA51B503886003841A2 /* PFOfflineStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFOfflineStore.m; sourceTree = ""; }; + 8166FCA71B503886003841A2 /* PFPin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPin.h; sourceTree = ""; }; + 8166FCA81B503886003841A2 /* PFPin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPin.m; sourceTree = ""; }; + 8166FCAA1B503886003841A2 /* PFSQLiteDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSQLiteDatabase.h; sourceTree = ""; }; + 8166FCAB1B503886003841A2 /* PFSQLiteDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFSQLiteDatabase.m; sourceTree = ""; }; + 8166FCAC1B503886003841A2 /* PFSQLiteDatabaseResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSQLiteDatabaseResult.h; sourceTree = ""; }; + 8166FCAD1B503886003841A2 /* PFSQLiteDatabaseResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFSQLiteDatabaseResult.m; sourceTree = ""; }; + 8166FCAE1B503886003841A2 /* PFSQLiteStatement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSQLiteStatement.h; sourceTree = ""; }; + 8166FCAF1B503886003841A2 /* PFSQLiteStatement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFSQLiteStatement.m; sourceTree = ""; }; + 8166FCC91B5038B7003841A2 /* PFPaymentTransactionObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPaymentTransactionObserver.h; sourceTree = ""; }; + 8166FCCA1B5038B7003841A2 /* PFPaymentTransactionObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPaymentTransactionObserver.m; sourceTree = ""; }; + 8166FCCB1B5038B7003841A2 /* PFPaymentTransactionObserver_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPaymentTransactionObserver_Private.h; sourceTree = ""; }; + 8166FCD11B503914003841A2 /* PFUserAuthenticationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUserAuthenticationController.h; sourceTree = ""; }; + 8166FCD21B503914003841A2 /* PFUserAuthenticationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFUserAuthenticationController.m; sourceTree = ""; }; + 8166FCD51B503914003841A2 /* PFAnonymousAuthenticationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAnonymousAuthenticationProvider.h; sourceTree = ""; }; + 8166FCD61B503914003841A2 /* PFAnonymousAuthenticationProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFAnonymousAuthenticationProvider.m; sourceTree = ""; }; + 8166FCD71B503914003841A2 /* PFAnonymousUtils_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAnonymousUtils_Private.h; sourceTree = ""; }; + 8166FCD81B503914003841A2 /* PFAuthenticationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAuthenticationProvider.h; sourceTree = ""; }; + 8166FCE61B504083003841A2 /* PFPushManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushManager.h; sourceTree = ""; }; + 8166FCE71B504083003841A2 /* PFPushManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPushManager.m; sourceTree = ""; }; + 816AC9B81A3F48250031D94C /* PFApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFApplication.h; sourceTree = ""; }; + 816AC9B91A3F48250031D94C /* PFApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFApplication.m; sourceTree = ""; }; + 816F449B1A8E8933009CDB32 /* ParseUnitTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ParseUnitTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 816F97101A93FAC400CADE60 /* PFNullability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFNullability.h; sourceTree = ""; }; + 818D049919A3B84500BEE20F /* PFThreadsafety.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFThreadsafety.h; sourceTree = ""; }; + 818D049A19A3B84500BEE20F /* PFThreadsafety.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFThreadsafety.m; sourceTree = ""; }; + 818D58681B5D9F4B00813989 /* PFURLSessionCommandRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionCommandRunner.h; sourceTree = ""; }; + 818D58691B5D9F4B00813989 /* PFURLSessionCommandRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFURLSessionCommandRunner.m; sourceTree = ""; }; + 818D586E1B5DA43800813989 /* PFCommandRunning.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCommandRunning.m; sourceTree = ""; }; + 818D58711B5DAAFE00813989 /* PFCommandRunningConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCommandRunningConstants.h; sourceTree = ""; }; + 818D58721B5DAAFE00813989 /* PFCommandRunningConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCommandRunningConstants.m; sourceTree = ""; }; + 818D6F121B3C8D1900F94C82 /* PFObjectLocalIdStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectLocalIdStore.h; sourceTree = ""; }; + 818D6F131B3C8D1900F94C82 /* PFObjectLocalIdStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectLocalIdStore.m; sourceTree = ""; }; + 818D6F1E1B3DCB5A00F94C82 /* PFObjectEstimatedData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectEstimatedData.h; sourceTree = ""; }; + 818D6F1F1B3DCB5A00F94C82 /* PFObjectEstimatedData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectEstimatedData.m; sourceTree = ""; }; + 81951F141ACB90DA00E142EB /* PFJSONSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFJSONSerialization.h; sourceTree = ""; }; + 81951F151ACB90DA00E142EB /* PFJSONSerialization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFJSONSerialization.m; sourceTree = ""; }; + 8196D5591B0AB64B000465A1 /* PFAnalyticsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAnalyticsController.h; sourceTree = ""; }; + 8196D55A1B0AB64B000465A1 /* PFAnalyticsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFAnalyticsController.m; sourceTree = ""; }; + 8196D55F1B0AB661000465A1 /* PFAnalyticsUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAnalyticsUtilities.h; sourceTree = ""; }; + 8196D5601B0AB661000465A1 /* PFAnalyticsUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFAnalyticsUtilities.m; sourceTree = ""; }; + 8196D58B1B0BD23B000465A1 /* PFCoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCoreManager.h; sourceTree = ""; }; + 8196D58C1B0BD23B000465A1 /* PFCoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCoreManager.m; sourceTree = ""; }; + 819A4B061A67330200D01241 /* PFHash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFHash.h; sourceTree = ""; }; + 819A4B071A67330200D01241 /* PFHash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFHash.m; sourceTree = ""; }; + 81A016251B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFExtensionDataSharingTestHelper.h; sourceTree = ""; }; + 81A016261B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFExtensionDataSharingTestHelper.m; sourceTree = ""; }; + 81A2458B1B1E99C6006A6953 /* PFFieldOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFieldOperation.h; sourceTree = ""; }; + 81A2458C1B1E99C6006A6953 /* PFFieldOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFieldOperation.m; sourceTree = ""; }; + 81A245911B1E99EA006A6953 /* PFFieldOperationDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFieldOperationDecoder.h; sourceTree = ""; }; + 81A245921B1E99EA006A6953 /* PFFieldOperationDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFieldOperationDecoder.m; sourceTree = ""; }; + 81A245F11B1FB188006A6953 /* PFDataProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFDataProvider.h; sourceTree = ""; }; + 81A715A21B423A4100A504FC /* PFObjectUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectUtilities.h; sourceTree = ""; }; + 81A715A31B423A4100A504FC /* PFObjectUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectUtilities.m; sourceTree = ""; }; + 81ABC0FC1B5427EC00BA9009 /* PFUserController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUserController.h; sourceTree = ""; }; + 81ABC0FD1B5427EC00BA9009 /* PFUserController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFUserController.m; sourceTree = ""; }; + 81AFA6701B0ECD4E000763C0 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libOCMock.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 81AFA6751B0ECF90000763C0 /* OCMock.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OCMock.framework; path = ../Release/OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 81AFE0E51A1FDB7900AB6CB3 /* PFRESTUserCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTUserCommand.h; sourceTree = ""; }; + 81AFE0E61A1FDB7900AB6CB3 /* PFRESTUserCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTUserCommand.m; sourceTree = ""; }; + 81B3F2531AC9D4E100A92677 /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; + 81BB6E1F1B0E7A1A00465C38 /* PFBase64Encoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFBase64Encoder.h; sourceTree = ""; }; + 81BB6E201B0E7A1A00465C38 /* PFBase64Encoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFBase64Encoder.m; sourceTree = ""; }; + 81BBE12D19FFCB3700622646 /* PFURLConstructor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLConstructor.h; sourceTree = ""; }; + 81BBE12E19FFCB3700622646 /* PFURLConstructor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFURLConstructor.m; sourceTree = ""; }; + 81BBE1331A0062B800622646 /* PFRESTAnalyticsCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTAnalyticsCommand.h; sourceTree = ""; }; + 81BBE1341A0062B800622646 /* PFRESTAnalyticsCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTAnalyticsCommand.m; sourceTree = ""; }; + 81BCB4BD1B744626006659CB /* PFURLSessionDataTaskDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionDataTaskDelegate.h; sourceTree = ""; }; + 81BCB4BE1B744626006659CB /* PFURLSessionDataTaskDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFURLSessionDataTaskDelegate.m; sourceTree = ""; }; + 81BCB4BF1B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionDataTaskDelegate_Private.h; sourceTree = ""; }; + 81BCB4C01B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionJSONDataTaskDelegate.h; sourceTree = ""; }; + 81BCB4C11B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFURLSessionJSONDataTaskDelegate.m; sourceTree = ""; }; + 81BCB4C21B744626006659CB /* PFURLSessionUploadTaskDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionUploadTaskDelegate.h; sourceTree = ""; }; + 81BCB4C31B744626006659CB /* PFURLSessionUploadTaskDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFURLSessionUploadTaskDelegate.m; sourceTree = ""; }; + 81BF4AB41B0BF3E500A3D75B /* PFConfigController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFConfigController.h; sourceTree = ""; }; + 81BF4AB51B0BF3E500A3D75B /* PFConfigController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFConfigController.m; sourceTree = ""; }; + 81BF4ABA1B0BF64B00A3D75B /* PFCurrentConfigController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCurrentConfigController.h; sourceTree = ""; }; + 81BF4ABB1B0BF64B00A3D75B /* PFCurrentConfigController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCurrentConfigController.m; sourceTree = ""; }; + 81C09F861AF97A490043B49C /* ParseUnitTests-OSX.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ParseUnitTests-OSX.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 81C1EE471AE1EF960031C438 /* PFWeakValue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFWeakValue.h; sourceTree = ""; }; + 81C1EE481AE1EF960031C438 /* PFWeakValue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFWeakValue.m; sourceTree = ""; }; + 81C3821C19CCA89E0066284A /* Parse.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Parse.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 81C382B919D0AC380066284A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 81C6BDEC1B4DB16500553A83 /* PFInstallationConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFInstallationConstants.h; sourceTree = ""; }; + 81C6BDED1B4DB16500553A83 /* PFInstallationConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFInstallationConstants.m; sourceTree = ""; }; + 81C6BDF31B4DD32700553A83 /* PFCurrentObjectControlling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCurrentObjectControlling.h; sourceTree = ""; }; + 81C76EE71B4B201E0031C2FD /* PFObjectConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectConstants.h; sourceTree = ""; }; + 81C76EEA1B4B218C0031C2FD /* PFObjectConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectConstants.m; sourceTree = ""; }; + 81C7F4891AF4110B007B5418 /* PFQueryUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFQueryUtilities.h; sourceTree = ""; }; + 81C7F48A1AF4110B007B5418 /* PFQueryUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFQueryUtilities.m; sourceTree = ""; }; + 81C7F4971AF42187007B5418 /* PFFileState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFileState.h; sourceTree = ""; }; + 81C7F4981AF42187007B5418 /* PFFileState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFileState.m; sourceTree = ""; }; + 81C7F49D1AF421FF007B5418 /* PFFileState_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFileState_Private.h; sourceTree = ""; }; + 81C7F4A01AF4220A007B5418 /* PFMutableFileState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMutableFileState.h; sourceTree = ""; }; + 81C7F4A11AF4220A007B5418 /* PFMutableFileState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMutableFileState.m; sourceTree = ""; }; + 81C7F4A71AF42BD9007B5418 /* PFMutableQueryState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMutableQueryState.h; sourceTree = ""; }; + 81C7F4A81AF42BD9007B5418 /* PFMutableQueryState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMutableQueryState.m; sourceTree = ""; }; + 81C7F4A91AF42BD9007B5418 /* PFQueryState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFQueryState.h; sourceTree = ""; }; + 81C7F4AA1AF42BD9007B5418 /* PFQueryState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFQueryState.m; sourceTree = ""; }; + 81C7F4AB1AF42BD9007B5418 /* PFQueryState_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFQueryState_Private.h; sourceTree = ""; }; + 81C9C9F519FEA89200D514C5 /* PFRESTPushCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTPushCommand.h; sourceTree = ""; }; + 81C9C9F619FEA89200D514C5 /* PFRESTPushCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTPushCommand.m; sourceTree = ""; }; + 81C9CA0419FECF5F00D514C5 /* PFRESTFileCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRESTFileCommand.h; sourceTree = ""; }; + 81C9CA0519FECF5F00D514C5 /* PFRESTFileCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRESTFileCommand.m; sourceTree = ""; }; + 81CB7F6D1B166FE500DC601D /* PFObjectState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectState.h; sourceTree = ""; }; + 81CB7F6E1B166FE500DC601D /* PFObjectState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectState.m; sourceTree = ""; }; + 81CB7F731B166FF500DC601D /* PFMutableObjectState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMutableObjectState.h; sourceTree = ""; }; + 81CB7F741B166FF500DC601D /* PFMutableObjectState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMutableObjectState.m; sourceTree = ""; }; + 81CB7F791B16710D00DC601D /* PFObjectState_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectState_Private.h; sourceTree = ""; }; + 81CB7F8C1B1795C000DC601D /* PFPushState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushState.h; sourceTree = ""; }; + 81CB7F8D1B1795C000DC601D /* PFPushState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPushState.m; sourceTree = ""; }; + 81CB7F921B1795CF00DC601D /* PFMutablePushState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMutablePushState.h; sourceTree = ""; }; + 81CB7F931B1795CF00DC601D /* PFMutablePushState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMutablePushState.m; sourceTree = ""; }; + 81CB7F981B17970400DC601D /* PFPushState_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushState_Private.h; sourceTree = ""; }; + 81CB7F9E1B1800E400DC601D /* PFPushController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushController.h; sourceTree = ""; }; + 81CB7F9F1B1800E400DC601D /* PFPushController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPushController.m; sourceTree = ""; }; + 81CD66521B4DA5A70042FC0B /* PFCurrentInstallationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCurrentInstallationController.h; sourceTree = ""; }; + 81CD66531B4DA5A70042FC0B /* PFCurrentInstallationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCurrentInstallationController.m; sourceTree = ""; }; + 81CD66581B4DA5BA0042FC0B /* PFInstallationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFInstallationController.h; sourceTree = ""; }; + 81CD66591B4DA5BA0042FC0B /* PFInstallationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFInstallationController.m; sourceTree = ""; }; + 81D0EE9719B0A2060000AE75 /* PFKeychainStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFKeychainStore.h; sourceTree = ""; }; + 81D0EE9819B0A2060000AE75 /* PFKeychainStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFKeychainStore.m; sourceTree = ""; }; + 81D843C71B012FBA007CEBCB /* PFCloudCodeController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCloudCodeController.h; sourceTree = ""; }; + 81D843C81B012FBA007CEBCB /* PFCloudCodeController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCloudCodeController.m; sourceTree = ""; }; + 81D8E75F1B7323ED004B014C /* HashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HashTests.m; sourceTree = ""; }; + 81DDB90B199A3EC200B50F35 /* ParseModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParseModule.h; sourceTree = ""; }; + 81DDB90C199A3EC200B50F35 /* ParseModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ParseModule.m; sourceTree = ""; }; + 81DEF07D199C42A300D86A21 /* PFFile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFile.h; sourceTree = ""; }; + 81DEF07E199C42A300D86A21 /* PFFile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFile.m; sourceTree = ""; }; + 81DEF089199D555800D86A21 /* PFNetworkActivityIndicatorManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFNetworkActivityIndicatorManager.h; sourceTree = ""; }; + 81DEF08A199D555800D86A21 /* PFNetworkActivityIndicatorManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFNetworkActivityIndicatorManager.m; sourceTree = ""; }; + 81E033561B573F3E00B25168 /* PFMockURLProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMockURLProtocol.h; sourceTree = ""; }; + 81E033571B573F3E00B25168 /* PFMockURLProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMockURLProtocol.m; sourceTree = ""; }; + 81E033581B573F3E00B25168 /* PFMockURLResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMockURLResponse.h; sourceTree = ""; }; + 81E033591B573F3E00B25168 /* PFMockURLResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMockURLResponse.m; sourceTree = ""; }; + 81E033641B573FC500B25168 /* PFTestSKPaymentQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestSKPaymentQueue.h; sourceTree = ""; }; + 81E033651B573FC500B25168 /* PFTestSKPaymentQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestSKPaymentQueue.m; sourceTree = ""; }; + 81E033661B573FC500B25168 /* PFTestSKPaymentTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestSKPaymentTransaction.h; sourceTree = ""; }; + 81E033671B573FC500B25168 /* PFTestSKPaymentTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestSKPaymentTransaction.m; sourceTree = ""; }; + 81E033681B573FC500B25168 /* PFTestSKProduct.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestSKProduct.h; sourceTree = ""; }; + 81E033691B573FC500B25168 /* PFTestSKProduct.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestSKProduct.m; sourceTree = ""; }; + 81E0336A1B573FC500B25168 /* PFTestSKProductsRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestSKProductsRequest.h; sourceTree = ""; }; + 81E0336B1B573FC500B25168 /* PFTestSKProductsRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestSKProductsRequest.m; sourceTree = ""; }; + 81E0336C1B573FC500B25168 /* PFTestSKProductsResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFTestSKProductsResponse.h; sourceTree = ""; }; + 81E0336D1B573FC500B25168 /* PFTestSKProductsResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFTestSKProductsResponse.m; sourceTree = ""; }; + 81E0337C1B57441F00B25168 /* CLLocationManager+TestAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CLLocationManager+TestAdditions.h"; sourceTree = ""; }; + 81E0337D1B57441F00B25168 /* CLLocationManager+TestAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CLLocationManager+TestAdditions.m"; sourceTree = ""; }; + 81E2D5AF19DDAAB5009053A1 /* PFAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAssert.h; sourceTree = ""; }; + 81E7A21A1B602560006CB680 /* PFUserFileCodingLogic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFUserFileCodingLogic.h; sourceTree = ""; }; + 81E7A21B1B602560006CB680 /* PFUserFileCodingLogic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFUserFileCodingLogic.m; sourceTree = ""; }; + 81E7A2231B6042BD006CB680 /* PFObjectFileCodingLogic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectFileCodingLogic.h; sourceTree = ""; }; + 81E7A2241B6042BD006CB680 /* PFObjectFileCodingLogic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectFileCodingLogic.m; sourceTree = ""; }; + 81EB595C1AF46434001EA1FC /* PFFileController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFileController.h; sourceTree = ""; }; + 81EB595D1AF46434001EA1FC /* PFFileController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFileController.m; sourceTree = ""; }; + 81EB6632198A7FA600851598 /* PFConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFConfig.h; sourceTree = ""; }; + 81EB6633198A7FA600851598 /* PFConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFConfig.m; sourceTree = ""; }; + 81EBF34B1B33E82200991947 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + 81EDD4D11B59A6EC002F69C0 /* PFCommandRunning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCommandRunning.h; sourceTree = ""; }; + 81EEE1AE1B446D600087AC4D /* PFCurrentUserController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFCurrentUserController.h; sourceTree = ""; }; + 81EEE1AF1B446D600087AC4D /* PFCurrentUserController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFCurrentUserController.m; sourceTree = ""; }; + 91115EF71A097AF30092D1C9 /* PFEventuallyPin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFEventuallyPin.h; sourceTree = ""; }; + 91115EF81A097AF30092D1C9 /* PFEventuallyPin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFEventuallyPin.m; sourceTree = ""; }; + 913B9F2C1A311FF40040247C /* PFCommandCache_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PFCommandCache_Private.h; sourceTree = ""; }; + 919311D519AE5EB20008FF12 /* PFDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFDecoder.h; sourceTree = ""; }; + 919311D619AE5EB20008FF12 /* PFDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFDecoder.m; sourceTree = ""; }; + 91DF24901A09BA7600CFC7D4 /* PFEventuallyQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFEventuallyQueue.h; sourceTree = ""; }; + 91DF24911A09BA7600CFC7D4 /* PFEventuallyQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFEventuallyQueue.m; sourceTree = ""; }; + 91DF24941A09BAF100CFC7D4 /* PFPinningEventuallyQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPinningEventuallyQueue.h; sourceTree = ""; }; + 91DF24951A09BAF100CFC7D4 /* PFPinningEventuallyQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPinningEventuallyQueue.m; sourceTree = ""; }; + 91DF24981A0B0FF200CFC7D4 /* PFEventuallyQueue_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFEventuallyQueue_Private.h; sourceTree = ""; }; + 97010FAC1630B18F00AB761E /* ParseOSX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ParseOSX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 970110191630B1FE00AB761E /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 971AC55C1716405A00A4EB71 /* ParseOSX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ParseOSX.h; sourceTree = ""; }; + 9739513816B9D28E0010B884 /* PFAnalytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFAnalytics.h; sourceTree = ""; }; + 9739513916B9D28E0010B884 /* PFAnalytics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFAnalytics.m; sourceTree = ""; }; + 97A3EA0C161FB6A9007A96B2 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; + 97AA93B816780B7600445C2D /* FrameworkOSX.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = FrameworkOSX.plist; sourceTree = ""; }; + 97DE045016321428007154E8 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; + 97DE045916321492007154E8 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 97DE045B163214C0007154E8 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 97E18AE41623835600B17A67 /* PFLocationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFLocationManager.h; sourceTree = ""; }; + 97E18AE51623835600B17A67 /* PFLocationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFLocationManager.m; sourceTree = ""; }; + E9BBE98E16D18E5800CD7B52 /* PFObject+Subclass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "PFObject+Subclass.h"; path = "Parse/PFObject+Subclass.h"; sourceTree = SOURCE_ROOT; }; + E9E81E8316EEF93E001D034F /* PFSubclassing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSubclassing.h; sourceTree = ""; }; + F50C66311B33A708001941A6 /* PFPushUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushUtilities.h; sourceTree = ""; }; + F50C66321B33A708001941A6 /* PFPushUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPushUtilities.m; sourceTree = ""; }; + F51534F61B571E9100C49F56 /* PFACLPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFACLPrivate.h; sourceTree = ""; }; + F51534F81B571E9100C49F56 /* PFACLState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFACLState.h; sourceTree = ""; }; + F51534F91B571E9100C49F56 /* PFACLState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFACLState.m; sourceTree = ""; }; + F51534FA1B571E9100C49F56 /* PFACLState_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFACLState_Private.h; sourceTree = ""; }; + F51534FB1B571E9100C49F56 /* PFMutableACLState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMutableACLState.h; sourceTree = ""; }; + F51534FC1B571E9100C49F56 /* PFMutableACLState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMutableACLState.m; sourceTree = ""; }; + F51535571B57573700C49F56 /* PFDefaultACLController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFDefaultACLController.h; sourceTree = ""; }; + F51535581B57573700C49F56 /* PFDefaultACLController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFDefaultACLController.m; sourceTree = ""; }; + F51D06321B792CF10044539E /* PFSQLiteDatabaseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSQLiteDatabaseController.h; sourceTree = ""; }; + F51D06331B792CF10044539E /* PFSQLiteDatabaseController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFSQLiteDatabaseController.m; sourceTree = ""; }; + F51D06361B793A110044539E /* PFSQLiteDatabase_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSQLiteDatabase_Private.h; sourceTree = ""; }; + F5556A141B66F36000410837 /* URLSessionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLSessionTests.m; sourceTree = ""; }; + F5556A171B66F47900410837 /* PFURLSession_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSession_Private.h; sourceTree = ""; }; + F55ABB511B4F39DA00A0ECD5 /* BoltsSDK-iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "BoltsSDK-iOS.xcconfig"; sourceTree = ""; }; + F55ABB521B4F39DA00A0ECD5 /* BoltsSDK-OSX.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "BoltsSDK-OSX.xcconfig"; sourceTree = ""; }; + F55ABB531B4F39DA00A0ECD5 /* Parse-iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Parse-iOS.xcconfig"; sourceTree = ""; }; + F55ABB541B4F39DA00A0ECD5 /* Parse-OSX.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Parse-OSX.xcconfig"; sourceTree = ""; }; + F55ABB591B4F39DA00A0ECD5 /* ParseUnitTests-iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "ParseUnitTests-iOS.xcconfig"; sourceTree = ""; }; + F55ABB5A1B4F39DA00A0ECD5 /* ParseUnitTests-OSX.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "ParseUnitTests-OSX.xcconfig"; sourceTree = ""; }; + F55ABB5C1B4F39DA00A0ECD5 /* Common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Common.xcconfig; sourceTree = ""; }; + F55ABB601B4F39DA00A0ECD5 /* iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = iOS.xcconfig; sourceTree = ""; }; + F55ABB611B4F39DA00A0ECD5 /* OSX.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = OSX.xcconfig; sourceTree = ""; }; + F55ABB631B4F39DA00A0ECD5 /* Application.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Application.xcconfig; sourceTree = ""; }; + F55ABB641B4F39DA00A0ECD5 /* Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Framework.xcconfig; sourceTree = ""; }; + F55ABB651B4F39DA00A0ECD5 /* UnitTest.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UnitTest.xcconfig; sourceTree = ""; }; + F55ABB671B4F39DA00A0ECD5 /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + F55ABB681B4F39DA00A0ECD5 /* Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + F55ABB691B4F39DA00A0ECD5 /* Warnings.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + F55ABB7F1B4F3B1E00A0ECD5 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.4.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + F55ABB811B4F3B3200A0ECD5 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.4.sdk/System/Library/Frameworks/CoreLocation.framework; sourceTree = DEVELOPER_DIR; }; + F55C740B1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFURLSessionCommandRunner_Private.h; sourceTree = ""; }; + F5732DE01B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLSessionDataTaskDelegateTests.m; sourceTree = ""; }; + F586B34E1B1E3BD70082E3BD /* PFBaseState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFBaseState.h; sourceTree = ""; }; + F586B34F1B1E3BD70082E3BD /* PFBaseState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFBaseState.m; sourceTree = ""; }; + F5ADB9C51B6C503E002A819E /* TestFileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestFileManager.h; sourceTree = ""; }; + F5ADB9C61B6C503E002A819E /* TestFileManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestFileManager.m; sourceTree = ""; }; + F5ADB9C91B6C5047002A819E /* TestCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestCache.h; sourceTree = ""; }; + F5ADB9CA1B6C5047002A819E /* TestCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestCache.m; sourceTree = ""; }; + F5B0B3121B44A05100F3EBC4 /* PFPaymentTransactionObserver_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPaymentTransactionObserver_Private.h; sourceTree = ""; }; + F5B0B3141B44A21100F3EBC4 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.4.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; + F5C42CD21B34F68C00C720D8 /* PFObjectSubclassingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectSubclassingController.h; sourceTree = ""; }; + F5C42CD31B34F68C00C720D8 /* PFObjectSubclassingController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectSubclassingController.m; sourceTree = ""; }; + F5C42CD81B38761B00C720D8 /* PFObjectSubclassInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFObjectSubclassInfo.h; sourceTree = ""; }; + F5C42CD91B38761B00C720D8 /* PFObjectSubclassInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFObjectSubclassInfo.m; sourceTree = ""; }; + F5C8F2BE1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PFAsyncTaskQueue.h; sourceTree = ""; }; + F5C8F2BF1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PFAsyncTaskQueue.m; sourceTree = ""; }; + F5E381331B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLSessionUploadTaskDelegateTests.m; sourceTree = ""; }; + F5E8DE171B290FFF00EEA594 /* PFRelationState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFRelationState.h; sourceTree = ""; }; + F5E8DE181B290FFF00EEA594 /* PFRelationState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFRelationState.m; sourceTree = ""; }; + F5E8DE1D1B29112000EEA594 /* PFMutableRelationState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMutableRelationState.h; sourceTree = ""; }; + F5E8DE1E1B29112000EEA594 /* PFMutableRelationState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMutableRelationState.m; sourceTree = ""; }; + F5E8DE231B2912BC00EEA594 /* PFRelationState_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PFRelationState_Private.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 816F44711A8E8933009CDB32 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 81AFA6711B0ECD4E000763C0 /* libOCMock.a in Frameworks */, + 816F44741A8E8933009CDB32 /* Parse.framework in Frameworks */, + F5C42CC71B34C22100C720D8 /* AudioToolbox.framework in Frameworks */, + 816F44761A8E8933009CDB32 /* StoreKit.framework in Frameworks */, + 816F44771A8E8933009CDB32 /* libsqlite3.dylib in Frameworks */, + 816F44781A8E8933009CDB32 /* Accounts.framework in Frameworks */, + 816F44791A8E8933009CDB32 /* Social.framework in Frameworks */, + 816F447C1A8E8933009CDB32 /* Bolts.framework in Frameworks */, + F5B0B3151B44A21100F3EBC4 /* SystemConfiguration.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81C09F761AF97A490043B49C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F5B0B3171B44A2CA00F3EBC4 /* StoreKit.framework in Frameworks */, + 81AFA6761B0ECF90000763C0 /* OCMock.framework in Frameworks */, + 81C09F891AF97EA70043B49C /* ParseOSX.framework in Frameworks */, + 815868E21AF9818D009A5751 /* Bolts.framework in Frameworks */, + F5B0B3161B44A22300F3EBC4 /* SystemConfiguration.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81C3821819CCA89E0066284A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97010FA91630B18F00AB761E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 81B3F2021AC5DAA400A92677 /* Cocoa.framework in Frameworks */, + 81B3F2011AC5DA7600A92677 /* libsqlite3.dylib in Frameworks */, + 97DE045C163214C0007154E8 /* SystemConfiguration.framework in Frameworks */, + 97DE045A16321492007154E8 /* Security.framework in Frameworks */, + 97DE045116321428007154E8 /* CoreLocation.framework in Frameworks */, + 815868E71AF98731009A5751 /* Bolts.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0925ABB613D791770095FEFA /* Parse */ = { + isa = PBXGroup; + children = ( + 09809FAE1434F98C00EC3E74 /* Internal */, + 09809FB81434F98C00EC3E74 /* Resources */, + 97DE04271631E686007154E8 /* OSX */, + 09EEA12D1434FB1F00E3A3FA /* Parse.h */, + 09EEA12E1434FB1F00E3A3FA /* Parse.m */, + 64C47802147336C70092082F /* PFACL.h */, + 64C47803147336C70092082F /* PFACL.m */, + 9739513816B9D28E0010B884 /* PFAnalytics.h */, + 9739513916B9D28E0010B884 /* PFAnalytics.m */, + 638CBBB415191435004F54E4 /* PFAnonymousUtils.h */, + 638CBBB515191435004F54E4 /* PFAnonymousUtils.m */, + 805D3D9F15E31241007E8D10 /* PFCloud.h */, + 805D3DA015E31241007E8D10 /* PFCloud.m */, + 81EB6632198A7FA600851598 /* PFConfig.h */, + 81EB6633198A7FA600851598 /* PFConfig.m */, + 0925ABEB13D791770095FEFA /* PFConstants.h */, + 0925ABEC13D791770095FEFA /* PFConstants.m */, + 81DEF07D199C42A300D86A21 /* PFFile.h */, + 81DEF07E199C42A300D86A21 /* PFFile.m */, + 09B119F614880776002B5594 /* PFGeoPoint.h */, + 09B119F714880776002B5594 /* PFGeoPoint.m */, + 44B78E11157D21B000A5E97F /* PFInstallation.h */, + 44B78E12157D21B000A5E97F /* PFInstallation.m */, + 0925ABED13D791770095FEFA /* PFObject.h */, + 0925ABEE13D791770095FEFA /* PFObject.m */, + 499E425515B6409000A2C28E /* PFProduct.h */, + 499E425615B6409000A2C28E /* PFProduct.m */, + 49FDE2EC158C138F00126F64 /* PFPurchase.h */, + 49FDE2ED158C138F00126F64 /* PFPurchase.m */, + 0925ABF113D791770095FEFA /* PFPush.h */, + 0925ABF213D791770095FEFA /* PFPush.m */, + 0925ABF313D791770095FEFA /* PFQuery.h */, + 0925ABF413D791770095FEFA /* PFQuery.m */, + 8083B859155DAB1B0023EEFA /* PFRelation.h */, + 8083B85A155DAB1B0023EEFA /* PFRelation.m */, + 63723F6D1565A085007A1A73 /* PFRole.h */, + 63723F6E1565A085007A1A73 /* PFRole.m */, + 812145751AA4A4C1000B23F5 /* PFSession.h */, + 812145761AA4A4C1000B23F5 /* PFSession.m */, + 0925ABF513D791770095FEFA /* PFUser.h */, + 0925ABF613D791770095FEFA /* PFUser.m */, + E9BBE98E16D18E5800CD7B52 /* PFObject+Subclass.h */, + E9E81E8316EEF93E001D034F /* PFSubclassing.h */, + 816F97101A93FAC400CADE60 /* PFNullability.h */, + 81DEF089199D555800D86A21 /* PFNetworkActivityIndicatorManager.h */, + 81DEF08A199D555800D86A21 /* PFNetworkActivityIndicatorManager.m */, + ); + path = Parse; + sourceTree = ""; + }; + 09809FAE1434F98C00EC3E74 /* Internal */ = { + isa = PBXGroup; + children = ( + F51534F21B571E9100C49F56 /* ACL */, + 7C83A03B15B4A609005E2C8E /* Commands */, + 818D049D19A3B8FD00BEE20F /* HTTPRequest */, + 497DBE85155459CE004EC871 /* Views */, + 8103FA3F198FC196000BAE3F /* Tasks */, + 818D049819A3B82D00BEE20F /* ThreadSafety */, + 814B64171A769EF900213055 /* Logging */, + 8148815B1B795CD4008763BF /* MultiProcessLock */, + 81A207C31AEB0AA0008A5F1A /* Query */, + 81C7F48F1AF4215B007B5418 /* File */, + 8196D5501B0A9FBF000465A1 /* Analytics */, + 81D843C61B012FB0007CEBCB /* CloudCode */, + 81BF4AB21B0BF2FE00A3D75B /* Config */, + 8166FC9F1B503886003841A2 /* LocalDataStore */, + 81CB7F6A1B166FC700DC601D /* Object */, + 81CD664F1B4DA5A70042FC0B /* Installation */, + 812FC61C1B0FF9E90043C07F /* Purchase */, + F5E8DE0F1B290B3700EEA594 /* Relation */, + 81A2458A1B1E99C6006A6953 /* FieldOperation */, + 81CB7F891B17957F00DC601D /* Push */, + 8166FC8B1B5037F4003841A2 /* Product */, + 8124C89B1B27BEC900758E00 /* Session */, + 81EEE1AC1B446D600087AC4D /* User */, + 814881411B795C63008763BF /* KeyValueCache */, + 8148814B1B795CAC008763BF /* PropertyInfo */, + 09EEA1351435143500E3A3FA /* ParseInternal.h */, + 81A245F11B1FB188006A6953 /* PFDataProvider.h */, + 812714861AE6F1270076AE8D /* ParseManager.h */, + 812714871AE6F1270076AE8D /* ParseManager.m */, + 8124C8811B27542A00758E00 /* PFCoreDataProvider.h */, + 8196D58B1B0BD23B000465A1 /* PFCoreManager.h */, + 8196D58C1B0BD23B000465A1 /* PFCoreManager.m */, + 81068EBA1ADE462500A34D13 /* Parse_Private.h */, + 81DDB90B199A3EC200B50F35 /* ParseModule.h */, + 81DDB90C199A3EC200B50F35 /* ParseModule.m */, + 81E2D5AF19DDAAB5009053A1 /* PFAssert.h */, + 816AC9B81A3F48250031D94C /* PFApplication.h */, + 816AC9B91A3F48250031D94C /* PFApplication.m */, + 097952A114CE462B00E6E88C /* PFBlockRetryer.h */, + 097952A214CE462B00E6E88C /* PFBlockRetryer.m */, + 81068EEF1AE0845D00A34D13 /* PFEncoder.h */, + 81068EF01AE0845D00A34D13 /* PFEncoder.m */, + 919311D519AE5EB20008FF12 /* PFDecoder.h */, + 919311D619AE5EB20008FF12 /* PFDecoder.m */, + 81BB6E1F1B0E7A1A00465C38 /* PFBase64Encoder.h */, + 81BB6E201B0E7A1A00465C38 /* PFBase64Encoder.m */, + 815618FE1A1F79AC0076504A /* PFDateFormatter.h */, + 815618FF1A1F79AC0076504A /* PFDateFormatter.m */, + 8159609F1ABCA3B30069EBCC /* PFFileManager.h */, + 815960A01ABCA3B30069EBCC /* PFFileManager.m */, + 81951F141ACB90DA00E142EB /* PFJSONSerialization.h */, + 81951F151ACB90DA00E142EB /* PFJSONSerialization.m */, + 81443B311A27838500F3FD17 /* PFDevice.h */, + 81443B321A27838500F3FD17 /* PFDevice.m */, + 819A4B061A67330200D01241 /* PFHash.h */, + 819A4B071A67330200D01241 /* PFHash.m */, + 09B119FB1488429D002B5594 /* PFGeoPointPrivate.h */, + 09809FB11434F98C00EC3E74 /* PFInternalUtils.h */, + 09809FB21434F98C00EC3E74 /* PFInternalUtils.m */, + 813E76981B7A9BD000FA3294 /* PFErrorUtilities.h */, + 813E76991B7A9BD000FA3294 /* PFErrorUtilities.m */, + 81D0EE9719B0A2060000AE75 /* PFKeychainStore.h */, + 81D0EE9819B0A2060000AE75 /* PFKeychainStore.m */, + 81C1EE471AE1EF960031C438 /* PFWeakValue.h */, + 81C1EE481AE1EF960031C438 /* PFWeakValue.m */, + 81329E8C1AE1E8840071EE3E /* PFReachability.h */, + 81329E8D1AE1E8840071EE3E /* PFReachability.m */, + 810B7D751A0291FF003C0909 /* PFMacros.h */, + 6390EB1B151EDDA40001B779 /* PFMulticastDelegate.h */, + 6390EB1C151EDDA40001B779 /* PFMulticastDelegate.m */, + 7CF213BA16D41D980065CF1A /* PFTaskQueue.h */, + 7CF213BB16D41D980065CF1A /* PFTaskQueue.m */, + 97E18AE41623835600B17A67 /* PFLocationManager.h */, + 97E18AE51623835600B17A67 /* PFLocationManager.m */, + F5C8F2BE1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.h */, + F5C8F2BF1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.m */, + F586B34E1B1E3BD70082E3BD /* PFBaseState.h */, + F586B34F1B1E3BD70082E3BD /* PFBaseState.m */, + ); + path = Internal; + sourceTree = ""; + }; + 09809FB81434F98C00EC3E74 /* Resources */ = { + isa = PBXGroup; + children = ( + 09809FB91434F98C00EC3E74 /* Framework.plist */, + 97AA93B816780B7600445C2D /* FrameworkOSX.plist */, + 81B3F2531AC9D4E100A92677 /* Localizable.strings */, + 8139B12C1A7BF559002BEF84 /* third_party_licenses.txt */, + ); + path = Resources; + sourceTree = ""; + }; + 099C082113AC013900D71869 /* Tests */ = { + isa = PBXGroup; + children = ( + 814915CB1B66D44500EFD14F /* Unit */, + 810ECC6A1B573C6B002944D4 /* Other */, + 810ECC601B573B96002944D4 /* Resources */, + ); + path = Tests; + sourceTree = ""; + }; + 09D3363F139C54930098E916 = { + isa = PBXGroup; + children = ( + F55ABB501B4F39DA00A0ECD5 /* Configurations */, + 0925ABB613D791770095FEFA /* Parse */, + 099C082113AC013900D71869 /* Tests */, + 09D3364C139C54940098E916 /* Frameworks */, + 09D3364B139C54940098E916 /* Products */, + ); + indentWidth = 4; + sourceTree = ""; + usesTabs = 0; + }; + 09D3364B139C54940098E916 /* Products */ = { + isa = PBXGroup; + children = ( + 97010FAC1630B18F00AB761E /* ParseOSX.framework */, + 81C3821C19CCA89E0066284A /* Parse.framework */, + 816F449B1A8E8933009CDB32 /* ParseUnitTests-iOS.xctest */, + 81C09F861AF97A490043B49C /* ParseUnitTests-OSX.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 09D3364C139C54940098E916 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8103FA47198FC304000BAE3F /* Bolts */, + 81AFA6781B0ECF9B000763C0 /* OCMock */, + 813E97A91A26A76A00373BA7 /* System Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + 497DBE85155459CE004EC871 /* Views */ = { + isa = PBXGroup; + children = ( + 8101A14619ACDA97008BB503 /* PFAlertView.h */, + 8101A14719ACDA97008BB503 /* PFAlertView.m */, + ); + name = Views; + sourceTree = ""; + }; + 7C83A03B15B4A609005E2C8E /* Commands */ = { + isa = PBXGroup; + children = ( + 81EDD4BC1B59A6D8002F69C0 /* CommandRunner */, + 8119C9961A76E28F0085B516 /* PFNetworkCommand.h */, + 7C1FDDCA14E1B1BD00A77007 /* PFCommandCache.h */, + 7C1FDDCB14E1B1BD00A77007 /* PFCommandCache.m */, + 913B9F2C1A311FF40040247C /* PFCommandCache_Private.h */, + 7C9455DE15B8793F0037A86D /* PFCommandResult.h */, + 7CF87D38162FC8FB00FF5C22 /* PFCommandResult.m */, + 91115EF71A097AF30092D1C9 /* PFEventuallyPin.h */, + 91115EF81A097AF30092D1C9 /* PFEventuallyPin.m */, + 91DF24901A09BA7600CFC7D4 /* PFEventuallyQueue.h */, + 91DF24911A09BA7600CFC7D4 /* PFEventuallyQueue.m */, + 91DF24981A0B0FF200CFC7D4 /* PFEventuallyQueue_Private.h */, + 91DF24941A09BAF100CFC7D4 /* PFPinningEventuallyQueue.h */, + 91DF24951A09BAF100CFC7D4 /* PFPinningEventuallyQueue.m */, + 815EE8ED19F976D50076FE5D /* REST */, + ); + name = Commands; + sourceTree = ""; + }; + 8103FA3F198FC196000BAE3F /* Tasks */ = { + isa = PBXGroup; + children = ( + 8103FA33198FC190000BAE3F /* BFTask+Private.h */, + 8103FA34198FC190000BAE3F /* BFTask+Private.m */, + 8103FA35198FC190000BAE3F /* PFCategoryLoader.h */, + 8103FA36198FC190000BAE3F /* PFCategoryLoader.m */, + ); + name = Tasks; + sourceTree = ""; + }; + 8103FA47198FC304000BAE3F /* Bolts */ = { + isa = PBXGroup; + children = ( + 8103FA48198FC30C000BAE3F /* iOS */, + 8103FA49198FC311000BAE3F /* OSX */, + ); + name = Bolts; + sourceTree = ""; + }; + 8103FA48198FC30C000BAE3F /* iOS */ = { + isa = PBXGroup; + children = ( + 8103FA42198FC25B000BAE3F /* Bolts.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 8103FA49198FC311000BAE3F /* OSX */ = { + isa = PBXGroup; + children = ( + 8103FA44198FC267000BAE3F /* Bolts.framework */, + ); + name = OSX; + sourceTree = ""; + }; + 810ECC601B573B96002944D4 /* Resources */ = { + isa = PBXGroup; + children = ( + 810ECC611B573B96002944D4 /* ParseUnitTests-iOS-Info.plist */, + 810ECC621B573B96002944D4 /* ParseUnitTests-OSX-Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 810ECC6A1B573C6B002944D4 /* Other */ = { + isa = PBXGroup; + children = ( + F5ADB9C41B6C502F002A819E /* FileManager */, + F5ADB9C31B6C5028002A819E /* Cache */, + 81A016241B59E19D00B0C7ED /* ExtensionDataSharing */, + 81E0337B1B57441F00B25168 /* LocationManager */, + 81E033551B573F3E00B25168 /* NetworkMocking */, + 810ECC711B573CC5002944D4 /* OCMock */, + 81E033631B573FC500B25168 /* StoreKitMocking */, + 810ECC6B1B573C6B002944D4 /* Swift */, + 81308B6A1B5781F500FFFF44 /* Swizzling */, + 810ECC761B573D28002944D4 /* TestCases */, + ); + path = Other; + sourceTree = ""; + }; + 810ECC6B1B573C6B002944D4 /* Swift */ = { + isa = PBXGroup; + children = ( + 810ECC6C1B573C6B002944D4 /* ParseUnitTests-iOS-Bridging-Header.h */, + 810ECC6D1B573C6B002944D4 /* ParseUnitTests-OSX-Bridging-Header.h */, + 810ECC6E1B573C6B002944D4 /* SwiftSubclass.swift */, + ); + path = Swift; + sourceTree = ""; + }; + 810ECC711B573CC5002944D4 /* OCMock */ = { + isa = PBXGroup; + children = ( + 810ECC721B573CC5002944D4 /* OCMock+Parse.h */, + 810ECC731B573CC5002944D4 /* OCMock+Parse.m */, + ); + path = OCMock; + sourceTree = ""; + }; + 810ECC761B573D28002944D4 /* TestCases */ = { + isa = PBXGroup; + children = ( + 810ECC771B573D28002944D4 /* TestCase */, + 810ECC7A1B573D28002944D4 /* UnitTestCase */, + ); + path = TestCases; + sourceTree = ""; + }; + 810ECC771B573D28002944D4 /* TestCase */ = { + isa = PBXGroup; + children = ( + 810ECC781B573D28002944D4 /* PFTestCase.h */, + 810ECC791B573D28002944D4 /* PFTestCase.m */, + ); + path = TestCase; + sourceTree = ""; + }; + 810ECC7A1B573D28002944D4 /* UnitTestCase */ = { + isa = PBXGroup; + children = ( + 810ECC7B1B573D28002944D4 /* PFUnitTestCase.h */, + 810ECC7C1B573D28002944D4 /* PFUnitTestCase.m */, + ); + path = UnitTestCase; + sourceTree = ""; + }; + 811214701B3E1CDD0052741B /* BatchController */ = { + isa = PBXGroup; + children = ( + 811214711B3E1CF10052741B /* PFObjectBatchController.h */, + 811214721B3E1CF10052741B /* PFObjectBatchController.m */, + ); + path = BatchController; + sourceTree = ""; + }; + 8124C8701B26B9E700758E00 /* PinningStore */ = { + isa = PBXGroup; + children = ( + 8124C8711B26B9E700758E00 /* PFPinningObjectStore.h */, + 8124C8721B26B9E700758E00 /* PFPinningObjectStore.m */, + ); + path = PinningStore; + sourceTree = ""; + }; + 8124C8821B27588800758E00 /* ChannelsController */ = { + isa = PBXGroup; + children = ( + 8124C8831B27588800758E00 /* PFPushChannelsController.h */, + 8124C8841B27588800758E00 /* PFPushChannelsController.m */, + ); + path = ChannelsController; + sourceTree = ""; + }; + 8124C8871B276B8800758E00 /* FilePersistence */ = { + isa = PBXGroup; + children = ( + 8124C8881B276B8800758E00 /* PFObjectFilePersistenceController.h */, + 8124C8891B276B8800758E00 /* PFObjectFilePersistenceController.m */, + ); + path = FilePersistence; + sourceTree = ""; + }; + 8124C89B1B27BEC900758E00 /* Session */ = { + isa = PBXGroup; + children = ( + 8166FC991B503830003841A2 /* PFSession_Private.h */, + 8124C89C1B27BEFB00758E00 /* Controller */, + 8124C8A91B27D5C900758E00 /* Utilities */, + ); + path = Session; + sourceTree = ""; + }; + 8124C89C1B27BEFB00758E00 /* Controller */ = { + isa = PBXGroup; + children = ( + 8124C89D1B27BF0900758E00 /* PFSessionController.h */, + 8124C89E1B27BF0900758E00 /* PFSessionController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 8124C8A91B27D5C900758E00 /* Utilities */ = { + isa = PBXGroup; + children = ( + 8124C8AA1B27D5D600758E00 /* PFSessionUtilities.h */, + 8124C8AB1B27D5D600758E00 /* PFSessionUtilities.m */, + ); + path = Utilities; + sourceTree = ""; + }; + 812B02911B5DE3EE003846EE /* Session */ = { + isa = PBXGroup; + children = ( + F5556A171B66F47900410837 /* PFURLSession_Private.h */, + 812B02921B5DE3EE003846EE /* PFURLSession.h */, + 812B02931B5DE3EE003846EE /* PFURLSession.m */, + 81BCB4BC1B744626006659CB /* TaskDelegate */, + ); + path = Session; + sourceTree = ""; + }; + 812B02A51B5DE562003846EE /* URLRequestConstructor */ = { + isa = PBXGroup; + children = ( + 812B02A61B5DE562003846EE /* PFCommandURLRequestConstructor.h */, + 812B02A71B5DE562003846EE /* PFCommandURLRequestConstructor.m */, + ); + path = URLRequestConstructor; + sourceTree = ""; + }; + 812B62F61B5F303C009CEAA9 /* Coder */ = { + isa = PBXGroup; + children = ( + 812B62F71B5F303C009CEAA9 /* File */, + ); + path = Coder; + sourceTree = ""; + }; + 812B62F71B5F303C009CEAA9 /* File */ = { + isa = PBXGroup; + children = ( + 812B62FE1B5F30D3009CEAA9 /* PFObjectFileCoder.h */, + 812B62FF1B5F30D3009CEAA9 /* PFObjectFileCoder.m */, + 81E7A2231B6042BD006CB680 /* PFObjectFileCodingLogic.h */, + 81E7A2241B6042BD006CB680 /* PFObjectFileCodingLogic.m */, + ); + path = File; + sourceTree = ""; + }; + 812B7AB51AF2FA3F00D15FF5 /* Controller */ = { + isa = PBXGroup; + children = ( + 812B7AB61AF2FA4800D15FF5 /* PFQueryController.h */, + 812B7AB71AF2FA4800D15FF5 /* PFQueryController.m */, + 8143E65B1AFC1BA5008C4E06 /* PFOfflineQueryController.h */, + 8143E65C1AFC1BA5008C4E06 /* PFOfflineQueryController.m */, + 8143E6611AFC1C7D008C4E06 /* PFCachedQueryController.h */, + 8143E6621AFC1C7D008C4E06 /* PFCachedQueryController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 812FC61C1B0FF9E90043C07F /* Purchase */ = { + isa = PBXGroup; + children = ( + 812FC61D1B0FF9E90043C07F /* Controller */, + 8166FCC81B5038B7003841A2 /* PaymentTransactionObserver */, + ); + path = Purchase; + sourceTree = ""; + }; + 812FC61D1B0FF9E90043C07F /* Controller */ = { + isa = PBXGroup; + children = ( + 812FC61E1B0FF9FA0043C07F /* PFPurchaseController.h */, + 812FC61F1B0FF9FA0043C07F /* PFPurchaseController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 81308B6A1B5781F500FFFF44 /* Swizzling */ = { + isa = PBXGroup; + children = ( + 81308B6B1B5781F500FFFF44 /* PFTestSwizzledMethod.h */, + 81308B6C1B5781F500FFFF44 /* PFTestSwizzledMethod.m */, + 81308B6D1B5781F500FFFF44 /* PFTestSwizzlingUtilities.h */, + 81308B6E1B5781F500FFFF44 /* PFTestSwizzlingUtilities.m */, + ); + path = Swizzling; + sourceTree = ""; + }; + 813E97A91A26A76A00373BA7 /* System Frameworks */ = { + isa = PBXGroup; + children = ( + 63CA84EA1612660F002E09F8 /* Accounts.framework */, + 81EBF34B1B33E82200991947 /* AppKit.framework */, + 81C382B919D0AC380066284A /* AudioToolbox.framework */, + 09429995139C60A700DFA018 /* CFNetwork.framework */, + 970110191630B1FE00AB761E /* Cocoa.framework */, + 09D3365B139C54940098E916 /* CoreGraphics.framework */, + 6393F38B15D3018400C4F78D /* libsqlite3.dylib */, + 0942999A139C612100DFA018 /* libz.1.1.3.dylib */, + 2FE3E9E9147B383200445083 /* QuartzCore.framework */, + 97DE045916321492007154E8 /* Security.framework */, + 63CBA36B1612829C0062C84A /* Social.framework */, + 498C29FE1551DC450034BB80 /* StoreKit.framework */, + 095ACE9913C69BF700566243 /* AudioToolbox.framework */, + 97A3EA0C161FB6A9007A96B2 /* CoreLocation.framework */, + 09BEF2D913D39E23001BBCDB /* Security.framework */, + 0942999C139C613700DFA018 /* SystemConfiguration.framework */, + F55ABB811B4F3B3200A0ECD5 /* CoreLocation.framework */, + F55ABB7F1B4F3B1E00A0ECD5 /* UIKit.framework */, + F5B0B3141B44A21100F3EBC4 /* SystemConfiguration.framework */, + ); + name = "System Frameworks"; + sourceTree = ""; + }; + 814881411B795C63008763BF /* KeyValueCache */ = { + isa = PBXGroup; + children = ( + 814881421B795C63008763BF /* PFKeyValueCache.h */, + 814881431B795C63008763BF /* PFKeyValueCache.m */, + 814881441B795C63008763BF /* PFKeyValueCache_Private.h */, + ); + path = KeyValueCache; + sourceTree = ""; + }; + 8148814B1B795CAC008763BF /* PropertyInfo */ = { + isa = PBXGroup; + children = ( + 8148814C1B795CAC008763BF /* PFPropertyInfo.h */, + 8148814D1B795CAC008763BF /* PFPropertyInfo.m */, + 8148814E1B795CAC008763BF /* PFPropertyInfo_Runtime.h */, + 8148814F1B795CAC008763BF /* PFPropertyInfo_Runtime.m */, + 814881501B795CAC008763BF /* PFPropertyInfo_Private.h */, + ); + path = PropertyInfo; + sourceTree = ""; + }; + 8148815B1B795CD4008763BF /* MultiProcessLock */ = { + isa = PBXGroup; + children = ( + 8148815C1B795CD4008763BF /* PFMultiProcessFileLock.h */, + 8148815D1B795CD4008763BF /* PFMultiProcessFileLock.m */, + 8148815E1B795CD4008763BF /* PFMultiProcessFileLockController.h */, + 8148815F1B795CD4008763BF /* PFMultiProcessFileLockController.m */, + ); + path = MultiProcessLock; + sourceTree = ""; + }; + 814915CB1B66D44500EFD14F /* Unit */ = { + isa = PBXGroup; + children = ( + 814915CC1B66D44500EFD14F /* ACLStateTests.m */, + 814915CD1B66D44500EFD14F /* ACLUnitTests.m */, + 814915CE1B66D44500EFD14F /* AlertViewTests.m */, + 814915CF1B66D44500EFD14F /* AnalyticsCommandTests.m */, + 814915D01B66D44500EFD14F /* AnalyticsControllerTests.m */, + 814915D11B66D44500EFD14F /* AnalyticsUnitTests.m */, + 814915D21B66D44500EFD14F /* AnalyticsUtilitiesTests.m */, + 814915D31B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m */, + 814915D41B66D44500EFD14F /* AnonymousUtilsTests.m */, + 814915D51B66D44500EFD14F /* BaseStateTests.m */, + 814915D61B66D44500EFD14F /* BlockRetryerTests.m */, + 814915D71B66D44500EFD14F /* CloudCodeControllerTests.m */, + 814915D81B66D44500EFD14F /* CloudCommandTests.m */, + 814915D91B66D44500EFD14F /* CloudUnitTests.m */, + 814915DA1B66D44500EFD14F /* CommandResultTests.m */, + 814915DB1B66D44500EFD14F /* CommandUnitTests.m */, + 814915DC1B66D44500EFD14F /* CommandURLRequestConstructorTests.m */, + 814915DD1B66D44500EFD14F /* ConfigCommandTests.m */, + 814915DE1B66D44500EFD14F /* ConfigControllerTests.m */, + 814915DF1B66D44500EFD14F /* ConfigUnitTests.m */, + 814915E01B66D44500EFD14F /* CurrentConfigControllerTests.m */, + 814915E11B66D44500EFD14F /* DateFormatterTests.m */, + 814915E21B66D44500EFD14F /* DecoderTests.m */, + 814915E31B66D44500EFD14F /* DefaultACLControllerTests.m */, + 814915E41B66D44500EFD14F /* DeviceTests.m */, + 814915E51B66D44500EFD14F /* ExtensionDataSharingMobileTests.m */, + 814915E61B66D44500EFD14F /* ExtensionDataSharingTests.m */, + 814915E71B66D44500EFD14F /* FieldOperationDecoderTests.m */, + 814915E81B66D44500EFD14F /* FieldOperationTests.m */, + 814915E91B66D44500EFD14F /* FileCommandTests.m */, + 814915EA1B66D44500EFD14F /* FileControllerTests.m */, + 814915EB1B66D44500EFD14F /* FileStateTests.m */, + 814915EC1B66D44500EFD14F /* FileUnitTests.m */, + 814915ED1B66D44500EFD14F /* GeoPointLocationTests.m */, + 814915EE1B66D44500EFD14F /* GeoPointUnitTests.m */, + 81D8E75F1B7323ED004B014C /* HashTests.m */, + 814915EF1B66D44500EFD14F /* IncrementUnitTests.m */, + 814915F01B66D44500EFD14F /* InstallationIdentifierUnitTests.m */, + 814915F11B66D44500EFD14F /* InstallationUnitTests.m */, + 814915F21B66D44500EFD14F /* KeychainStoreTests.m */, + 814915F31B66D44500EFD14F /* KeyValueCacheTests.m */, + 814915F41B66D44500EFD14F /* LocationManagerMobileTests.m */, + 814915F51B66D44500EFD14F /* LocationManagerTests.m */, + 814915F61B66D44500EFD14F /* ObjectBatchCommandTests.m */, + 814915F71B66D44500EFD14F /* ObjectBatchControllerTests.m */, + 814915F81B66D44500EFD14F /* ObjectCommandTests.m */, + 814915F91B66D44500EFD14F /* ObjectEstimatedDataTests.m */, + 814915FA1B66D44500EFD14F /* ObjectFileCoderTests.m */, + 814915FB1B66D44500EFD14F /* ObjectFileCodingLogicTests.m */, + 811AAF171B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m */, + 814915FC1B66D44500EFD14F /* ObjectLocalIdStoreTests.m */, + 814915FD1B66D44500EFD14F /* ObjectOfflineTests.m */, + 814915FE1B66D44500EFD14F /* ObjectPinTests.m */, + 814915FF1B66D44500EFD14F /* ObjectStateTests.m */, + 814916001B66D44500EFD14F /* ObjectSubclassingControllerTests.m */, + 814916011B66D44500EFD14F /* ObjectSubclassPropertiesTests.m */, + 814916021B66D44500EFD14F /* ObjectSubclassTests.m */, + 814916031B66D44500EFD14F /* ObjectUnitTests.m */, + 814916041B66D44500EFD14F /* ObjectUtilitiesTests.m */, + 814916051B66D44500EFD14F /* OfflineQueryControllerTests.m */, + 814916061B66D44500EFD14F /* OfflineQueryLogicUnitTests.m */, + 814916071B66D44500EFD14F /* OperationSetUnitTests.m */, + 814916081B66D44500EFD14F /* ParseModuleUnitTests.m */, + 814916091B66D44500EFD14F /* ParseSetupUnitTests.m */, + 8149160A1B66D44500EFD14F /* PinningObjectStoreTests.m */, + 8149160B1B66D44500EFD14F /* PinUnitTests.m */, + 8149160C1B66D44500EFD14F /* ProductTests.m */, + 8149160D1B66D44500EFD14F /* PropertyInfoTests.m */, + 8149160E1B66D44500EFD14F /* PurchaseControllerTests.m */, + 8149160F1B66D44500EFD14F /* PurchaseUnitTests.m */, + 814916101B66D44500EFD14F /* PushChannelsControllerTests.m */, + 814916111B66D44500EFD14F /* PushCommandTests.m */, + 814916121B66D44500EFD14F /* PushControllerTests.m */, + 814916131B66D44500EFD14F /* PushManagerTests.m */, + 814916141B66D44500EFD14F /* PushMobileTests.m */, + 814916151B66D44500EFD14F /* PushStateTests.m */, + 814916161B66D44500EFD14F /* PushUnitTests.m */, + 814916171B66D44500EFD14F /* QueryCachedControllerTests.m */, + 814916181B66D44500EFD14F /* QueryControllerUnitTests.m */, + 814916191B66D44500EFD14F /* QueryPredicateUnitTests.m */, + 8149161A1B66D44500EFD14F /* QueryStateUnitTests.m */, + 8149161B1B66D44500EFD14F /* QueryUnitTests.m */, + 8149161C1B66D44500EFD14F /* QueryUtilitiesTests.m */, + 8149161D1B66D44500EFD14F /* RelationStateTests.m */, + 8149161E1B66D44500EFD14F /* RelationUnitTests.m */, + 8149161F1B66D44500EFD14F /* RoleUnitTests.m */, + 814916201B66D44500EFD14F /* SessionControllerTests.m */, + 814916211B66D44500EFD14F /* SessionUnitTests.m */, + 814916221B66D44500EFD14F /* SessionUtilitiesTests.m */, + 814916231B66D44500EFD14F /* SQLiteDatabaseTest.m */, + 814916241B66D44500EFD14F /* URLSessionCommandRunnerTests.m */, + F5732DE01B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m */, + F5556A141B66F36000410837 /* URLSessionTests.m */, + F5E381331B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m */, + 814916251B66D44500EFD14F /* UserCommandTests.m */, + 814916261B66D44500EFD14F /* UserControllerTests.m */, + 814916271B66D44500EFD14F /* UserFileCodingLogicTests.m */, + 814916281B66D44500EFD14F /* UserUnitTests.m */, + ); + path = Unit; + sourceTree = ""; + }; + 814B64171A769EF900213055 /* Logging */ = { + isa = PBXGroup; + children = ( + 814B640E1A769EF500213055 /* PFLogger.h */, + 814B640F1A769EF500213055 /* PFLogger.m */, + 814B64101A769EF500213055 /* PFLogging.h */, + ); + name = Logging; + path = ThreadSafety; + sourceTree = ""; + }; + 814BCDEE1B4DF61800007B7F /* State */ = { + isa = PBXGroup; + children = ( + 814BCDEF1B4DF63600007B7F /* PFUserState.h */, + 814BCDFB1B4DF7E800007B7F /* PFUserState_Private.h */, + 814BCDF01B4DF63600007B7F /* PFUserState.m */, + 814BCDF51B4DF66500007B7F /* PFMutableUserState.h */, + 814BCDF61B4DF66500007B7F /* PFMutableUserState.m */, + ); + path = State; + sourceTree = ""; + }; + 815EE8ED19F976D50076FE5D /* REST */ = { + isa = PBXGroup; + children = ( + 815EE90319F976E90076FE5D /* Command */, + ); + name = REST; + path = Commands; + sourceTree = ""; + }; + 815EE90319F976E90076FE5D /* Command */ = { + isa = PBXGroup; + children = ( + 815EE8EE19F976D50076FE5D /* PFRESTCommand.h */, + 815EE8F019F976D50076FE5D /* PFRESTCommand_Private.h */, + 815EE8EF19F976D50076FE5D /* PFRESTCommand.m */, + 81BBE1331A0062B800622646 /* PFRESTAnalyticsCommand.h */, + 81BBE1341A0062B800622646 /* PFRESTAnalyticsCommand.m */, + 815EE91B19F987910076FE5D /* PFRESTCloudCommand.h */, + 815EE91C19F987910076FE5D /* PFRESTCloudCommand.m */, + 815EE92119F989380076FE5D /* PFRESTConfigCommand.h */, + 815EE92219F989380076FE5D /* PFRESTConfigCommand.m */, + 81C9CA0419FECF5F00D514C5 /* PFRESTFileCommand.h */, + 81C9CA0519FECF5F00D514C5 /* PFRESTFileCommand.m */, + 81146C7C1A785203001F8473 /* PFRESTObjectCommand.h */, + 81146C7D1A785203001F8473 /* PFRESTObjectCommand.m */, + 81C9C9F519FEA89200D514C5 /* PFRESTPushCommand.h */, + 81C9C9F619FEA89200D514C5 /* PFRESTPushCommand.m */, + 815EE94419FAD12F0076FE5D /* PFRESTQueryCommand.h */, + 815EE94519FAD12F0076FE5D /* PFRESTQueryCommand.m */, + 8121457B1AA4A808000B23F5 /* PFRESTSessionCommand.h */, + 8121457C1AA4A808000B23F5 /* PFRESTSessionCommand.m */, + 81AFE0E51A1FDB7900AB6CB3 /* PFRESTUserCommand.h */, + 81AFE0E61A1FDB7900AB6CB3 /* PFRESTUserCommand.m */, + 81493AA21A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h */, + 81493AA31A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m */, + ); + name = Command; + sourceTree = ""; + }; + 815EE93E19FA56D90076FE5D /* RequestConstructor */ = { + isa = PBXGroup; + children = ( + 815EE93F19FA5A390076FE5D /* PFHTTPRequest.h */, + 815EE93A19FA56D20076FE5D /* PFHTTPURLRequestConstructor.h */, + 815EE93B19FA56D20076FE5D /* PFHTTPURLRequestConstructor.m */, + ); + name = RequestConstructor; + sourceTree = ""; + }; + 8166FB981B4F2EFD003841A2 /* Constants */ = { + isa = PBXGroup; + children = ( + 8166FB991B4F2F08003841A2 /* PFUserConstants.h */, + 8166FB9A1B4F2F08003841A2 /* PFUserConstants.m */, + ); + path = Constants; + sourceTree = ""; + }; + 8166FC601B50375D003841A2 /* OperationSet */ = { + isa = PBXGroup; + children = ( + 8166FC611B50375D003841A2 /* PFOperationSet.h */, + 8166FC621B50375D003841A2 /* PFOperationSet.m */, + ); + path = OperationSet; + sourceTree = ""; + }; + 8166FC671B50376D003841A2 /* Controller */ = { + isa = PBXGroup; + children = ( + 8166FC681B50376D003841A2 /* OfflineController */, + 8166FC6B1B50376D003841A2 /* PFObjectController.h */, + 8166FC6D1B50376D003841A2 /* PFObjectController_Private.h */, + 8166FC6C1B50376D003841A2 /* PFObjectController.m */, + 8166FC6E1B50376D003841A2 /* PFObjectControlling.h */, + ); + path = Controller; + sourceTree = ""; + }; + 8166FC681B50376D003841A2 /* OfflineController */ = { + isa = PBXGroup; + children = ( + 8166FC691B50376D003841A2 /* PFOfflineObjectController.h */, + 8166FC6A1B50376D003841A2 /* PFOfflineObjectController.m */, + ); + path = OfflineController; + sourceTree = ""; + }; + 8166FC7E1B503794003841A2 /* InstallationIdentifierStore */ = { + isa = PBXGroup; + children = ( + 8166FC7F1B503794003841A2 /* PFInstallationIdentifierStore.h */, + 8166FC801B503794003841A2 /* PFInstallationIdentifierStore.m */, + 8166FC811B503794003841A2 /* PFInstallationIdentifierStore_Private.h */, + ); + path = InstallationIdentifierStore; + sourceTree = ""; + }; + 8166FC8B1B5037F4003841A2 /* Product */ = { + isa = PBXGroup; + children = ( + 8166FC8C1B5037F4003841A2 /* PFProduct+Private.h */, + 8166FC8D1B5037F4003841A2 /* ProductsRequestHandler */, + ); + path = Product; + sourceTree = ""; + }; + 8166FC8D1B5037F4003841A2 /* ProductsRequestHandler */ = { + isa = PBXGroup; + children = ( + 8166FC8E1B5037F4003841A2 /* PFProductsRequestHandler.h */, + 8166FC8F1B5037F5003841A2 /* PFProductsRequestHandler.m */, + ); + path = ProductsRequestHandler; + sourceTree = ""; + }; + 8166FC9F1B503886003841A2 /* LocalDataStore */ = { + isa = PBXGroup; + children = ( + 8166FCA01B503886003841A2 /* OfflineQueryLogic */, + 8166FCA31B503886003841A2 /* OfflineStore */, + 8166FCA61B503886003841A2 /* Pin */, + 8166FCA91B503886003841A2 /* SQLite */, + ); + path = LocalDataStore; + sourceTree = ""; + }; + 8166FCA01B503886003841A2 /* OfflineQueryLogic */ = { + isa = PBXGroup; + children = ( + 8166FCA11B503886003841A2 /* PFOfflineQueryLogic.h */, + 8166FCA21B503886003841A2 /* PFOfflineQueryLogic.m */, + ); + path = OfflineQueryLogic; + sourceTree = ""; + }; + 8166FCA31B503886003841A2 /* OfflineStore */ = { + isa = PBXGroup; + children = ( + 8166FCA41B503886003841A2 /* PFOfflineStore.h */, + 8166FCA51B503886003841A2 /* PFOfflineStore.m */, + ); + path = OfflineStore; + sourceTree = ""; + }; + 8166FCA61B503886003841A2 /* Pin */ = { + isa = PBXGroup; + children = ( + 8166FCA71B503886003841A2 /* PFPin.h */, + 8166FCA81B503886003841A2 /* PFPin.m */, + ); + path = Pin; + sourceTree = ""; + }; + 8166FCA91B503886003841A2 /* SQLite */ = { + isa = PBXGroup; + children = ( + F51D06361B793A110044539E /* PFSQLiteDatabase_Private.h */, + 8166FCAA1B503886003841A2 /* PFSQLiteDatabase.h */, + 8166FCAB1B503886003841A2 /* PFSQLiteDatabase.m */, + F51D06321B792CF10044539E /* PFSQLiteDatabaseController.h */, + F51D06331B792CF10044539E /* PFSQLiteDatabaseController.m */, + 8166FCAC1B503886003841A2 /* PFSQLiteDatabaseResult.h */, + 8166FCAD1B503886003841A2 /* PFSQLiteDatabaseResult.m */, + 8166FCAE1B503886003841A2 /* PFSQLiteStatement.h */, + 8166FCAF1B503886003841A2 /* PFSQLiteStatement.m */, + ); + path = SQLite; + sourceTree = ""; + }; + 8166FCC81B5038B7003841A2 /* PaymentTransactionObserver */ = { + isa = PBXGroup; + children = ( + 8166FCC91B5038B7003841A2 /* PFPaymentTransactionObserver.h */, + 8166FCCA1B5038B7003841A2 /* PFPaymentTransactionObserver.m */, + 8166FCCB1B5038B7003841A2 /* PFPaymentTransactionObserver_Private.h */, + ); + path = PaymentTransactionObserver; + sourceTree = ""; + }; + 8166FCCF1B503914003841A2 /* AuthenticationProviders */ = { + isa = PBXGroup; + children = ( + 8166FCD01B503914003841A2 /* Controller */, + 8166FCD31B503914003841A2 /* Providers */, + ); + path = AuthenticationProviders; + sourceTree = ""; + }; + 8166FCD01B503914003841A2 /* Controller */ = { + isa = PBXGroup; + children = ( + 8166FCD11B503914003841A2 /* PFUserAuthenticationController.h */, + 8166FCD21B503914003841A2 /* PFUserAuthenticationController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 8166FCD31B503914003841A2 /* Providers */ = { + isa = PBXGroup; + children = ( + 8166FCD41B503914003841A2 /* Anonymous */, + 8166FCD81B503914003841A2 /* PFAuthenticationProvider.h */, + ); + path = Providers; + sourceTree = ""; + }; + 8166FCD41B503914003841A2 /* Anonymous */ = { + isa = PBXGroup; + children = ( + 8166FCD51B503914003841A2 /* PFAnonymousAuthenticationProvider.h */, + 8166FCD61B503914003841A2 /* PFAnonymousAuthenticationProvider.m */, + 8166FCD71B503914003841A2 /* PFAnonymousUtils_Private.h */, + ); + path = Anonymous; + sourceTree = ""; + }; + 8166FCE51B504083003841A2 /* Manager */ = { + isa = PBXGroup; + children = ( + 8166FCE61B504083003841A2 /* PFPushManager.h */, + 8166FCE71B504083003841A2 /* PFPushManager.m */, + ); + path = Manager; + sourceTree = ""; + }; + 818D049819A3B82D00BEE20F /* ThreadSafety */ = { + isa = PBXGroup; + children = ( + 818D049919A3B84500BEE20F /* PFThreadsafety.h */, + 818D049A19A3B84500BEE20F /* PFThreadsafety.m */, + ); + path = ThreadSafety; + sourceTree = ""; + }; + 818D049D19A3B8FD00BEE20F /* HTTPRequest */ = { + isa = PBXGroup; + children = ( + 815EE93E19FA56D90076FE5D /* RequestConstructor */, + 81BBE12C19FFCB1300622646 /* URLConstructor */, + ); + path = HTTPRequest; + sourceTree = ""; + }; + 818D6F111B3C8D1900F94C82 /* LocalIdStore */ = { + isa = PBXGroup; + children = ( + 818D6F121B3C8D1900F94C82 /* PFObjectLocalIdStore.h */, + 818D6F131B3C8D1900F94C82 /* PFObjectLocalIdStore.m */, + ); + path = LocalIdStore; + sourceTree = ""; + }; + 818D6F1D1B3DCB4100F94C82 /* EstimatedData */ = { + isa = PBXGroup; + children = ( + 818D6F1E1B3DCB5A00F94C82 /* PFObjectEstimatedData.h */, + 818D6F1F1B3DCB5A00F94C82 /* PFObjectEstimatedData.m */, + ); + path = EstimatedData; + sourceTree = ""; + }; + 8196D5501B0A9FBF000465A1 /* Analytics */ = { + isa = PBXGroup; + children = ( + 8166FC571B503741003841A2 /* PFAnalytics_Private.h */, + 8196D5581B0AB64B000465A1 /* Controller */, + 8196D5571B0AB641000465A1 /* Utilities */, + ); + path = Analytics; + sourceTree = ""; + }; + 8196D5571B0AB641000465A1 /* Utilities */ = { + isa = PBXGroup; + children = ( + 8196D55F1B0AB661000465A1 /* PFAnalyticsUtilities.h */, + 8196D5601B0AB661000465A1 /* PFAnalyticsUtilities.m */, + ); + path = Utilities; + sourceTree = ""; + }; + 8196D5581B0AB64B000465A1 /* Controller */ = { + isa = PBXGroup; + children = ( + 8196D5591B0AB64B000465A1 /* PFAnalyticsController.h */, + 8196D55A1B0AB64B000465A1 /* PFAnalyticsController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 81A016241B59E19D00B0C7ED /* ExtensionDataSharing */ = { + isa = PBXGroup; + children = ( + 81A016251B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.h */, + 81A016261B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m */, + ); + path = ExtensionDataSharing; + sourceTree = ""; + }; + 81A207C31AEB0AA0008A5F1A /* Query */ = { + isa = PBXGroup; + children = ( + 8166FC961B50381B003841A2 /* PFQueryPrivate.h */, + 812B7AB51AF2FA3F00D15FF5 /* Controller */, + 81C7F4A61AF42BD9007B5418 /* State */, + 81C7F4881AF41100007B5418 /* Utilities */, + ); + path = Query; + sourceTree = ""; + }; + 81A2458A1B1E99C6006A6953 /* FieldOperation */ = { + isa = PBXGroup; + children = ( + 81A2458B1B1E99C6006A6953 /* PFFieldOperation.h */, + 81A2458C1B1E99C6006A6953 /* PFFieldOperation.m */, + 81A245911B1E99EA006A6953 /* PFFieldOperationDecoder.h */, + 81A245921B1E99EA006A6953 /* PFFieldOperationDecoder.m */, + ); + path = FieldOperation; + sourceTree = ""; + }; + 81A715A11B423A3600A504FC /* Utilities */ = { + isa = PBXGroup; + children = ( + 81A715A21B423A4100A504FC /* PFObjectUtilities.h */, + 81A715A31B423A4100A504FC /* PFObjectUtilities.m */, + ); + path = Utilities; + sourceTree = ""; + }; + 81ABC0FB1B5427DD00BA9009 /* Controller */ = { + isa = PBXGroup; + children = ( + 81ABC0FC1B5427EC00BA9009 /* PFUserController.h */, + 81ABC0FD1B5427EC00BA9009 /* PFUserController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 81AFA6781B0ECF9B000763C0 /* OCMock */ = { + isa = PBXGroup; + children = ( + 81AFA6751B0ECF90000763C0 /* OCMock.framework */, + 81AFA6701B0ECD4E000763C0 /* libOCMock.a */, + ); + name = OCMock; + sourceTree = ""; + }; + 81BBE12C19FFCB1300622646 /* URLConstructor */ = { + isa = PBXGroup; + children = ( + 81BBE12D19FFCB3700622646 /* PFURLConstructor.h */, + 81BBE12E19FFCB3700622646 /* PFURLConstructor.m */, + ); + name = URLConstructor; + sourceTree = ""; + }; + 81BCB4BC1B744626006659CB /* TaskDelegate */ = { + isa = PBXGroup; + children = ( + 81BCB4BF1B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h */, + 81BCB4BD1B744626006659CB /* PFURLSessionDataTaskDelegate.h */, + 81BCB4BE1B744626006659CB /* PFURLSessionDataTaskDelegate.m */, + 81BCB4C01B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h */, + 81BCB4C11B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m */, + 810749AC1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h */, + 810749AD1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m */, + 81BCB4C21B744626006659CB /* PFURLSessionUploadTaskDelegate.h */, + 81BCB4C31B744626006659CB /* PFURLSessionUploadTaskDelegate.m */, + ); + path = TaskDelegate; + sourceTree = ""; + }; + 81BF4AB21B0BF2FE00A3D75B /* Config */ = { + isa = PBXGroup; + children = ( + 8166FC5A1B50374B003841A2 /* PFConfig_Private.h */, + 81BF4AB31B0BF3BB00A3D75B /* Controller */, + ); + path = Config; + sourceTree = ""; + }; + 81BF4AB31B0BF3BB00A3D75B /* Controller */ = { + isa = PBXGroup; + children = ( + 81BF4AB41B0BF3E500A3D75B /* PFConfigController.h */, + 81BF4AB51B0BF3E500A3D75B /* PFConfigController.m */, + 81BF4ABA1B0BF64B00A3D75B /* PFCurrentConfigController.h */, + 81BF4ABB1B0BF64B00A3D75B /* PFCurrentConfigController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 81C6BDEB1B4DB10100553A83 /* Constants */ = { + isa = PBXGroup; + children = ( + 81C6BDEC1B4DB16500553A83 /* PFInstallationConstants.h */, + 81C6BDED1B4DB16500553A83 /* PFInstallationConstants.m */, + ); + path = Constants; + sourceTree = ""; + }; + 81C6BDF21B4DD31500553A83 /* CurrentController */ = { + isa = PBXGroup; + children = ( + 81C6BDF31B4DD32700553A83 /* PFCurrentObjectControlling.h */, + ); + path = CurrentController; + sourceTree = ""; + }; + 81C76EE61B4B200D0031C2FD /* Constants */ = { + isa = PBXGroup; + children = ( + 81C76EE71B4B201E0031C2FD /* PFObjectConstants.h */, + 81C76EEA1B4B218C0031C2FD /* PFObjectConstants.m */, + ); + path = Constants; + sourceTree = ""; + }; + 81C7F4881AF41100007B5418 /* Utilities */ = { + isa = PBXGroup; + children = ( + 81C7F4891AF4110B007B5418 /* PFQueryUtilities.h */, + 81C7F48A1AF4110B007B5418 /* PFQueryUtilities.m */, + ); + path = Utilities; + sourceTree = ""; + }; + 81C7F48F1AF4215B007B5418 /* File */ = { + isa = PBXGroup; + children = ( + 8166FC7B1B503787003841A2 /* PFFile_Private.h */, + 81EB595B1AF46429001EA1FC /* Controller */, + 81C7F4961AF42187007B5418 /* State */, + ); + path = File; + sourceTree = ""; + }; + 81C7F4961AF42187007B5418 /* State */ = { + isa = PBXGroup; + children = ( + 81C7F49D1AF421FF007B5418 /* PFFileState_Private.h */, + 81C7F4971AF42187007B5418 /* PFFileState.h */, + 81C7F4981AF42187007B5418 /* PFFileState.m */, + 81C7F4A01AF4220A007B5418 /* PFMutableFileState.h */, + 81C7F4A11AF4220A007B5418 /* PFMutableFileState.m */, + ); + path = State; + sourceTree = ""; + }; + 81C7F4A61AF42BD9007B5418 /* State */ = { + isa = PBXGroup; + children = ( + 81C7F4A71AF42BD9007B5418 /* PFMutableQueryState.h */, + 81C7F4A81AF42BD9007B5418 /* PFMutableQueryState.m */, + 81C7F4A91AF42BD9007B5418 /* PFQueryState.h */, + 81C7F4AA1AF42BD9007B5418 /* PFQueryState.m */, + 81C7F4AB1AF42BD9007B5418 /* PFQueryState_Private.h */, + ); + path = State; + sourceTree = ""; + }; + 81CB7F6A1B166FC700DC601D /* Object */ = { + isa = PBXGroup; + children = ( + 8166FC5D1B503755003841A2 /* PFObjectPrivate.h */, + 811214701B3E1CDD0052741B /* BatchController */, + 812B62F61B5F303C009CEAA9 /* Coder */, + 81C76EE61B4B200D0031C2FD /* Constants */, + 8166FC671B50376D003841A2 /* Controller */, + 81C6BDF21B4DD31500553A83 /* CurrentController */, + 818D6F1D1B3DCB4100F94C82 /* EstimatedData */, + 8124C8871B276B8800758E00 /* FilePersistence */, + 818D6F111B3C8D1900F94C82 /* LocalIdStore */, + 8166FC601B50375D003841A2 /* OperationSet */, + 8124C8701B26B9E700758E00 /* PinningStore */, + 81CB7F6C1B166FD700DC601D /* State */, + F5C42CCA1B34C76D00C720D8 /* Subclassing */, + 81A715A11B423A3600A504FC /* Utilities */, + ); + path = Object; + sourceTree = ""; + }; + 81CB7F6C1B166FD700DC601D /* State */ = { + isa = PBXGroup; + children = ( + 81CB7F6D1B166FE500DC601D /* PFObjectState.h */, + 81CB7F791B16710D00DC601D /* PFObjectState_Private.h */, + 81CB7F6E1B166FE500DC601D /* PFObjectState.m */, + 81CB7F731B166FF500DC601D /* PFMutableObjectState.h */, + 81CB7F741B166FF500DC601D /* PFMutableObjectState.m */, + ); + path = State; + sourceTree = ""; + }; + 81CB7F891B17957F00DC601D /* Push */ = { + isa = PBXGroup; + children = ( + 8166FC931B503809003841A2 /* PFPushPrivate.h */, + 8124C8821B27588800758E00 /* ChannelsController */, + 81CB7F8A1B17957F00DC601D /* Controller */, + 8166FCE51B504083003841A2 /* Manager */, + 81CB7F8B1B17957F00DC601D /* State */, + F50C66301B33A6CE001941A6 /* Utilites */, + ); + path = Push; + sourceTree = ""; + }; + 81CB7F8A1B17957F00DC601D /* Controller */ = { + isa = PBXGroup; + children = ( + 81CB7F9E1B1800E400DC601D /* PFPushController.h */, + 81CB7F9F1B1800E400DC601D /* PFPushController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 81CB7F8B1B17957F00DC601D /* State */ = { + isa = PBXGroup; + children = ( + 81CB7F8C1B1795C000DC601D /* PFPushState.h */, + 81CB7F981B17970400DC601D /* PFPushState_Private.h */, + 81CB7F8D1B1795C000DC601D /* PFPushState.m */, + 81CB7F921B1795CF00DC601D /* PFMutablePushState.h */, + 81CB7F931B1795CF00DC601D /* PFMutablePushState.m */, + ); + path = State; + sourceTree = ""; + }; + 81CD664F1B4DA5A70042FC0B /* Installation */ = { + isa = PBXGroup; + children = ( + 8166FC821B503794003841A2 /* PFInstallationPrivate.h */, + 81C6BDEB1B4DB10100553A83 /* Constants */, + 81CD66501B4DA5A70042FC0B /* Controller */, + 81CD66511B4DA5A70042FC0B /* CurrentInstallationController */, + 8166FC7E1B503794003841A2 /* InstallationIdentifierStore */, + ); + path = Installation; + sourceTree = ""; + }; + 81CD66501B4DA5A70042FC0B /* Controller */ = { + isa = PBXGroup; + children = ( + 81CD66581B4DA5BA0042FC0B /* PFInstallationController.h */, + 81CD66591B4DA5BA0042FC0B /* PFInstallationController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 81CD66511B4DA5A70042FC0B /* CurrentInstallationController */ = { + isa = PBXGroup; + children = ( + 81CD66521B4DA5A70042FC0B /* PFCurrentInstallationController.h */, + 81CD66531B4DA5A70042FC0B /* PFCurrentInstallationController.m */, + ); + path = CurrentInstallationController; + sourceTree = ""; + }; + 81D843C61B012FB0007CEBCB /* CloudCode */ = { + isa = PBXGroup; + children = ( + 81D843C71B012FBA007CEBCB /* PFCloudCodeController.h */, + 81D843C81B012FBA007CEBCB /* PFCloudCodeController.m */, + ); + path = CloudCode; + sourceTree = ""; + }; + 81E033551B573F3E00B25168 /* NetworkMocking */ = { + isa = PBXGroup; + children = ( + 81E033561B573F3E00B25168 /* PFMockURLProtocol.h */, + 81E033571B573F3E00B25168 /* PFMockURLProtocol.m */, + 81E033581B573F3E00B25168 /* PFMockURLResponse.h */, + 81E033591B573F3E00B25168 /* PFMockURLResponse.m */, + ); + path = NetworkMocking; + sourceTree = ""; + }; + 81E033631B573FC500B25168 /* StoreKitMocking */ = { + isa = PBXGroup; + children = ( + 81E033641B573FC500B25168 /* PFTestSKPaymentQueue.h */, + 81E033651B573FC500B25168 /* PFTestSKPaymentQueue.m */, + 81E033661B573FC500B25168 /* PFTestSKPaymentTransaction.h */, + 81E033671B573FC500B25168 /* PFTestSKPaymentTransaction.m */, + 81E033681B573FC500B25168 /* PFTestSKProduct.h */, + 81E033691B573FC500B25168 /* PFTestSKProduct.m */, + 81E0336A1B573FC500B25168 /* PFTestSKProductsRequest.h */, + 81E0336B1B573FC500B25168 /* PFTestSKProductsRequest.m */, + 81E0336C1B573FC500B25168 /* PFTestSKProductsResponse.h */, + 81E0336D1B573FC500B25168 /* PFTestSKProductsResponse.m */, + ); + path = StoreKitMocking; + sourceTree = ""; + }; + 81E0337B1B57441F00B25168 /* LocationManager */ = { + isa = PBXGroup; + children = ( + 81E0337C1B57441F00B25168 /* CLLocationManager+TestAdditions.h */, + 81E0337D1B57441F00B25168 /* CLLocationManager+TestAdditions.m */, + ); + path = LocationManager; + sourceTree = ""; + }; + 81E7A2181B602545006CB680 /* Coder */ = { + isa = PBXGroup; + children = ( + 81E7A2191B602545006CB680 /* File */, + ); + path = Coder; + sourceTree = ""; + }; + 81E7A2191B602545006CB680 /* File */ = { + isa = PBXGroup; + children = ( + 81E7A21A1B602560006CB680 /* PFUserFileCodingLogic.h */, + 81E7A21B1B602560006CB680 /* PFUserFileCodingLogic.m */, + ); + path = File; + sourceTree = ""; + }; + 81EB595B1AF46429001EA1FC /* Controller */ = { + isa = PBXGroup; + children = ( + 81EB595C1AF46434001EA1FC /* PFFileController.h */, + 81EB595D1AF46434001EA1FC /* PFFileController.m */, + ); + path = Controller; + sourceTree = ""; + }; + 81EDD4BC1B59A6D8002F69C0 /* CommandRunner */ = { + isa = PBXGroup; + children = ( + 81EDD4D11B59A6EC002F69C0 /* PFCommandRunning.h */, + 818D586E1B5DA43800813989 /* PFCommandRunning.m */, + 818D58711B5DAAFE00813989 /* PFCommandRunningConstants.h */, + 818D58721B5DAAFE00813989 /* PFCommandRunningConstants.m */, + 812B02A51B5DE562003846EE /* URLRequestConstructor */, + 81EDD4C11B59A6D8002F69C0 /* URLSession */, + ); + name = CommandRunner; + path = Commands/CommandRunner; + sourceTree = ""; + }; + 81EDD4C11B59A6D8002F69C0 /* URLSession */ = { + isa = PBXGroup; + children = ( + F55C740B1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h */, + 818D58681B5D9F4B00813989 /* PFURLSessionCommandRunner.h */, + 818D58691B5D9F4B00813989 /* PFURLSessionCommandRunner.m */, + 812B02911B5DE3EE003846EE /* Session */, + ); + path = URLSession; + sourceTree = ""; + }; + 81EEE1AC1B446D600087AC4D /* User */ = { + isa = PBXGroup; + children = ( + 8166FC9C1B503847003841A2 /* PFUserPrivate.h */, + 8166FCCF1B503914003841A2 /* AuthenticationProviders */, + 81E7A2181B602545006CB680 /* Coder */, + 8166FB981B4F2EFD003841A2 /* Constants */, + 81ABC0FB1B5427DD00BA9009 /* Controller */, + 81EEE1AD1B446D600087AC4D /* CurrentUserController */, + 814BCDEE1B4DF61800007B7F /* State */, + ); + path = User; + sourceTree = ""; + }; + 81EEE1AD1B446D600087AC4D /* CurrentUserController */ = { + isa = PBXGroup; + children = ( + 81EEE1AE1B446D600087AC4D /* PFCurrentUserController.h */, + 81EEE1AF1B446D600087AC4D /* PFCurrentUserController.m */, + ); + path = CurrentUserController; + sourceTree = ""; + }; + 97DE04271631E686007154E8 /* OSX */ = { + isa = PBXGroup; + children = ( + 971AC55C1716405A00A4EB71 /* ParseOSX.h */, + ); + path = OSX; + sourceTree = ""; + }; + F50C66301B33A6CE001941A6 /* Utilites */ = { + isa = PBXGroup; + children = ( + F50C66311B33A708001941A6 /* PFPushUtilities.h */, + F50C66321B33A708001941A6 /* PFPushUtilities.m */, + ); + path = Utilites; + sourceTree = ""; + }; + F51534F21B571E9100C49F56 /* ACL */ = { + isa = PBXGroup; + children = ( + F51534F61B571E9100C49F56 /* PFACLPrivate.h */, + F51535561B57573700C49F56 /* DefaultACLController */, + F51534F71B571E9100C49F56 /* State */, + ); + path = ACL; + sourceTree = ""; + }; + F51534F71B571E9100C49F56 /* State */ = { + isa = PBXGroup; + children = ( + F51534F81B571E9100C49F56 /* PFACLState.h */, + F51534F91B571E9100C49F56 /* PFACLState.m */, + F51534FA1B571E9100C49F56 /* PFACLState_Private.h */, + F51534FB1B571E9100C49F56 /* PFMutableACLState.h */, + F51534FC1B571E9100C49F56 /* PFMutableACLState.m */, + ); + path = State; + sourceTree = ""; + }; + F51535561B57573700C49F56 /* DefaultACLController */ = { + isa = PBXGroup; + children = ( + F51535571B57573700C49F56 /* PFDefaultACLController.h */, + F51535581B57573700C49F56 /* PFDefaultACLController.m */, + ); + path = DefaultACLController; + sourceTree = ""; + }; + F55ABB501B4F39DA00A0ECD5 /* Configurations */ = { + isa = PBXGroup; + children = ( + F55ABB531B4F39DA00A0ECD5 /* Parse-iOS.xcconfig */, + F55ABB541B4F39DA00A0ECD5 /* Parse-OSX.xcconfig */, + F55ABB591B4F39DA00A0ECD5 /* ParseUnitTests-iOS.xcconfig */, + F55ABB5A1B4F39DA00A0ECD5 /* ParseUnitTests-OSX.xcconfig */, + F55ABB511B4F39DA00A0ECD5 /* BoltsSDK-iOS.xcconfig */, + F55ABB521B4F39DA00A0ECD5 /* BoltsSDK-OSX.xcconfig */, + F55ABB5B1B4F39DA00A0ECD5 /* Shared */, + ); + path = Configurations; + sourceTree = ""; + }; + F55ABB5B1B4F39DA00A0ECD5 /* Shared */ = { + isa = PBXGroup; + children = ( + F55ABB5C1B4F39DA00A0ECD5 /* Common.xcconfig */, + F55ABB691B4F39DA00A0ECD5 /* Warnings.xcconfig */, + F55ABB5F1B4F39DA00A0ECD5 /* Platform */, + F55ABB621B4F39DA00A0ECD5 /* Product */, + F55ABB661B4F39DA00A0ECD5 /* Project */, + ); + path = Shared; + sourceTree = ""; + }; + F55ABB5F1B4F39DA00A0ECD5 /* Platform */ = { + isa = PBXGroup; + children = ( + F55ABB601B4F39DA00A0ECD5 /* iOS.xcconfig */, + F55ABB611B4F39DA00A0ECD5 /* OSX.xcconfig */, + ); + path = Platform; + sourceTree = ""; + }; + F55ABB621B4F39DA00A0ECD5 /* Product */ = { + isa = PBXGroup; + children = ( + F55ABB631B4F39DA00A0ECD5 /* Application.xcconfig */, + F55ABB641B4F39DA00A0ECD5 /* Framework.xcconfig */, + F55ABB651B4F39DA00A0ECD5 /* UnitTest.xcconfig */, + ); + path = Product; + sourceTree = ""; + }; + F55ABB661B4F39DA00A0ECD5 /* Project */ = { + isa = PBXGroup; + children = ( + F55ABB671B4F39DA00A0ECD5 /* Debug.xcconfig */, + F55ABB681B4F39DA00A0ECD5 /* Release.xcconfig */, + 814C3AB01B6975DE00E307BB /* Test.xcconfig */, + ); + path = Project; + sourceTree = ""; + }; + F5ADB9C31B6C5028002A819E /* Cache */ = { + isa = PBXGroup; + children = ( + F5ADB9C91B6C5047002A819E /* TestCache.h */, + F5ADB9CA1B6C5047002A819E /* TestCache.m */, + ); + path = Cache; + sourceTree = ""; + }; + F5ADB9C41B6C502F002A819E /* FileManager */ = { + isa = PBXGroup; + children = ( + F5ADB9C51B6C503E002A819E /* TestFileManager.h */, + F5ADB9C61B6C503E002A819E /* TestFileManager.m */, + ); + path = FileManager; + sourceTree = ""; + }; + F5C42CCA1B34C76D00C720D8 /* Subclassing */ = { + isa = PBXGroup; + children = ( + F5C42CD21B34F68C00C720D8 /* PFObjectSubclassingController.h */, + F5C42CD31B34F68C00C720D8 /* PFObjectSubclassingController.m */, + F5C42CD81B38761B00C720D8 /* PFObjectSubclassInfo.h */, + F5C42CD91B38761B00C720D8 /* PFObjectSubclassInfo.m */, + ); + path = Subclassing; + sourceTree = ""; + }; + F5E8DE0F1B290B3700EEA594 /* Relation */ = { + isa = PBXGroup; + children = ( + 810ECA6F1B573853002944D4 /* PFRelationPrivate.h */, + F5E8DE101B290BDE00EEA594 /* State */, + ); + path = Relation; + sourceTree = ""; + }; + F5E8DE101B290BDE00EEA594 /* State */ = { + isa = PBXGroup; + children = ( + F5E8DE231B2912BC00EEA594 /* PFRelationState_Private.h */, + F5E8DE171B290FFF00EEA594 /* PFRelationState.h */, + F5E8DE181B290FFF00EEA594 /* PFRelationState.m */, + F5E8DE1D1B29112000EEA594 /* PFMutableRelationState.h */, + F5E8DE1E1B29112000EEA594 /* PFMutableRelationState.m */, + ); + path = State; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 818AAA6D19D3687900FC1B3C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8124C8731B26B9E700758E00 /* PFPinningObjectStore.h in Headers */, + 810B7D761A0291FF003C0909 /* PFMacros.h in Headers */, + 81BBE1351A0062B800622646 /* PFRESTAnalyticsCommand.h in Headers */, + 81CB7FA01B1800E400DC601D /* PFPushController.h in Headers */, + 815EE93C19FA56D20076FE5D /* PFHTTPURLRequestConstructor.h in Headers */, + F51535591B57573700C49F56 /* PFDefaultACLController.h in Headers */, + 818AAA7019D36B1C00FC1B3C /* PFACL.h in Headers */, + F51534FF1B571E9100C49F56 /* PFACLPrivate.h in Headers */, + 8124C8851B27588800758E00 /* PFPushChannelsController.h in Headers */, + 81A245F21B1FB188006A6953 /* PFDataProvider.h in Headers */, + 818AAA7419D36B1C00FC1B3C /* PFConfig.h in Headers */, + 818AAA7119D36B1C00FC1B3C /* PFAnalytics.h in Headers */, + 818AAA7C19D36B1C00FC1B3C /* PFPush.h in Headers */, + F51D06341B792CF10044539E /* PFSQLiteDatabaseController.h in Headers */, + 81C9CA0619FECF5F00D514C5 /* PFRESTFileCommand.h in Headers */, + 81CB7F7A1B16710D00DC601D /* PFObjectState_Private.h in Headers */, + 81BB6E211B0E7A1A00465C38 /* PFBase64Encoder.h in Headers */, + 818AAA6F19D36B1C00FC1B3C /* Parse.h in Headers */, + 819A4B081A67330200D01241 /* PFHash.h in Headers */, + 91DF24991A0B0FF200CFC7D4 /* PFEventuallyQueue_Private.h in Headers */, + 818AAA7619D36B1C00FC1B3C /* PFFile.h in Headers */, + 816AC9BA1A3F48250031D94C /* PFApplication.h in Headers */, + F5B0B2EC1B449F1D00F3EBC4 /* BFTask+Private.h in Headers */, + F5B0B2ED1B449F1D00F3EBC4 /* PFCategoryLoader.h in Headers */, + F5B0B2EE1B449F1D00F3EBC4 /* PFThreadsafety.h in Headers */, + F5B0B2EF1B449F1D00F3EBC4 /* PFRelationState_Private.h in Headers */, + F5B0B2F01B449F1D00F3EBC4 /* ParseInternal.h in Headers */, + 81CD66541B4DA5A70042FC0B /* PFCurrentInstallationController.h in Headers */, + F5B0B2F11B449F1D00F3EBC4 /* PFCoreDataProvider.h in Headers */, + F5B0B2F21B449F1D00F3EBC4 /* ParseModule.h in Headers */, + F5B0B2F31B449F1D00F3EBC4 /* PFAssert.h in Headers */, + F5B0B2F61B449F1D00F3EBC4 /* PFBlockRetryer.h in Headers */, + 814BCDF11B4DF63600007B7F /* PFUserState.h in Headers */, + F5B0B2F81B449F1D00F3EBC4 /* PFDecoder.h in Headers */, + F5B0B2FA1B449F1D00F3EBC4 /* PFGeoPointPrivate.h in Headers */, + 810749AE1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h in Headers */, + F5B0B2FC1B449F1D00F3EBC4 /* PFInternalUtils.h in Headers */, + F5B0B2FD1B449F1D00F3EBC4 /* PFKeychainStore.h in Headers */, + 818D58731B5DAAFE00813989 /* PFCommandRunningConstants.h in Headers */, + F5B0B2FF1B449F1D00F3EBC4 /* PFMulticastDelegate.h in Headers */, + 81C6BDF41B4DD32700553A83 /* PFCurrentObjectControlling.h in Headers */, + 8166FCCC1B5038B7003841A2 /* PFPaymentTransactionObserver.h in Headers */, + 8166FB9B1B4F2F08003841A2 /* PFUserConstants.h in Headers */, + 8166FC871B503794003841A2 /* PFInstallationIdentifierStore_Private.h in Headers */, + F5B0B30A1B449F1D00F3EBC4 /* PFTaskQueue.h in Headers */, + F5B0B30C1B449F1D00F3EBC4 /* PFLocationManager.h in Headers */, + 8166FCD91B503914003841A2 /* PFUserAuthenticationController.h in Headers */, + 81ABC0FE1B5427EC00BA9009 /* PFUserController.h in Headers */, + 81E7A21C1B602560006CB680 /* PFUserFileCodingLogic.h in Headers */, + F5B0B30D1B449F1D00F3EBC4 /* PFAsyncTaskQueue.h in Headers */, + F5B0B30E1B449F1D00F3EBC4 /* PFBaseState.h in Headers */, + 8166FCCE1B5038B7003841A2 /* PFPaymentTransactionObserver_Private.h in Headers */, + 8166FC6F1B50376D003841A2 /* PFOfflineObjectController.h in Headers */, + 814881591B795CAC008763BF /* PFPropertyInfo_Private.h in Headers */, + F5B0B2DE1B449EEF00F3EBC4 /* PFCommandCache.h in Headers */, + 81CD665A1B4DA5BA0042FC0B /* PFInstallationController.h in Headers */, + F5B0B2DF1B449EEF00F3EBC4 /* PFCommandCache_Private.h in Headers */, + F5B0B2E01B449EEF00F3EBC4 /* PFCommandResult.h in Headers */, + 812B02961B5DE3EE003846EE /* PFURLSession.h in Headers */, + 8166FC731B50376D003841A2 /* PFObjectController.h in Headers */, + F5B0B2EB1B449EEF00F3EBC4 /* PFAlertView.h in Headers */, + 8119C9971A76E28F0085B516 /* PFNetworkCommand.h in Headers */, + 8166FCB01B503886003841A2 /* PFOfflineQueryLogic.h in Headers */, + 81951F161ACB90DA00E142EB /* PFJSONSerialization.h in Headers */, + 81068EBB1ADE462500A34D13 /* Parse_Private.h in Headers */, + 81A2458D1B1E99C6006A6953 /* PFFieldOperation.h in Headers */, + 8166FC5E1B503755003841A2 /* PFObjectPrivate.h in Headers */, + 8166FC831B503794003841A2 /* PFInstallationIdentifierStore.h in Headers */, + 81BCB4CA1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h in Headers */, + 8166FCE31B503914003841A2 /* PFAuthenticationProvider.h in Headers */, + 814BCDF71B4DF66500007B7F /* PFMutableUserState.h in Headers */, + 815EE92319F989380076FE5D /* PFRESTConfigCommand.h in Headers */, + 81C9C9F719FEA89200D514C5 /* PFRESTPushCommand.h in Headers */, + 81E7A2251B6042BD006CB680 /* PFObjectFileCodingLogic.h in Headers */, + 81068EF11AE0845D00A34D13 /* PFEncoder.h in Headers */, + 812B7AB81AF2FA4800D15FF5 /* PFQueryController.h in Headers */, + 81BCB4C81B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h in Headers */, + F5556A181B66F47900410837 /* PFURLSession_Private.h in Headers */, + 815EE8F919F976D50076FE5D /* PFRESTCommand_Private.h in Headers */, + 81CB7F6F1B166FE500DC601D /* PFObjectState.h in Headers */, + 815EE94019FA5A390076FE5D /* PFHTTPRequest.h in Headers */, + 815EE8F519F976D50076FE5D /* PFRESTCommand.h in Headers */, + 818AAA7319D36B1C00FC1B3C /* PFCloud.h in Headers */, + 81A715A41B423A4100A504FC /* PFObjectUtilities.h in Headers */, + 81C76EE81B4B201E0031C2FD /* PFObjectConstants.h in Headers */, + 81CB7F751B166FF500DC601D /* PFMutableObjectState.h in Headers */, + 81C1EE491AE1EF960031C438 /* PFWeakValue.h in Headers */, + 8166FCB41B503886003841A2 /* PFOfflineStore.h in Headers */, + 81329E8E1AE1E8840071EE3E /* PFReachability.h in Headers */, + 81C7F4AC1AF42BD9007B5418 /* PFMutableQueryState.h in Headers */, + F5B0B3131B44A05100F3EBC4 /* PFPaymentTransactionObserver_Private.h in Headers */, + 81CB7F991B17970400DC601D /* PFPushState_Private.h in Headers */, + 81C7F4A21AF4220A007B5418 /* PFMutableFileState.h in Headers */, + 8124C8AC1B27D5D600758E00 /* PFSessionUtilities.h in Headers */, + 818AAA7719D36B1C00FC1B3C /* PFGeoPoint.h in Headers */, + 814B64111A769EF500213055 /* PFLogger.h in Headers */, + 818AAA7519D36B1C00FC1B3C /* PFConstants.h in Headers */, + 8166FCC01B503886003841A2 /* PFSQLiteDatabaseResult.h in Headers */, + 8166FC581B503741003841A2 /* PFAnalytics_Private.h in Headers */, + 81BF4AB61B0BF3E500A3D75B /* PFConfigController.h in Headers */, + F5E8DE191B29100000EEA594 /* PFRelationState.h in Headers */, + 81C7F49E1AF421FF007B5418 /* PFFileState_Private.h in Headers */, + 8143E6631AFC1C7D008C4E06 /* PFCachedQueryController.h in Headers */, + F51535031B571E9100C49F56 /* PFMutableACLState.h in Headers */, + 818AAA8419D36B1C00FC1B3C /* PFNetworkActivityIndicatorManager.h in Headers */, + 8166FC771B50376D003841A2 /* PFObjectController_Private.h in Headers */, + 91DF24921A09BA7600CFC7D4 /* PFEventuallyQueue.h in Headers */, + 81AFE0E71A1FDB7900AB6CB3 /* PFRESTUserCommand.h in Headers */, + 8121457D1AA4A808000B23F5 /* PFRESTSessionCommand.h in Headers */, + 812FC6201B0FF9FA0043C07F /* PFPurchaseController.h in Headers */, + 8166FC631B50375D003841A2 /* PFOperationSet.h in Headers */, + 8166FC791B50376D003841A2 /* PFObjectControlling.h in Headers */, + 81BCB4CE1B744626006659CB /* PFURLSessionUploadTaskDelegate.h in Headers */, + 814881601B795CD4008763BF /* PFMultiProcessFileLock.h in Headers */, + F5C42CDA1B38761B00C720D8 /* PFObjectSubclassInfo.h in Headers */, + 8196D55B1B0AB64B000465A1 /* PFAnalyticsController.h in Headers */, + 813E769A1B7A9BD000FA3294 /* PFErrorUtilities.h in Headers */, + 818AAA7E19D36B1C00FC1B3C /* PFRelation.h in Headers */, + 8166FCE11B503914003841A2 /* PFAnonymousUtils_Private.h in Headers */, + 8166FCB81B503886003841A2 /* PFPin.h in Headers */, + 81493AA41A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h in Headers */, + 8166FCC41B503886003841A2 /* PFSQLiteStatement.h in Headers */, + 818D586A1B5D9F4B00813989 /* PFURLSessionCommandRunner.h in Headers */, + 81146C7E1A785203001F8473 /* PFRESTObjectCommand.h in Headers */, + 81EDD4D21B59A6EC002F69C0 /* PFCommandRunning.h in Headers */, + 815EE91D19F987910076FE5D /* PFRESTCloudCommand.h in Headers */, + 818AAA7A19D36B1C00FC1B3C /* PFProduct.h in Headers */, + 818AAA7D19D36B1C00FC1B3C /* PFQuery.h in Headers */, + 81C7F48B1AF4110B007B5418 /* PFQueryUtilities.h in Headers */, + 8166FC971B50381B003841A2 /* PFQueryPrivate.h in Headers */, + 81CB7F8E1B1795C000DC601D /* PFPushState.h in Headers */, + 812B02A81B5DE562003846EE /* PFCommandURLRequestConstructor.h in Headers */, + 815EE94619FAD12F0076FE5D /* PFRESTQueryCommand.h in Headers */, + F51535021B571E9100C49F56 /* PFACLState_Private.h in Headers */, + 818AAA7919D36B1C00FC1B3C /* PFObject.h in Headers */, + F50C66331B33A708001941A6 /* PFPushUtilities.h in Headers */, + 8166FCBC1B503886003841A2 /* PFSQLiteDatabase.h in Headers */, + 8166FC911B5037F5003841A2 /* PFProductsRequestHandler.h in Headers */, + 8166FC901B5037F5003841A2 /* PFProduct+Private.h in Headers */, + 814881451B795C63008763BF /* PFKeyValueCache.h in Headers */, + 8166FC941B503809003841A2 /* PFPushPrivate.h in Headers */, + 8124C89F1B27BF0900758E00 /* PFSessionController.h in Headers */, + 818AAA7F19D36B1C00FC1B3C /* PFRole.h in Headers */, + 81CB7F941B1795CF00DC601D /* PFMutablePushState.h in Headers */, + 8166FCE81B504083003841A2 /* PFPushManager.h in Headers */, + 812145771AA4A4C1000B23F5 /* PFSession.h in Headers */, + 91115EF91A097AF30092D1C9 /* PFEventuallyPin.h in Headers */, + 91DF24961A09BAF100CFC7D4 /* PFPinningEventuallyQueue.h in Headers */, + 8196D58D1B0BD23B000465A1 /* PFCoreManager.h in Headers */, + 812714881AE6F1270076AE8D /* ParseManager.h in Headers */, + 8166FC7C1B503787003841A2 /* PFFile_Private.h in Headers */, + 81EB595E1AF46434001EA1FC /* PFFileController.h in Headers */, + 814881491B795C63008763BF /* PFKeyValueCache_Private.h in Headers */, + 814B64151A769EF500213055 /* PFLogging.h in Headers */, + F55C740C1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h in Headers */, + 8166FC5B1B50374B003841A2 /* PFConfig_Private.h in Headers */, + 81BBE12F19FFCB3700622646 /* PFURLConstructor.h in Headers */, + 810ECA701B573853002944D4 /* PFRelationPrivate.h in Headers */, + F5E8DE1F1B29112000EEA594 /* PFMutableRelationState.h in Headers */, + 8166FC9A1B503830003841A2 /* PFSession_Private.h in Headers */, + 818D6F201B3DCB5A00F94C82 /* PFObjectEstimatedData.h in Headers */, + 8124C88A1B276B8800758E00 /* PFObjectFilePersistenceController.h in Headers */, + 81C6BDEE1B4DB16500553A83 /* PFInstallationConstants.h in Headers */, + 8166FC9D1B503847003841A2 /* PFUserPrivate.h in Headers */, + 81C7F4991AF42187007B5418 /* PFFileState.h in Headers */, + 818AAA7219D36B1C00FC1B3C /* PFAnonymousUtils.h in Headers */, + 818D6F141B3C8D1900F94C82 /* PFObjectLocalIdStore.h in Headers */, + 814881551B795CAC008763BF /* PFPropertyInfo_Runtime.h in Headers */, + 8166FC891B503794003841A2 /* PFInstallationPrivate.h in Headers */, + 81BCB4C41B744626006659CB /* PFURLSessionDataTaskDelegate.h in Headers */, + 815619001A1F79AC0076504A /* PFDateFormatter.h in Headers */, + 81D843C91B012FBA007CEBCB /* PFCloudCodeController.h in Headers */, + 814881641B795CD4008763BF /* PFMultiProcessFileLockController.h in Headers */, + 81EEE1B01B446D600087AC4D /* PFCurrentUserController.h in Headers */, + 815960A11ABCA3B30069EBCC /* PFFileManager.h in Headers */, + 81A245931B1E99EA006A6953 /* PFFieldOperationDecoder.h in Headers */, + 814881511B795CAC008763BF /* PFPropertyInfo.h in Headers */, + 818AAA8319D36B1C00FC1B3C /* PFSubclassing.h in Headers */, + 811214731B3E1CF10052741B /* PFObjectBatchController.h in Headers */, + 8196D5611B0AB661000465A1 /* PFAnalyticsUtilities.h in Headers */, + 818AAA8219D36B1C00FC1B3C /* PFObject+Subclass.h in Headers */, + 814BCDFC1B4DF7E800007B7F /* PFUserState_Private.h in Headers */, + 8166FCDD1B503914003841A2 /* PFAnonymousAuthenticationProvider.h in Headers */, + 81C7F4B01AF42BD9007B5418 /* PFQueryState.h in Headers */, + 812B63001B5F30D3009CEAA9 /* PFObjectFileCoder.h in Headers */, + 818AAA7819D36B1C00FC1B3C /* PFInstallation.h in Headers */, + 818AAA7B19D36B1C00FC1B3C /* PFPurchase.h in Headers */, + 81443B331A27838500F3FD17 /* PFDevice.h in Headers */, + 81C7F4B41AF42BD9007B5418 /* PFQueryState_Private.h in Headers */, + F5C42CD41B34F68C00C720D8 /* PFObjectSubclassingController.h in Headers */, + 8143E65D1AFC1BA5008C4E06 /* PFOfflineQueryController.h in Headers */, + 818AAA8119D36B1C00FC1B3C /* PFUser.h in Headers */, + F51D06371B793A110044539E /* PFSQLiteDatabase_Private.h in Headers */, + F51535001B571E9100C49F56 /* PFACLState.h in Headers */, + 81BF4ABC1B0BF64B00A3D75B /* PFCurrentConfigController.h in Headers */, + 816F97111A93FAC400CADE60 /* PFNullability.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97010FAA1630B18F00AB761E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 81F0E89919E6F83E00812A88 /* PFRelation.h in Headers */, + 81BCB4C51B744626006659CB /* PFURLSessionDataTaskDelegate.h in Headers */, + 81C9CA0719FECF5F00D514C5 /* PFRESTFileCommand.h in Headers */, + 8166FCB11B503886003841A2 /* PFOfflineQueryLogic.h in Headers */, + 812714891AE6F1270076AE8D /* ParseManager.h in Headers */, + 8166FC9E1B503847003841A2 /* PFUserPrivate.h in Headers */, + 8124C88B1B276B8800758E00 /* PFObjectFilePersistenceController.h in Headers */, + 81F0E88E19E6F7D600812A88 /* Parse.h in Headers */, + 81F0E89519E6F83E00812A88 /* PFFile.h in Headers */, + 814B64161A769EF500213055 /* PFLogging.h in Headers */, + 81E7A21D1B602560006CB680 /* PFUserFileCodingLogic.h in Headers */, + 81BBE1361A0062B800622646 /* PFRESTAnalyticsCommand.h in Headers */, + 8143E6641AFC1C7D008C4E06 /* PFCachedQueryController.h in Headers */, + 81ABC0FF1B5427EC00BA9009 /* PFUserController.h in Headers */, + 815EE8FA19F976D50076FE5D /* PFRESTCommand_Private.h in Headers */, + 810749AF1B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.h in Headers */, + F51D06381B793A110044539E /* PFSQLiteDatabase_Private.h in Headers */, + 8121457E1AA4A808000B23F5 /* PFRESTSessionCommand.h in Headers */, + 81C7F4AD1AF42BD9007B5418 /* PFMutableQueryState.h in Headers */, + 81EDD4D31B59A6EC002F69C0 /* PFCommandRunning.h in Headers */, + 8196D55C1B0AB64B000465A1 /* PFAnalyticsController.h in Headers */, + 81068EF21AE0845D00A34D13 /* PFEncoder.h in Headers */, + 81BB6E221B0E7A1A00465C38 /* PFBase64Encoder.h in Headers */, + F5B0B3191B44A33100F3EBC4 /* PFCommandCache.h in Headers */, + F5B0B31A1B44A33100F3EBC4 /* PFCommandCache_Private.h in Headers */, + F5B0B31B1B44A33100F3EBC4 /* PFCommandResult.h in Headers */, + F5B0B31C1B44A33100F3EBC4 /* PFEventuallyPin.h in Headers */, + F5B0B31D1B44A33100F3EBC4 /* PFEventuallyQueue.h in Headers */, + 8166FCE21B503914003841A2 /* PFAnonymousUtils_Private.h in Headers */, + F5B0B31E1B44A33100F3EBC4 /* PFEventuallyQueue_Private.h in Headers */, + F5B0B31F1B44A33100F3EBC4 /* PFPinningEventuallyQueue.h in Headers */, + 8166FC881B503794003841A2 /* PFInstallationIdentifierStore_Private.h in Headers */, + 814881651B795CD4008763BF /* PFMultiProcessFileLockController.h in Headers */, + F5B0B3221B44A33100F3EBC4 /* ParseInternal.h in Headers */, + 81CD66551B4DA5A70042FC0B /* PFCurrentInstallationController.h in Headers */, + F5B0B3231B44A33100F3EBC4 /* ParseModule.h in Headers */, + F5B0B3251B44A33100F3EBC4 /* PFApplication.h in Headers */, + F5B0B3261B44A33100F3EBC4 /* PFAssert.h in Headers */, + F5B0B3271B44A33100F3EBC4 /* PFAsyncTaskQueue.h in Headers */, + F5B0B3281B44A33100F3EBC4 /* PFBaseState.h in Headers */, + F51535091B57240900C49F56 /* PFMutableACLState.h in Headers */, + F5B0B3291B44A33100F3EBC4 /* PFBlockRetryer.h in Headers */, + F5B0B32B1B44A33100F3EBC4 /* PFCoreDataProvider.h in Headers */, + 814881611B795CD4008763BF /* PFMultiProcessFileLock.h in Headers */, + F5B0B32C1B44A33100F3EBC4 /* PFDecoder.h in Headers */, + F5B0B32E1B44A33100F3EBC4 /* PFGeoPointPrivate.h in Headers */, + F5B0B3301B44A33100F3EBC4 /* PFInternalUtils.h in Headers */, + 8166FC951B503809003841A2 /* PFPushPrivate.h in Headers */, + F5B0B3331B44A33100F3EBC4 /* PFLocationManager.h in Headers */, + F5B0B3341B44A33100F3EBC4 /* PFMulticastDelegate.h in Headers */, + 8166FCBD1B503886003841A2 /* PFSQLiteDatabase.h in Headers */, + 8166FC841B503794003841A2 /* PFInstallationIdentifierStore.h in Headers */, + F5B0B3431B44A33200F3EBC4 /* PFTaskQueue.h in Headers */, + 8166FC5C1B50374B003841A2 /* PFConfig_Private.h in Headers */, + 8166FC5F1B503755003841A2 /* PFObjectPrivate.h in Headers */, + 814BCDFD1B4DF7E800007B7F /* PFUserState_Private.h in Headers */, + F5B0B3451B44A33200F3EBC4 /* PFWeakValue.h in Headers */, + F5B0B3461B44A33200F3EBC4 /* PFPushState.h in Headers */, + F5B0B3471B44A33200F3EBC4 /* PFPushState_Private.h in Headers */, + 812B63011B5F30D3009CEAA9 /* PFObjectFileCoder.h in Headers */, + F5B0B3481B44A33200F3EBC4 /* PFMutablePushState.h in Headers */, + F5B0B3491B44A33200F3EBC4 /* PFPushController.h in Headers */, + F5B0B34A1B44A33200F3EBC4 /* PFPushChannelsController.h in Headers */, + F5B0B34B1B44A33200F3EBC4 /* PFPushUtilities.h in Headers */, + F5B0B34C1B44A33200F3EBC4 /* PFRelationState_Private.h in Headers */, + F5E8DE201B29112000EEA594 /* PFMutableRelationState.h in Headers */, + 81BCB4CF1B744626006659CB /* PFURLSessionUploadTaskDelegate.h in Headers */, + 81EEE1B11B446D600087AC4D /* PFCurrentUserController.h in Headers */, + 819A4B091A67330200D01241 /* PFHash.h in Headers */, + 81EBF3401B33E7B100991947 /* PFPush.h in Headers */, + 81C7F4B11AF42BD9007B5418 /* PFQueryState.h in Headers */, + 818D586B1B5D9F4B00813989 /* PFURLSessionCommandRunner.h in Headers */, + 81C6BDF51B4DD32700553A83 /* PFCurrentObjectControlling.h in Headers */, + 81EBF33F1B33E7A800991947 /* PFInstallation.h in Headers */, + 81A245941B1E99EA006A6953 /* PFFieldOperationDecoder.h in Headers */, + 8103FA3C198FC190000BAE3F /* PFCategoryLoader.h in Headers */, + 81D0EE9A19B0A2060000AE75 /* PFKeychainStore.h in Headers */, + 81BF4AB71B0BF3E500A3D75B /* PFConfigController.h in Headers */, + F515355A1B57573700C49F56 /* PFDefaultACLController.h in Headers */, + 81F0E88F19E6F7DB00812A88 /* ParseOSX.h in Headers */, + 8119C9981A76E28F0085B516 /* PFNetworkCommand.h in Headers */, + 8166FCDE1B503914003841A2 /* PFAnonymousAuthenticationProvider.h in Headers */, + 81CD665B1B4DA5BA0042FC0B /* PFInstallationController.h in Headers */, + 815EE94119FA5A390076FE5D /* PFHTTPRequest.h in Headers */, + 81A245F31B1FB188006A6953 /* PFDataProvider.h in Headers */, + 81329E8F1AE1E8840071EE3E /* PFReachability.h in Headers */, + 8166FC8A1B503794003841A2 /* PFInstallationPrivate.h in Headers */, + 8166FCB91B503886003841A2 /* PFPin.h in Headers */, + 815EE94719FAD12F0076FE5D /* PFRESTQueryCommand.h in Headers */, + 81F0E89A19E6F83E00812A88 /* PFRole.h in Headers */, + F5E8DE1A1B29100000EEA594 /* PFRelationState.h in Headers */, + 81C7F4B51AF42BD9007B5418 /* PFQueryState_Private.h in Headers */, + F5C42CD51B34F68C00C720D8 /* PFObjectSubclassingController.h in Headers */, + F51535081B57240900C49F56 /* PFACLState_Private.h in Headers */, + 81E7A2261B6042BD006CB680 /* PFObjectFileCodingLogic.h in Headers */, + 812145781AA4A4C1000B23F5 /* PFSession.h in Headers */, + 81443B341A27838500F3FD17 /* PFDevice.h in Headers */, + 81C7F4A31AF4220A007B5418 /* PFMutableFileState.h in Headers */, + 810B7D771A0291FF003C0909 /* PFMacros.h in Headers */, + 81C6BDEF1B4DB16500553A83 /* PFInstallationConstants.h in Headers */, + 8166FCE41B503914003841A2 /* PFAuthenticationProvider.h in Headers */, + F51535061B57240900C49F56 /* PFACLState.h in Headers */, + 8166FC9B1B503830003841A2 /* PFSession_Private.h in Headers */, + 81F0E89619E6F83E00812A88 /* PFGeoPoint.h in Headers */, + 81F0E89B19E6F83E00812A88 /* PFUser.h in Headers */, + 8166FC641B50375D003841A2 /* PFOperationSet.h in Headers */, + 81F0E89219E6F83E00812A88 /* PFAnonymousUtils.h in Headers */, + 8166FC7D1B503787003841A2 /* PFFile_Private.h in Headers */, + 814BCDF81B4DF66500007B7F /* PFMutableUserState.h in Headers */, + F590194B1B7992E700F763EF /* PFSQLiteDatabaseController.h in Headers */, + 81951F171ACB90DA00E142EB /* PFJSONSerialization.h in Headers */, + 818D6F211B3DCB5A00F94C82 /* PFObjectEstimatedData.h in Headers */, + 813E769B1B7A9BD000FA3294 /* PFErrorUtilities.h in Headers */, + 81F0E89819E6F83E00812A88 /* PFQuery.h in Headers */, + 815619011A1F79AC0076504A /* PFDateFormatter.h in Headers */, + 811214741B3E1CF10052741B /* PFObjectBatchController.h in Headers */, + 8148814A1B795C63008763BF /* PFKeyValueCache_Private.h in Headers */, + 81D843CA1B012FBA007CEBCB /* PFCloudCodeController.h in Headers */, + 81068EBC1ADE462500A34D13 /* Parse_Private.h in Headers */, + 812B02971B5DE3EE003846EE /* PFURLSession.h in Headers */, + 8103FA38198FC190000BAE3F /* BFTask+Private.h in Headers */, + F55C740D1B631557000EDAFA /* PFURLSessionCommandRunner_Private.h in Headers */, + 8166FCB51B503886003841A2 /* PFOfflineStore.h in Headers */, + 8166FCE91B504083003841A2 /* PFPushManager.h in Headers */, + 8166FC701B50376D003841A2 /* PFOfflineObjectController.h in Headers */, + 81BF4ABD1B0BF64B00A3D75B /* PFCurrentConfigController.h in Headers */, + 818D58741B5DAAFE00813989 /* PFCommandRunningConstants.h in Headers */, + 810ECA711B573853002944D4 /* PFRelationPrivate.h in Headers */, + 81F0E89019E6F83E00812A88 /* PFACL.h in Headers */, + 814881461B795C63008763BF /* PFKeyValueCache.h in Headers */, + 81EB595F1AF46434001EA1FC /* PFFileController.h in Headers */, + 815EE94219FA88FB0076FE5D /* PFHTTPURLRequestConstructor.h in Headers */, + 81F0E89419E6F83E00812A88 /* PFConstants.h in Headers */, + 8143E65E1AFC1BA5008C4E06 /* PFOfflineQueryController.h in Headers */, + 81CB7F7B1B16710D00DC601D /* PFObjectState_Private.h in Headers */, + 8166FCDA1B503914003841A2 /* PFUserAuthenticationController.h in Headers */, + 81BBE13019FFCB3700622646 /* PFURLConstructor.h in Headers */, + 812B02A91B5DE562003846EE /* PFCommandURLRequestConstructor.h in Headers */, + 8166FC741B50376D003841A2 /* PFObjectController.h in Headers */, + F5C42CDB1B38761B00C720D8 /* PFObjectSubclassInfo.h in Headers */, + 815EE91E19F987910076FE5D /* PFRESTCloudCommand.h in Headers */, + 816F97121A93FAC400CADE60 /* PFNullability.h in Headers */, + 81146C7F1A785203001F8473 /* PFRESTObjectCommand.h in Headers */, + 8124C8741B26B9E700758E00 /* PFPinningObjectStore.h in Headers */, + 81EB6635198A7FA600851598 /* PFConfig.h in Headers */, + 81F0E89C19E6F83E00812A88 /* PFObject+Subclass.h in Headers */, + 815EE8F619F976D50076FE5D /* PFRESTCommand.h in Headers */, + 81A2458E1B1E99C6006A6953 /* PFFieldOperation.h in Headers */, + 81F0E89D19E6F83E00812A88 /* PFSubclassing.h in Headers */, + 81C7F49F1AF421FF007B5418 /* PFFileState_Private.h in Headers */, + 8139B1361A7C2E76002BEF84 /* PFRESTUserCommand.h in Headers */, + 8166FC591B503741003841A2 /* PFAnalytics_Private.h in Headers */, + 814BCDF21B4DF63600007B7F /* PFUserState.h in Headers */, + 815EE92419F989390076FE5D /* PFRESTConfigCommand.h in Headers */, + 8166FCC11B503886003841A2 /* PFSQLiteDatabaseResult.h in Headers */, + 815960A21ABCA3B30069EBCC /* PFFileManager.h in Headers */, + 81BCB4CB1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.h in Headers */, + F51535051B57240900C49F56 /* PFACLPrivate.h in Headers */, + 81F0E89119E6F83E00812A88 /* PFAnalytics.h in Headers */, + 81F0E89319E6F83E00812A88 /* PFCloud.h in Headers */, + 8124C8A01B27BF0900758E00 /* PFSessionController.h in Headers */, + 814B64121A769EF500213055 /* PFLogger.h in Headers */, + 8124C8AD1B27D5D600758E00 /* PFSessionUtilities.h in Headers */, + 814881561B795CAC008763BF /* PFPropertyInfo_Runtime.h in Headers */, + 81BCB4C91B744626006659CB /* PFURLSessionDataTaskDelegate_Private.h in Headers */, + 8166FB9C1B4F2F08003841A2 /* PFUserConstants.h in Headers */, + 81C76EE91B4B201E0031C2FD /* PFObjectConstants.h in Headers */, + 81CB7F701B166FE500DC601D /* PFObjectState.h in Headers */, + 818D6F151B3C8D1900F94C82 /* PFObjectLocalIdStore.h in Headers */, + 8166FC781B50376D003841A2 /* PFObjectController_Private.h in Headers */, + 81493AA51A0D6DE0008D5504 /* PFRESTObjectBatchCommand.h in Headers */, + 814881521B795CAC008763BF /* PFPropertyInfo.h in Headers */, + 8166FC7A1B50376D003841A2 /* PFObjectControlling.h in Headers */, + 8196D58E1B0BD23B000465A1 /* PFCoreManager.h in Headers */, + 81C7F48C1AF4110B007B5418 /* PFQueryUtilities.h in Headers */, + 8166FC981B50381B003841A2 /* PFQueryPrivate.h in Headers */, + 8196D5621B0AB661000465A1 /* PFAnalyticsUtilities.h in Headers */, + F5556A191B66F47900410837 /* PFURLSession_Private.h in Headers */, + 8148815A1B795CAC008763BF /* PFPropertyInfo_Private.h in Headers */, + 81C9C9F819FEA89200D514C5 /* PFRESTPushCommand.h in Headers */, + 81C7F49A1AF42187007B5418 /* PFFileState.h in Headers */, + 81CB7F761B166FF500DC601D /* PFMutableObjectState.h in Headers */, + 8166FCC51B503886003841A2 /* PFSQLiteStatement.h in Headers */, + 812B7AB91AF2FA4800D15FF5 /* PFQueryController.h in Headers */, + 81F0E89719E6F83E00812A88 /* PFObject.h in Headers */, + 81A715A51B423A4100A504FC /* PFObjectUtilities.h in Headers */, + 8171E9BA19AE37F000EAE6C1 /* PFThreadsafety.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXLegacyTarget section */ + 81493A931A0D3492008D5504 /* BoltsSDK-OSX */ = { + isa = PBXLegacyTarget; + buildArgumentsString = "$(SRCROOT)/Vendor/Bolts-ObjC/ \\\n$(SRCROOT)/Vendor/Bolts-ObjC/build/ \\\n'Vendor/Bolts-ObjC/scripts/build_framework.sh -n -c Release'"; + buildConfigurationList = 81493A941A0D3493008D5504 /* Build configuration list for PBXLegacyTarget "BoltsSDK-OSX" */; + buildPhases = ( + ); + buildToolPath = "$(SRCROOT)/Scripts/build_third_party.sh"; + buildWorkingDirectory = "$(SRCROOT)"; + dependencies = ( + ); + name = "BoltsSDK-OSX"; + passBuildSettingsInEnvironment = 0; + productName = BoltsSDK; + }; + F569F07A1B14DB1E00296F73 /* BoltsSDK-iOS */ = { + isa = PBXLegacyTarget; + buildArgumentsString = "$(SRCROOT)/Vendor/Bolts-ObjC/ \\\n$(SRCROOT)/Vendor/Bolts-ObjC/build/ \\\n'Vendor/Bolts-ObjC/scripts/build_framework.sh -n -c Release'"; + buildConfigurationList = F569F07B1B14DB1E00296F73 /* Build configuration list for PBXLegacyTarget "BoltsSDK-iOS" */; + buildPhases = ( + ); + buildToolPath = "$(SRCROOT)/Scripts/build_third_party.sh"; + buildWorkingDirectory = "$(SRCROOT)"; + dependencies = ( + ); + name = "BoltsSDK-iOS"; + passBuildSettingsInEnvironment = 0; + productName = BoltsSDK; + }; +/* End PBXLegacyTarget section */ + +/* Begin PBXNativeTarget section */ + 816F441B1A8E8933009CDB32 /* ParseUnitTests-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 816F44981A8E8933009CDB32 /* Build configuration list for PBXNativeTarget "ParseUnitTests-iOS" */; + buildPhases = ( + 816F441E1A8E8933009CDB32 /* Sources */, + 816F44711A8E8933009CDB32 /* Frameworks */, + 816F447D1A8E8933009CDB32 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ParseUnitTests-iOS"; + productName = ParseTests; + productReference = 816F449B1A8E8933009CDB32 /* ParseUnitTests-iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 81C09F501AF97A490043B49C /* ParseUnitTests-OSX */ = { + isa = PBXNativeTarget; + buildConfigurationList = 81C09F831AF97A490043B49C /* Build configuration list for PBXNativeTarget "ParseUnitTests-OSX" */; + buildPhases = ( + 81C09F511AF97A490043B49C /* Sources */, + 81C09F761AF97A490043B49C /* Frameworks */, + 81C09F821AF97A490043B49C /* Resources */, + 81C09F8C1AF9815F0043B49C /* Copy Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ParseUnitTests-OSX"; + productName = ParseTests; + productReference = 81C09F861AF97A490043B49C /* ParseUnitTests-OSX.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 81C3821B19CCA89E0066284A /* Parse-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 81C3823819CCA89F0066284A /* Build configuration list for PBXNativeTarget "Parse-iOS" */; + buildPhases = ( + 81C3823C19CCA9950066284A /* Generate Localizable Strings */, + 81C3821719CCA89E0066284A /* Sources */, + 81C3821819CCA89E0066284A /* Frameworks */, + 818AAA6D19D3687900FC1B3C /* Headers */, + 8139B12F1A7BF65F002BEF84 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F569F07F1B14DB3C00296F73 /* PBXTargetDependency */, + ); + name = "Parse-iOS"; + productName = "Parse-iOS"; + productReference = 81C3821C19CCA89E0066284A /* Parse.framework */; + productType = "com.apple.product-type.framework"; + }; + 97010FAB1630B18F00AB761E /* Parse-OSX */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97010FB41630B18F00AB761E /* Build configuration list for PBXNativeTarget "Parse-OSX" */; + buildPhases = ( + 97010FB71630B1B800AB761E /* Generate Localizable Strings */, + 97010FA81630B18F00AB761E /* Sources */, + 97010FA91630B18F00AB761E /* Frameworks */, + 97010FAA1630B18F00AB761E /* Headers */, + 8139B12D1A7BF570002BEF84 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 81493A9A1A0D3CE3008D5504 /* PBXTargetDependency */, + ); + name = "Parse-OSX"; + productName = ParseMac; + productReference = 97010FAC1630B18F00AB761E /* ParseOSX.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 09D33641139C54930098E916 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Parse Inc."; + TargetAttributes = { + 81493A931A0D3492008D5504 = { + CreatedOnToolsVersion = 6.1; + }; + 81C3821B19CCA89E0066284A = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 09D33644139C54930098E916 /* Build configuration list for PBXProject "Parse" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + "zh-Hans", + Base, + ); + mainGroup = 09D3363F139C54930098E916; + productRefGroup = 09D3364B139C54940098E916 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 81C3821B19CCA89E0066284A /* Parse-iOS */, + 816F441B1A8E8933009CDB32 /* ParseUnitTests-iOS */, + 97010FAB1630B18F00AB761E /* Parse-OSX */, + 81C09F501AF97A490043B49C /* ParseUnitTests-OSX */, + F569F07A1B14DB1E00296F73 /* BoltsSDK-iOS */, + 81493A931A0D3492008D5504 /* BoltsSDK-OSX */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8139B12D1A7BF570002BEF84 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8139B12E1A7BF630002BEF84 /* third_party_licenses.txt in Resources */, + 81B3F2551AC9D4E100A92677 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8139B12F1A7BF65F002BEF84 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8139B1301A7BF662002BEF84 /* third_party_licenses.txt in Resources */, + 81B3F2541AC9D4E100A92677 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 816F447D1A8E8933009CDB32 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81C09F821AF97A490043B49C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 81C3823C19CCA9950066284A /* Generate Localizable Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Generate Localizable Strings"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Generate localizable strings\nfind $PROJECT_DIR/Parse -name '*.m' -print0 | xargs -0 genstrings -q -o $PROJECT_DIR/Parse/Resources\n"; + }; + 97010FB71630B1B800AB761E /* Generate Localizable Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Generate Localizable Strings"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Generate localizable strings\nfind $PROJECT_DIR/Parse -name '*.m' -print0 | xargs -0 genstrings -q -o $PROJECT_DIR/Parse/Resources\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 816F441E1A8E8933009CDB32 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 814916CB1B66D44600EFD14F /* RelationStateTests.m in Sources */, + 8149167D1B66D44600EFD14F /* ObjectBatchCommandTests.m in Sources */, + 814916691B66D44600EFD14F /* FileUnitTests.m in Sources */, + F5ADB9C71B6C503E002A819E /* TestFileManager.m in Sources */, + 814916331B66D44500EFD14F /* AnalyticsUnitTests.m in Sources */, + F51050A01B6AA4D100749060 /* ExtensionDataSharingTests.m in Sources */, + 814916B91B66D44600EFD14F /* PushMobileTests.m in Sources */, + 81A016271B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m in Sources */, + 814916291B66D44500EFD14F /* ACLStateTests.m in Sources */, + 814916671B66D44600EFD14F /* FileStateTests.m in Sources */, + 814916E11B66D44600EFD14F /* UserUnitTests.m in Sources */, + 81E033721B573FC500B25168 /* PFTestSKProductsResponse.m in Sources */, + 810ECC741B573CC5002944D4 /* OCMock+Parse.m in Sources */, + 81E033711B573FC500B25168 /* PFTestSKProductsRequest.m in Sources */, + 8149163D1B66D44500EFD14F /* BlockRetryerTests.m in Sources */, + 811AAF181B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m in Sources */, + 81E0335C1B573F3E00B25168 /* PFMockURLResponse.m in Sources */, + 814916CD1B66D44600EFD14F /* RelationUnitTests.m in Sources */, + 81E0335A1B573F3E00B25168 /* PFMockURLProtocol.m in Sources */, + 814916591B66D44600EFD14F /* DeviceTests.m in Sources */, + 814916DF1B66D44600EFD14F /* UserFileCodingLogicTests.m in Sources */, + 814916B31B66D44600EFD14F /* PushCommandTests.m in Sources */, + 814916451B66D44600EFD14F /* CommandResultTests.m in Sources */, + 814916391B66D44500EFD14F /* AnonymousUtilsTests.m in Sources */, + 81E0336F1B573FC500B25168 /* PFTestSKPaymentTransaction.m in Sources */, + 8149167B1B66D44600EFD14F /* LocationManagerTests.m in Sources */, + F51050A11B6AA4D600749060 /* ExtensionDataSharingMobileTests.m in Sources */, + 814916751B66D44600EFD14F /* KeychainStoreTests.m in Sources */, + 810ECC6F1B573C6B002944D4 /* SwiftSubclass.swift in Sources */, + 814916CF1B66D44600EFD14F /* RoleUnitTests.m in Sources */, + 814916351B66D44500EFD14F /* AnalyticsUtilitiesTests.m in Sources */, + 814916C71B66D44600EFD14F /* QueryUnitTests.m in Sources */, + F5732DE11B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m in Sources */, + 814916B71B66D44600EFD14F /* PushManagerTests.m in Sources */, + 814916991B66D44600EFD14F /* ObjectUtilitiesTests.m in Sources */, + 814916731B66D44600EFD14F /* InstallationUnitTests.m in Sources */, + 814916A91B66D44600EFD14F /* ProductTests.m in Sources */, + F5E381311B68832000A3B9F2 /* URLSessionTests.m in Sources */, + 814916411B66D44600EFD14F /* CloudCommandTests.m in Sources */, + 814916311B66D44500EFD14F /* AnalyticsControllerTests.m in Sources */, + 814916971B66D44600EFD14F /* ObjectUnitTests.m in Sources */, + 81308B6F1B5781F500FFFF44 /* PFTestSwizzledMethod.m in Sources */, + 814916531B66D44600EFD14F /* DateFormatterTests.m in Sources */, + 814916DD1B66D44600EFD14F /* UserControllerTests.m in Sources */, + 81E033701B573FC500B25168 /* PFTestSKProduct.m in Sources */, + 8149169D1B66D44600EFD14F /* OfflineQueryLogicUnitTests.m in Sources */, + 810ECC7D1B573D28002944D4 /* PFTestCase.m in Sources */, + 814916AB1B66D44600EFD14F /* PropertyInfoTests.m in Sources */, + 8149164F1B66D44600EFD14F /* ConfigUnitTests.m in Sources */, + 814916551B66D44600EFD14F /* DecoderTests.m in Sources */, + 814916A71B66D44600EFD14F /* PinUnitTests.m in Sources */, + F5E381341B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m in Sources */, + 814916771B66D44600EFD14F /* KeyValueCacheTests.m in Sources */, + 814916BD1B66D44600EFD14F /* PushUnitTests.m in Sources */, + 81E0336E1B573FC500B25168 /* PFTestSKPaymentQueue.m in Sources */, + 814916891B66D44600EFD14F /* ObjectLocalIdStoreTests.m in Sources */, + F589894B1B7427FF008A566B /* AlertViewTests.m in Sources */, + 814916DB1B66D44600EFD14F /* UserCommandTests.m in Sources */, + 8149168D1B66D44600EFD14F /* ObjectPinTests.m in Sources */, + 8149169B1B66D44600EFD14F /* OfflineQueryControllerTests.m in Sources */, + 8149169F1B66D44600EFD14F /* OperationSetUnitTests.m in Sources */, + 814916A31B66D44600EFD14F /* ParseSetupUnitTests.m in Sources */, + 8149163B1B66D44500EFD14F /* BaseStateTests.m in Sources */, + 8149162F1B66D44500EFD14F /* AnalyticsCommandTests.m in Sources */, + 814916931B66D44600EFD14F /* ObjectSubclassPropertiesTests.m in Sources */, + 8149168B1B66D44600EFD14F /* ObjectOfflineTests.m in Sources */, + 814916B11B66D44600EFD14F /* PushChannelsControllerTests.m in Sources */, + 814916AD1B66D44600EFD14F /* PurchaseControllerTests.m in Sources */, + 8149167F1B66D44600EFD14F /* ObjectBatchControllerTests.m in Sources */, + 814916BF1B66D44600EFD14F /* QueryCachedControllerTests.m in Sources */, + 81D8E7601B7323ED004B014C /* HashTests.m in Sources */, + 814916851B66D44600EFD14F /* ObjectFileCoderTests.m in Sources */, + 814916831B66D44600EFD14F /* ObjectEstimatedDataTests.m in Sources */, + 814916791B66D44600EFD14F /* LocationManagerMobileTests.m in Sources */, + 8149162B1B66D44500EFD14F /* ACLUnitTests.m in Sources */, + 814916371B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m in Sources */, + 814916D31B66D44600EFD14F /* SessionUnitTests.m in Sources */, + 81E0337E1B57441F00B25168 /* CLLocationManager+TestAdditions.m in Sources */, + 814916471B66D44600EFD14F /* CommandUnitTests.m in Sources */, + 814916611B66D44600EFD14F /* FieldOperationTests.m in Sources */, + 814916A11B66D44600EFD14F /* ParseModuleUnitTests.m in Sources */, + 814916951B66D44600EFD14F /* ObjectSubclassTests.m in Sources */, + 814916B51B66D44600EFD14F /* PushControllerTests.m in Sources */, + 814916D11B66D44600EFD14F /* SessionControllerTests.m in Sources */, + 814916AF1B66D44600EFD14F /* PurchaseUnitTests.m in Sources */, + 8149166D1B66D44600EFD14F /* GeoPointUnitTests.m in Sources */, + 814916711B66D44600EFD14F /* InstallationIdentifierUnitTests.m in Sources */, + 8149164B1B66D44600EFD14F /* ConfigCommandTests.m in Sources */, + 814916871B66D44600EFD14F /* ObjectFileCodingLogicTests.m in Sources */, + 81308B711B5781F500FFFF44 /* PFTestSwizzlingUtilities.m in Sources */, + 8149168F1B66D44600EFD14F /* ObjectStateTests.m in Sources */, + 814916C31B66D44600EFD14F /* QueryPredicateUnitTests.m in Sources */, + 814916431B66D44600EFD14F /* CloudUnitTests.m in Sources */, + 810ECC7F1B573D28002944D4 /* PFUnitTestCase.m in Sources */, + 814916911B66D44600EFD14F /* ObjectSubclassingControllerTests.m in Sources */, + 8149164D1B66D44600EFD14F /* ConfigControllerTests.m in Sources */, + 814916A51B66D44600EFD14F /* PinningObjectStoreTests.m in Sources */, + 8149166B1B66D44600EFD14F /* GeoPointLocationTests.m in Sources */, + 814916D71B66D44600EFD14F /* SQLiteDatabaseTest.m in Sources */, + 814916BB1B66D44600EFD14F /* PushStateTests.m in Sources */, + 814916571B66D44600EFD14F /* DefaultACLControllerTests.m in Sources */, + 814916631B66D44600EFD14F /* FileCommandTests.m in Sources */, + 814916C51B66D44600EFD14F /* QueryStateUnitTests.m in Sources */, + 8149163F1B66D44500EFD14F /* CloudCodeControllerTests.m in Sources */, + 814916491B66D44600EFD14F /* CommandURLRequestConstructorTests.m in Sources */, + 8149165F1B66D44600EFD14F /* FieldOperationDecoderTests.m in Sources */, + 814916C91B66D44600EFD14F /* QueryUtilitiesTests.m in Sources */, + 814916D91B66D44600EFD14F /* URLSessionCommandRunnerTests.m in Sources */, + 814916D51B66D44600EFD14F /* SessionUtilitiesTests.m in Sources */, + F5ADB9CB1B6C5047002A819E /* TestCache.m in Sources */, + 8149166F1B66D44600EFD14F /* IncrementUnitTests.m in Sources */, + 814916651B66D44600EFD14F /* FileControllerTests.m in Sources */, + 814916511B66D44600EFD14F /* CurrentConfigControllerTests.m in Sources */, + 814916C11B66D44600EFD14F /* QueryControllerUnitTests.m in Sources */, + 814916811B66D44600EFD14F /* ObjectCommandTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81C09F511AF97A490043B49C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F510509F1B6AA4CE00749060 /* ExtensionDataSharingTests.m in Sources */, + 8149165A1B66D44600EFD14F /* DeviceTests.m in Sources */, + 814916701B66D44600EFD14F /* IncrementUnitTests.m in Sources */, + 814916A21B66D44600EFD14F /* ParseModuleUnitTests.m in Sources */, + 814916601B66D44600EFD14F /* FieldOperationDecoderTests.m in Sources */, + 81E0337F1B57441F00B25168 /* CLLocationManager+TestAdditions.m in Sources */, + 814916861B66D44600EFD14F /* ObjectFileCoderTests.m in Sources */, + 8149163A1B66D44500EFD14F /* AnonymousUtilsTests.m in Sources */, + 81308B721B5781F500FFFF44 /* PFTestSwizzlingUtilities.m in Sources */, + 814916761B66D44600EFD14F /* KeychainStoreTests.m in Sources */, + 814916AC1B66D44600EFD14F /* PropertyInfoTests.m in Sources */, + 8149168A1B66D44600EFD14F /* ObjectLocalIdStoreTests.m in Sources */, + 814916A61B66D44600EFD14F /* PinningObjectStoreTests.m in Sources */, + 814916501B66D44600EFD14F /* ConfigUnitTests.m in Sources */, + 814916E01B66D44600EFD14F /* UserFileCodingLogicTests.m in Sources */, + 814916D41B66D44600EFD14F /* SessionUnitTests.m in Sources */, + 811AAF191B72D7E400B1AC1F /* ObjectFilePersistenceControllerTests.m in Sources */, + 814916541B66D44600EFD14F /* DateFormatterTests.m in Sources */, + 814916BC1B66D44600EFD14F /* PushStateTests.m in Sources */, + 814916461B66D44600EFD14F /* CommandResultTests.m in Sources */, + 8149163E1B66D44500EFD14F /* BlockRetryerTests.m in Sources */, + 814916781B66D44600EFD14F /* KeyValueCacheTests.m in Sources */, + 814916361B66D44500EFD14F /* AnalyticsUtilitiesTests.m in Sources */, + 814916A81B66D44600EFD14F /* PinUnitTests.m in Sources */, + F5E381321B68832100A3B9F2 /* URLSessionTests.m in Sources */, + 8149166E1B66D44600EFD14F /* GeoPointUnitTests.m in Sources */, + 814916521B66D44600EFD14F /* CurrentConfigControllerTests.m in Sources */, + 8149164A1B66D44600EFD14F /* CommandURLRequestConstructorTests.m in Sources */, + 8149168E1B66D44600EFD14F /* ObjectPinTests.m in Sources */, + 8149169C1B66D44600EFD14F /* OfflineQueryControllerTests.m in Sources */, + 814916901B66D44600EFD14F /* ObjectStateTests.m in Sources */, + 814916D61B66D44600EFD14F /* SessionUtilitiesTests.m in Sources */, + 814916661B66D44600EFD14F /* FileControllerTests.m in Sources */, + 814916DA1B66D44600EFD14F /* URLSessionCommandRunnerTests.m in Sources */, + 814916BE1B66D44600EFD14F /* PushUnitTests.m in Sources */, + 814916C61B66D44600EFD14F /* QueryStateUnitTests.m in Sources */, + 814916D81B66D44600EFD14F /* SQLiteDatabaseTest.m in Sources */, + 814916B21B66D44600EFD14F /* PushChannelsControllerTests.m in Sources */, + 814916481B66D44600EFD14F /* CommandUnitTests.m in Sources */, + 810ECC751B573CC5002944D4 /* OCMock+Parse.m in Sources */, + 814916321B66D44500EFD14F /* AnalyticsControllerTests.m in Sources */, + 814916A01B66D44600EFD14F /* OperationSetUnitTests.m in Sources */, + 814916721B66D44600EFD14F /* InstallationIdentifierUnitTests.m in Sources */, + 814916881B66D44600EFD14F /* ObjectFileCodingLogicTests.m in Sources */, + 814916421B66D44600EFD14F /* CloudCommandTests.m in Sources */, + 814916841B66D44600EFD14F /* ObjectEstimatedDataTests.m in Sources */, + 814916621B66D44600EFD14F /* FieldOperationTests.m in Sources */, + 814916381B66D44500EFD14F /* AnonymousAuthenticationProviderTests.m in Sources */, + 814916B81B66D44600EFD14F /* PushManagerTests.m in Sources */, + 814916681B66D44600EFD14F /* FileStateTests.m in Sources */, + 81308B701B5781F500FFFF44 /* PFTestSwizzledMethod.m in Sources */, + 814916981B66D44600EFD14F /* ObjectUnitTests.m in Sources */, + 814916561B66D44600EFD14F /* DecoderTests.m in Sources */, + 81E0335D1B573F3E00B25168 /* PFMockURLResponse.m in Sources */, + 814916C01B66D44600EFD14F /* QueryCachedControllerTests.m in Sources */, + 814916B61B66D44600EFD14F /* PushControllerTests.m in Sources */, + 814916E21B66D44600EFD14F /* UserUnitTests.m in Sources */, + 81A016281B59E19D00B0C7ED /* PFExtensionDataSharingTestHelper.m in Sources */, + 814916441B66D44600EFD14F /* CloudUnitTests.m in Sources */, + 814916DE1B66D44600EFD14F /* UserControllerTests.m in Sources */, + 814916801B66D44600EFD14F /* ObjectBatchControllerTests.m in Sources */, + 814916CA1B66D44600EFD14F /* QueryUtilitiesTests.m in Sources */, + 81E0335B1B573F3E00B25168 /* PFMockURLProtocol.m in Sources */, + 8149166A1B66D44600EFD14F /* FileUnitTests.m in Sources */, + 814916341B66D44500EFD14F /* AnalyticsUnitTests.m in Sources */, + F5ADB9C81B6C503E002A819E /* TestFileManager.m in Sources */, + 814916921B66D44600EFD14F /* ObjectSubclassingControllerTests.m in Sources */, + 814916C21B66D44600EFD14F /* QueryControllerUnitTests.m in Sources */, + 8149169A1B66D44600EFD14F /* ObjectUtilitiesTests.m in Sources */, + 8149169E1B66D44600EFD14F /* OfflineQueryLogicUnitTests.m in Sources */, + 8149163C1B66D44500EFD14F /* BaseStateTests.m in Sources */, + 814916821B66D44600EFD14F /* ObjectCommandTests.m in Sources */, + 814916CC1B66D44600EFD14F /* RelationStateTests.m in Sources */, + 814916741B66D44600EFD14F /* InstallationUnitTests.m in Sources */, + 814916DC1B66D44600EFD14F /* UserCommandTests.m in Sources */, + 8149168C1B66D44600EFD14F /* ObjectOfflineTests.m in Sources */, + F5ADB9CC1B6C5047002A819E /* TestCache.m in Sources */, + 8149162A1B66D44500EFD14F /* ACLStateTests.m in Sources */, + 8149164C1B66D44600EFD14F /* ConfigCommandTests.m in Sources */, + F5732DE21B6712140066DCD5 /* URLSessionDataTaskDelegateTests.m in Sources */, + 814916401B66D44500EFD14F /* CloudCodeControllerTests.m in Sources */, + 814916CE1B66D44600EFD14F /* RelationUnitTests.m in Sources */, + 814916301B66D44500EFD14F /* AnalyticsCommandTests.m in Sources */, + 8149167E1B66D44600EFD14F /* ObjectBatchCommandTests.m in Sources */, + 814916641B66D44600EFD14F /* FileCommandTests.m in Sources */, + F5E381351B696C2F00A3B9F2 /* URLSessionUploadTaskDelegateTests.m in Sources */, + 810ECC801B573D28002944D4 /* PFUnitTestCase.m in Sources */, + 8149164E1B66D44600EFD14F /* ConfigControllerTests.m in Sources */, + 81D8E7611B7323ED004B014C /* HashTests.m in Sources */, + 8149167C1B66D44600EFD14F /* LocationManagerTests.m in Sources */, + 810ECC701B573C6B002944D4 /* SwiftSubclass.swift in Sources */, + 814916D01B66D44600EFD14F /* RoleUnitTests.m in Sources */, + 814916941B66D44600EFD14F /* ObjectSubclassPropertiesTests.m in Sources */, + 8149162C1B66D44500EFD14F /* ACLUnitTests.m in Sources */, + 814916A41B66D44600EFD14F /* ParseSetupUnitTests.m in Sources */, + 810ECC7E1B573D28002944D4 /* PFTestCase.m in Sources */, + 814916C81B66D44600EFD14F /* QueryUnitTests.m in Sources */, + 814916961B66D44600EFD14F /* ObjectSubclassTests.m in Sources */, + 814916581B66D44600EFD14F /* DefaultACLControllerTests.m in Sources */, + 814916B41B66D44600EFD14F /* PushCommandTests.m in Sources */, + 814916D21B66D44600EFD14F /* SessionControllerTests.m in Sources */, + 814916C41B66D44600EFD14F /* QueryPredicateUnitTests.m in Sources */, + 8149166C1B66D44600EFD14F /* GeoPointLocationTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81C3821719CCA89E0066284A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81C1EE4A1AE1EF960031C438 /* PFWeakValue.m in Sources */, + 814BCDF31B4DF63600007B7F /* PFUserState.m in Sources */, + 812B02AA1B5DE562003846EE /* PFCommandURLRequestConstructor.m in Sources */, + 8196D58F1B0BD23B000465A1 /* PFCoreManager.m in Sources */, + 81BCB4D01B744626006659CB /* PFURLSessionUploadTaskDelegate.m in Sources */, + 8166FCBA1B503886003841A2 /* PFPin.m in Sources */, + 81C3827819CCADA00066284A /* PFMulticastDelegate.m in Sources */, + 814881571B795CAC008763BF /* PFPropertyInfo_Runtime.m in Sources */, + 8124C8AE1B27D5D600758E00 /* PFSessionUtilities.m in Sources */, + 810749B01B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m in Sources */, + 8166FC651B50375D003841A2 /* PFOperationSet.m in Sources */, + 818D58751B5DAAFE00813989 /* PFCommandRunningConstants.m in Sources */, + 81443B351A27838500F3FD17 /* PFDevice.m in Sources */, + 8166FCC61B503886003841A2 /* PFSQLiteStatement.m in Sources */, + 81C3826C19CCADA00066284A /* ParseModule.m in Sources */, + 81C3824019CCAD2C0066284A /* PFACL.m in Sources */, + 81ABC1001B5427EC00BA9009 /* PFUserController.m in Sources */, + F5C8F2C11B1F7E7900CD98E7 /* PFAsyncTaskQueue.m in Sources */, + 81C3826F19CCADA00066284A /* PFCommandCache.m in Sources */, + 81CB7F961B1795CF00DC601D /* PFMutablePushState.m in Sources */, + 8166FC751B50376D003841A2 /* PFObjectController.m in Sources */, + 81C3826A19CCAD7F0066284A /* PFCategoryLoader.m in Sources */, + 8166FCDB1B503914003841A2 /* PFUserAuthenticationController.m in Sources */, + F5E8DE1B1B29100000EEA594 /* PFRelationState.m in Sources */, + 8127148A1AE6F1270076AE8D /* ParseManager.m in Sources */, + 81CB7F901B1795C000DC601D /* PFPushState.m in Sources */, + 8196D55D1B0AB64B000465A1 /* PFAnalyticsController.m in Sources */, + 81BCB4C61B744626006659CB /* PFURLSessionDataTaskDelegate.m in Sources */, + 81C3824619CCAD2C0066284A /* PFGeoPoint.m in Sources */, + 81493AA61A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m in Sources */, + 81A2458F1B1E99C6006A6953 /* PFFieldOperation.m in Sources */, + 8124C8861B27588800758E00 /* PFPushChannelsController.m in Sources */, + 814881621B795CD4008763BF /* PFMultiProcessFileLock.m in Sources */, + 81C3826819CCAD790066284A /* PFAlertView.m in Sources */, + 811214751B3E1CF10052741B /* PFObjectBatchController.m in Sources */, + 8166FCDF1B503914003841A2 /* PFAnonymousAuthenticationProvider.m in Sources */, + 8166FCC21B503886003841A2 /* PFSQLiteDatabaseResult.m in Sources */, + 812FC6211B0FF9FA0043C07F /* PFPurchaseController.m in Sources */, + 819A4B0A1A67330200D01241 /* PFHash.m in Sources */, + 814CAD6D1A76ACB600EA4269 /* PFRESTUserCommand.m in Sources */, + 81A245951B1E99EA006A6953 /* PFFieldOperationDecoder.m in Sources */, + 81CB7F711B166FE500DC601D /* PFObjectState.m in Sources */, + 814881471B795C63008763BF /* PFKeyValueCache.m in Sources */, + 81C3825119CCAD2C0066284A /* PFNetworkActivityIndicatorManager.m in Sources */, + 81C3824819CCAD2C0066284A /* PFObject.m in Sources */, + F51D06351B792CF10044539E /* PFSQLiteDatabaseController.m in Sources */, + 815960A31ABCA3B30069EBCC /* PFFileManager.m in Sources */, + 81CD66561B4DA5A70042FC0B /* PFCurrentInstallationController.m in Sources */, + 91DF24971A09BAF100CFC7D4 /* PFPinningEventuallyQueue.m in Sources */, + 815EE94819FAD12F0076FE5D /* PFRESTQueryCommand.m in Sources */, + 8121457F1AA4A808000B23F5 /* PFRESTSessionCommand.m in Sources */, + 814881531B795CAC008763BF /* PFPropertyInfo.m in Sources */, + 81C3824B19CCAD2C0066284A /* PFPush.m in Sources */, + 81CB7F771B166FF500DC601D /* PFMutableObjectState.m in Sources */, + F50C66341B33A708001941A6 /* PFPushUtilities.m in Sources */, + 81C3824C19CCAD2C0066284A /* PFQuery.m in Sources */, + 81BF4AB81B0BF3E500A3D75B /* PFConfigController.m in Sources */, + 8166FB9D1B4F2F08003841A2 /* PFUserConstants.m in Sources */, + 81BBE1371A0062B800622646 /* PFRESTAnalyticsCommand.m in Sources */, + 812B7ABA1AF2FA4800D15FF5 /* PFQueryController.m in Sources */, + 815EE91F19F987910076FE5D /* PFRESTCloudCommand.m in Sources */, + 81C3824519CCAD2C0066284A /* PFFile.m in Sources */, + 8196D5631B0AB661000465A1 /* PFAnalyticsUtilities.m in Sources */, + 815EE8F719F976D50076FE5D /* PFRESTCommand.m in Sources */, + 81EB59601AF46434001EA1FC /* PFFileController.m in Sources */, + 81C76EEB1B4B218C0031C2FD /* PFObjectConstants.m in Sources */, + 8166FC851B503794003841A2 /* PFInstallationIdentifierStore.m in Sources */, + 814BCDF91B4DF66500007B7F /* PFMutableUserState.m in Sources */, + 81EEE1B21B446D600087AC4D /* PFCurrentUserController.m in Sources */, + 8166FCB21B503886003841A2 /* PFOfflineQueryLogic.m in Sources */, + F51535011B571E9100C49F56 /* PFACLState.m in Sources */, + 815EE92519F989390076FE5D /* PFRESTConfigCommand.m in Sources */, + 81C7F48D1AF4110B007B5418 /* PFQueryUtilities.m in Sources */, + 8166FCCD1B5038B7003841A2 /* PFPaymentTransactionObserver.m in Sources */, + 81C9C9F919FEA89200D514C5 /* PFRESTPushCommand.m in Sources */, + 8166FC711B50376D003841A2 /* PFOfflineObjectController.m in Sources */, + 81C3827419CCADA00066284A /* PFKeychainStore.m in Sources */, + 81CB7FA11B1800E400DC601D /* PFPushController.m in Sources */, + 81C7F4B21AF42BD9007B5418 /* PFQueryState.m in Sources */, + 8124C8A11B27BF0900758E00 /* PFSessionController.m in Sources */, + 81329E901AE1E8840071EE3E /* PFReachability.m in Sources */, + 81C7F4A41AF4220A007B5418 /* PFMutableFileState.m in Sources */, + 81C3826E19CCADA00066284A /* PFBlockRetryer.m in Sources */, + 81BF4ABE1B0BF64B00A3D75B /* PFCurrentConfigController.m in Sources */, + 81C3824E19CCAD2C0066284A /* PFRole.m in Sources */, + 91DF24931A09BA7600CFC7D4 /* PFEventuallyQueue.m in Sources */, + 81C3826B19CCAD850066284A /* PFThreadsafety.m in Sources */, + 818D6F161B3C8D1900F94C82 /* PFObjectLocalIdStore.m in Sources */, + 8166FC921B5037F5003841A2 /* PFProductsRequestHandler.m in Sources */, + 81E7A2271B6042BD006CB680 /* PFObjectFileCodingLogic.m in Sources */, + 8124C88C1B276B8800758E00 /* PFObjectFilePersistenceController.m in Sources */, + 818D586C1B5D9F4B00813989 /* PFURLSessionCommandRunner.m in Sources */, + 815619021A1F79AC0076504A /* PFDateFormatter.m in Sources */, + 8124C8751B26B9E700758E00 /* PFPinningObjectStore.m in Sources */, + 81C7F49B1AF42187007B5418 /* PFFileState.m in Sources */, + F5E8DE211B29112000EEA594 /* PFMutableRelationState.m in Sources */, + F51535041B571E9100C49F56 /* PFMutableACLState.m in Sources */, + 81BB6E231B0E7A1A00465C38 /* PFBase64Encoder.m in Sources */, + 81C9CA0819FECF5F00D514C5 /* PFRESTFileCommand.m in Sources */, + 812B63021B5F30D3009CEAA9 /* PFObjectFileCoder.m in Sources */, + 81C3827319CCADA00066284A /* PFInternalUtils.m in Sources */, + 818D586F1B5DA43800813989 /* PFCommandRunning.m in Sources */, + 81CD665C1B4DA5BA0042FC0B /* PFInstallationController.m in Sources */, + 81C3826919CCAD7F0066284A /* BFTask+Private.m in Sources */, + 81068EF31AE0845D00A34D13 /* PFEncoder.m in Sources */, + 81951F181ACB90DA00E142EB /* PFJSONSerialization.m in Sources */, + 81C3824319CCAD2C0066284A /* PFCloud.m in Sources */, + 81C3825019CCAD2C0066284A /* PFUser.m in Sources */, + 81D843CB1B012FBA007CEBCB /* PFCloudCodeController.m in Sources */, + 8143E6651AFC1C7D008C4E06 /* PFCachedQueryController.m in Sources */, + 81C6BDF01B4DB16500553A83 /* PFInstallationConstants.m in Sources */, + 8143E65F1AFC1BA5008C4E06 /* PFOfflineQueryController.m in Sources */, + 814B64131A769EF500213055 /* PFLogger.m in Sources */, + 815EE93D19FA56D20076FE5D /* PFHTTPURLRequestConstructor.m in Sources */, + 81A715A61B423A4100A504FC /* PFObjectUtilities.m in Sources */, + 81BCB4CC1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m in Sources */, + 818D6F221B3DCB5A00F94C82 /* PFObjectEstimatedData.m in Sources */, + 81C3824419CCAD2C0066284A /* PFConfig.m in Sources */, + 814881661B795CD4008763BF /* PFMultiProcessFileLockController.m in Sources */, + 81BBE13119FFCB3700622646 /* PFURLConstructor.m in Sources */, + 81C3827019CCADA00066284A /* PFDecoder.m in Sources */, + 81C3824719CCAD2C0066284A /* PFInstallation.m in Sources */, + F586B3511B1E3BD70082E3BD /* PFBaseState.m in Sources */, + 91115EFA1A097AF30092D1C9 /* PFEventuallyPin.m in Sources */, + F5C42CD61B34F68C00C720D8 /* PFObjectSubclassingController.m in Sources */, + 81C3825519CCAD4D0066284A /* PFCommandResult.m in Sources */, + 81C3823E19CCAD090066284A /* PFConstants.m in Sources */, + 81C3824119CCAD2C0066284A /* PFAnalytics.m in Sources */, + 816AC9BB1A3F48250031D94C /* PFApplication.m in Sources */, + 812145791AA4A4C1000B23F5 /* PFSession.m in Sources */, + 81C3827E19CCADA00066284A /* PFTaskQueue.m in Sources */, + 81C3828019CCADA00066284A /* PFLocationManager.m in Sources */, + 81C3824D19CCAD2C0066284A /* PFRelation.m in Sources */, + F5C42CDC1B38761B00C720D8 /* PFObjectSubclassInfo.m in Sources */, + 81146C801A785203001F8473 /* PFRESTObjectCommand.m in Sources */, + 8166FCEA1B504083003841A2 /* PFPushManager.m in Sources */, + 8166FCB61B503886003841A2 /* PFOfflineStore.m in Sources */, + 8166FCBE1B503886003841A2 /* PFSQLiteDatabase.m in Sources */, + 81C3824919CCAD2C0066284A /* PFProduct.m in Sources */, + 81C3823F19CCAD2C0066284A /* Parse.m in Sources */, + 813E769C1B7A9BD000FA3294 /* PFErrorUtilities.m in Sources */, + 81C3824219CCAD2C0066284A /* PFAnonymousUtils.m in Sources */, + F515355B1B57573700C49F56 /* PFDefaultACLController.m in Sources */, + 81C7F4AE1AF42BD9007B5418 /* PFMutableQueryState.m in Sources */, + 812B02981B5DE3EE003846EE /* PFURLSession.m in Sources */, + 81C3824A19CCAD2C0066284A /* PFPurchase.m in Sources */, + 81E7A21E1B602560006CB680 /* PFUserFileCodingLogic.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97010FA81630B18F00AB761E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 814881481B795C63008763BF /* PFKeyValueCache.m in Sources */, + F515355C1B57573700C49F56 /* PFDefaultACLController.m in Sources */, + 91CDB94E1A32E5E800FF830F /* PFEventuallyPin.m in Sources */, + 91CDB94C1A32E5C900FF830F /* PFEventuallyQueue.m in Sources */, + 814881581B795CAC008763BF /* PFPropertyInfo_Runtime.m in Sources */, + 81D843CC1B012FBA007CEBCB /* PFCloudCodeController.m in Sources */, + F50C667C1B34B231001941A6 /* PFPushUtilities.m in Sources */, + 91CDB94D1A32E5C900FF830F /* PFPinningEventuallyQueue.m in Sources */, + 81C7F48E1AF4110B007B5418 /* PFQueryUtilities.m in Sources */, + 81C1EE4B1AE1EFA10031C438 /* PFWeakValue.m in Sources */, + 81BB6E241B0E7A1A00465C38 /* PFBase64Encoder.m in Sources */, + 81C7F4A51AF4220A007B5418 /* PFMutableFileState.m in Sources */, + 8166FCBF1B503886003841A2 /* PFSQLiteDatabase.m in Sources */, + 818D58701B5DA43800813989 /* PFCommandRunning.m in Sources */, + 8166FC661B50375D003841A2 /* PFOperationSet.m in Sources */, + 81C7F4B31AF42BD9007B5418 /* PFQueryState.m in Sources */, + 81BF4ABF1B0BF64B00A3D75B /* PFCurrentConfigController.m in Sources */, + 811214761B3E1CF10052741B /* PFObjectBatchController.m in Sources */, + 81986CA51A412277007B8860 /* PFApplication.m in Sources */, + 81A715A71B423A4100A504FC /* PFObjectUtilities.m in Sources */, + 7CBC8DA116D594F800AEC66D /* PFTaskQueue.m in Sources */, + 8124C8761B26B9E700758E00 /* PFPinningObjectStore.m in Sources */, + 8143E6601AFC1BA5008C4E06 /* PFOfflineQueryController.m in Sources */, + 81BCB4D11B744626006659CB /* PFURLSessionUploadTaskDelegate.m in Sources */, + 8171E99F19AE091000EAE6C1 /* PFFile.m in Sources */, + 81443B361A27838500F3FD17 /* PFDevice.m in Sources */, + 81EBF3451B33E7D800991947 /* PFMutablePushState.m in Sources */, + 9701107B1630B45800AB761E /* Parse.m in Sources */, + 81BF4AB91B0BF3E500A3D75B /* PFConfigController.m in Sources */, + 81C6BDF11B4DB16500553A83 /* PFInstallationConstants.m in Sources */, + 9701107C1630B45800AB761E /* PFACL.m in Sources */, + 810749B11B74662B00682EEB /* PFURLSessionFileDownloadTaskDelegate.m in Sources */, + 81CB7F721B166FE500DC601D /* PFObjectState.m in Sources */, + 8124C8AF1B27D5D600758E00 /* PFSessionUtilities.m in Sources */, + 9701107D1630B45800AB761E /* PFAnonymousUtils.m in Sources */, + 8121457A1AA4A4C1000B23F5 /* PFSession.m in Sources */, + 9701107E1630B45800AB761E /* PFCloud.m in Sources */, + 81EB59611AF46434001EA1FC /* PFFileController.m in Sources */, + 9701107F1630B45800AB761E /* PFConstants.m in Sources */, + 8143E6661AFC1C7D008C4E06 /* PFCachedQueryController.m in Sources */, + F5C8F2C01B1F7E7800CD98E7 /* PFAsyncTaskQueue.m in Sources */, + 81D0EE9C19B0A2060000AE75 /* PFKeychainStore.m in Sources */, + 814881671B795CD4008763BF /* PFMultiProcessFileLockController.m in Sources */, + 81068EF41AE0845D00A34D13 /* PFEncoder.m in Sources */, + F51535071B57240900C49F56 /* PFACLState.m in Sources */, + 970110821630B45800AB761E /* PFGeoPoint.m in Sources */, + 970110841630B45800AB761E /* PFObject.m in Sources */, + 81EBF3471B33E7E500991947 /* PFPushState.m in Sources */, + 815EE94319FA88FF0076FE5D /* PFHTTPURLRequestConstructor.m in Sources */, + 813E769D1B7A9BD000FA3294 /* PFErrorUtilities.m in Sources */, + 818D586D1B5D9F4B00813989 /* PFURLSessionCommandRunner.m in Sources */, + 81BCB4CD1B744626006659CB /* PFURLSessionJSONDataTaskDelegate.m in Sources */, + 8124C88D1B276B8800758E00 /* PFObjectFilePersistenceController.m in Sources */, + 8166FCE01B503914003841A2 /* PFAnonymousAuthenticationProvider.m in Sources */, + 970110881630B45800AB761E /* PFQuery.m in Sources */, + 970110891630B45800AB761E /* PFRelation.m in Sources */, + 81EBF3461B33E7DE00991947 /* PFPushChannelsController.m in Sources */, + 9701108A1630B45800AB761E /* PFRole.m in Sources */, + 9701108C1630B45800AB761E /* PFUser.m in Sources */, + 81E7A2281B6042BD006CB680 /* PFObjectFileCodingLogic.m in Sources */, + 8166FCEB1B504083003841A2 /* PFPushManager.m in Sources */, + 819A4B0B1A67330200D01241 /* PFHash.m in Sources */, + F5C42CD71B34F68C00C720D8 /* PFObjectSubclassingController.m in Sources */, + 812B02AB1B5DE562003846EE /* PFCommandURLRequestConstructor.m in Sources */, + 818D58761B5DAAFE00813989 /* PFCommandRunningConstants.m in Sources */, + 8124C8A21B27BF0900758E00 /* PFSessionController.m in Sources */, + 818D6F171B3C8D1900F94C82 /* PFObjectLocalIdStore.m in Sources */, + 8196D5901B0BD23B000465A1 /* PFCoreManager.m in Sources */, + 81DDB912199A551A00B50F35 /* ParseModule.m in Sources */, + F586B3521B1E3BE90082E3BD /* PFBaseState.m in Sources */, + 814881541B795CAC008763BF /* PFPropertyInfo.m in Sources */, + 81EBF3441B33E7D400991947 /* PFPushController.m in Sources */, + 8166FCB31B503886003841A2 /* PFOfflineQueryLogic.m in Sources */, + 81C9C9FA19FEA89200D514C5 /* PFRESTPushCommand.m in Sources */, + 81A245901B1E99C6006A6953 /* PFFieldOperation.m in Sources */, + 81BCB4C71B744626006659CB /* PFURLSessionDataTaskDelegate.m in Sources */, + 8166FC721B50376D003841A2 /* PFOfflineObjectController.m in Sources */, + 8103FA3A198FC190000BAE3F /* BFTask+Private.m in Sources */, + 815960A41ABCA3B30069EBCC /* PFFileManager.m in Sources */, + 81C7F4AF1AF42BD9007B5418 /* PFMutableQueryState.m in Sources */, + 8103FA3E198FC190000BAE3F /* PFCategoryLoader.m in Sources */, + F5E8DE221B29112000EEA594 /* PFMutableRelationState.m in Sources */, + 81C9CA0919FECF5F00D514C5 /* PFRESTFileCommand.m in Sources */, + 81EB6637198A7FA600851598 /* PFConfig.m in Sources */, + 8166FCC71B503886003841A2 /* PFSQLiteStatement.m in Sources */, + 81C76EEC1B4B218C0031C2FD /* PFObjectConstants.m in Sources */, + 814B64141A769EF500213055 /* PFLogger.m in Sources */, + 970110681630B44200AB761E /* PFBlockRetryer.m in Sources */, + F5E8DE1C1B29100000EEA594 /* PFRelationState.m in Sources */, + 815619031A1F79AC0076504A /* PFDateFormatter.m in Sources */, + 8166FCC31B503886003841A2 /* PFSQLiteDatabaseResult.m in Sources */, + 812B63031B5F30D3009CEAA9 /* PFObjectFileCoder.m in Sources */, + 81BBE13219FFCB3700622646 /* PFURLConstructor.m in Sources */, + 970110691630B44200AB761E /* PFCommandCache.m in Sources */, + 814BCDFA1B4DF66500007B7F /* PFMutableUserState.m in Sources */, + 8166FB9E1B4F2F08003841A2 /* PFUserConstants.m in Sources */, + 812B7ABB1AF2FA4800D15FF5 /* PFQueryController.m in Sources */, + 812145801AA4A808000B23F5 /* PFRESTSessionCommand.m in Sources */, + 81EEE1B31B446D600087AC4D /* PFCurrentUserController.m in Sources */, + 814BCDF41B4DF63600007B7F /* PFUserState.m in Sources */, + 818D6F231B3DCB5A00F94C82 /* PFObjectEstimatedData.m in Sources */, + 81C7F49C1AF42187007B5418 /* PFFileState.m in Sources */, + 815EE94919FAD12F0076FE5D /* PFRESTQueryCommand.m in Sources */, + 81CD665D1B4DA5BA0042FC0B /* PFInstallationController.m in Sources */, + 81CD66571B4DA5A70042FC0B /* PFCurrentInstallationController.m in Sources */, + 81EBF3481B33E7EB00991947 /* PFInstallation.m in Sources */, + 8169701C19BE94BB00EC1D1F /* PFDecoder.m in Sources */, + 81329E911AE1E8840071EE3E /* PFReachability.m in Sources */, + F515350A1B57240900C49F56 /* PFMutableACLState.m in Sources */, + 81493AA71A0D6DE0008D5504 /* PFRESTObjectBatchCommand.m in Sources */, + 812B02991B5DE3EE003846EE /* PFURLSession.m in Sources */, + 9701106E1630B44200AB761E /* PFInternalUtils.m in Sources */, + 81A245961B1E99EA006A6953 /* PFFieldOperationDecoder.m in Sources */, + 8171E9BB19AE37F500EAE6C1 /* PFThreadsafety.m in Sources */, + F5C42CDD1B38761B00C720D8 /* PFObjectSubclassInfo.m in Sources */, + 970110721630B44200AB761E /* PFMulticastDelegate.m in Sources */, + 81E7A21F1B602560006CB680 /* PFUserFileCodingLogic.m in Sources */, + F590194A1B7992E000F763EF /* PFSQLiteDatabaseController.m in Sources */, + 81146C811A785203001F8473 /* PFRESTObjectCommand.m in Sources */, + 81EBF3411B33E7C600991947 /* PFPush.m in Sources */, + 8127148B1AE6F1270076AE8D /* ParseManager.m in Sources */, + 970110791630B44200AB761E /* PFLocationManager.m in Sources */, + 81BBE1381A0062B800622646 /* PFRESTAnalyticsCommand.m in Sources */, + 815EE92019F987910076FE5D /* PFRESTCloudCommand.m in Sources */, + 8166FCDC1B503914003841A2 /* PFUserAuthenticationController.m in Sources */, + 815EE92619F989390076FE5D /* PFRESTConfigCommand.m in Sources */, + 814881631B795CD4008763BF /* PFMultiProcessFileLock.m in Sources */, + 81ABC1011B5427EC00BA9009 /* PFUserController.m in Sources */, + 974268CA1651ED4E00F2BC57 /* PFCommandResult.m in Sources */, + 81AFE0E91A1FDB7D00AB6CB3 /* PFRESTUserCommand.m in Sources */, + 97EB055516F7CCE400E09147 /* PFAnalytics.m in Sources */, + 81951F191ACB90DA00E142EB /* PFJSONSerialization.m in Sources */, + 8166FCBB1B503886003841A2 /* PFPin.m in Sources */, + 8166FCB71B503886003841A2 /* PFOfflineStore.m in Sources */, + 815EE8F819F976D50076FE5D /* PFRESTCommand.m in Sources */, + 81CB7F781B166FF500DC601D /* PFMutableObjectState.m in Sources */, + 8166FC861B503794003841A2 /* PFInstallationIdentifierStore.m in Sources */, + 8196D55E1B0AB64B000465A1 /* PFAnalyticsController.m in Sources */, + 8196D5641B0AB661000465A1 /* PFAnalyticsUtilities.m in Sources */, + 8166FC761B50376D003841A2 /* PFObjectController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 81493A9A1A0D3CE3008D5504 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 81493A931A0D3492008D5504 /* BoltsSDK-OSX */; + targetProxy = 81493A991A0D3CE3008D5504 /* PBXContainerItemProxy */; + }; + F569F07F1B14DB3C00296F73 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F569F07A1B14DB1E00296F73 /* BoltsSDK-iOS */; + targetProxy = F569F07E1B14DB3C00296F73 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 09D3366B139C54940098E916 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB671B4F39DA00A0ECD5 /* Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 09D3366C139C54940098E916 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB681B4F39DA00A0ECD5 /* Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 81493A951A0D3493008D5504 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB521B4F39DA00A0ECD5 /* BoltsSDK-OSX.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 81493A961A0D3493008D5504 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB521B4F39DA00A0ECD5 /* BoltsSDK-OSX.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 814C3AA91B6975B600E307BB /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 814C3AB01B6975DE00E307BB /* Test.xcconfig */; + buildSettings = { + }; + name = Test; + }; + 814C3AAA1B6975B600E307BB /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB531B4F39DA00A0ECD5 /* Parse-iOS.xcconfig */; + buildSettings = { + }; + name = Test; + }; + 814C3AAB1B6975B600E307BB /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB591B4F39DA00A0ECD5 /* ParseUnitTests-iOS.xcconfig */; + buildSettings = { + }; + name = Test; + }; + 814C3AAC1B6975B600E307BB /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB541B4F39DA00A0ECD5 /* Parse-OSX.xcconfig */; + buildSettings = { + }; + name = Test; + }; + 814C3AAD1B6975B600E307BB /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB5A1B4F39DA00A0ECD5 /* ParseUnitTests-OSX.xcconfig */; + buildSettings = { + }; + name = Test; + }; + 814C3AAE1B6975B600E307BB /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB511B4F39DA00A0ECD5 /* BoltsSDK-iOS.xcconfig */; + buildSettings = { + }; + name = Test; + }; + 814C3AAF1B6975B600E307BB /* Test */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB521B4F39DA00A0ECD5 /* BoltsSDK-OSX.xcconfig */; + buildSettings = { + }; + name = Test; + }; + 816F44991A8E8933009CDB32 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB591B4F39DA00A0ECD5 /* ParseUnitTests-iOS.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 816F449A1A8E8933009CDB32 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB591B4F39DA00A0ECD5 /* ParseUnitTests-iOS.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 81C09F841AF97A490043B49C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB5A1B4F39DA00A0ECD5 /* ParseUnitTests-OSX.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 81C09F851AF97A490043B49C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB5A1B4F39DA00A0ECD5 /* ParseUnitTests-OSX.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 81C3823419CCA89F0066284A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB531B4F39DA00A0ECD5 /* Parse-iOS.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 81C3823519CCA89F0066284A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB531B4F39DA00A0ECD5 /* Parse-iOS.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 97010FB51630B18F00AB761E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB541B4F39DA00A0ECD5 /* Parse-OSX.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 97010FB61630B18F00AB761E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB541B4F39DA00A0ECD5 /* Parse-OSX.xcconfig */; + buildSettings = { + }; + name = Release; + }; + F569F07C1B14DB1E00296F73 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB511B4F39DA00A0ECD5 /* BoltsSDK-iOS.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + F569F07D1B14DB1E00296F73 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F55ABB511B4F39DA00A0ECD5 /* BoltsSDK-iOS.xcconfig */; + buildSettings = { + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 09D33644139C54930098E916 /* Build configuration list for PBXProject "Parse" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09D3366B139C54940098E916 /* Debug */, + 814C3AA91B6975B600E307BB /* Test */, + 09D3366C139C54940098E916 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 81493A941A0D3493008D5504 /* Build configuration list for PBXLegacyTarget "BoltsSDK-OSX" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81493A951A0D3493008D5504 /* Debug */, + 814C3AAF1B6975B600E307BB /* Test */, + 81493A961A0D3493008D5504 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 816F44981A8E8933009CDB32 /* Build configuration list for PBXNativeTarget "ParseUnitTests-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 816F44991A8E8933009CDB32 /* Debug */, + 814C3AAB1B6975B600E307BB /* Test */, + 816F449A1A8E8933009CDB32 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 81C09F831AF97A490043B49C /* Build configuration list for PBXNativeTarget "ParseUnitTests-OSX" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81C09F841AF97A490043B49C /* Debug */, + 814C3AAD1B6975B600E307BB /* Test */, + 81C09F851AF97A490043B49C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 81C3823819CCA89F0066284A /* Build configuration list for PBXNativeTarget "Parse-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81C3823419CCA89F0066284A /* Debug */, + 814C3AAA1B6975B600E307BB /* Test */, + 81C3823519CCA89F0066284A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97010FB41630B18F00AB761E /* Build configuration list for PBXNativeTarget "Parse-OSX" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97010FB51630B18F00AB761E /* Debug */, + 814C3AAC1B6975B600E307BB /* Test */, + 97010FB61630B18F00AB761E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F569F07B1B14DB1E00296F73 /* Build configuration list for PBXLegacyTarget "BoltsSDK-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F569F07C1B14DB1E00296F73 /* Debug */, + 814C3AAE1B6975B600E307BB /* Test */, + F569F07D1B14DB1E00296F73 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 09D33641139C54930098E916 /* Project object */; +} diff --git a/Parse.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Parse.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7bf1865e5 --- /dev/null +++ b/Parse.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Parse.xcodeproj/xcshareddata/xcschemes/Parse-OSX.xcscheme b/Parse.xcodeproj/xcshareddata/xcschemes/Parse-OSX.xcscheme new file mode 100644 index 000000000..9cdff3423 --- /dev/null +++ b/Parse.xcodeproj/xcshareddata/xcschemes/Parse-OSX.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Parse.xcodeproj/xcshareddata/xcschemes/Parse-iOS.xcscheme b/Parse.xcodeproj/xcshareddata/xcschemes/Parse-iOS.xcscheme new file mode 100644 index 000000000..9b79852fe --- /dev/null +++ b/Parse.xcodeproj/xcshareddata/xcschemes/Parse-iOS.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Parse.xcworkspace/contents.xcworkspacedata b/Parse.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..49104e367 --- /dev/null +++ b/Parse.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.h b/Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.h new file mode 100644 index 000000000..5fe166419 --- /dev/null +++ b/Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFACL; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFDefaultACLController : NSObject + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +// TODO: (nlutsenko, richardross) Make it not terrible aka don't have singletons ++ (instancetype)defaultController; ++ (void)clearDefaultController; + +///-------------------------------------- +/// @name Default ACL +///-------------------------------------- + +/*! + Get the default ACL managed by this controller. + + @return A task that returns the ACL encapsulated by this controller. + */ +- (BFTask *)getDefaultACLAsync; + +/*! + Set the new default default ACL to be encapsulated in this controller. + + @param acl The new ACL. Will be copied. + @param accessForCurrentUser Whether or not we should add special access for the current user on this ACL. + + @return A task that returns the new (copied) ACL now encapsulated in this controller. + */ +- (BFTask *)setDefaultACLAsync:(PFACL *)acl withCurrentUserAccess:(BOOL)accessForCurrentUser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.m b/Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.m new file mode 100644 index 000000000..8eefd8bb9 --- /dev/null +++ b/Parse/Internal/ACL/DefaultACLController/PFDefaultACLController.m @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDefaultACLController.h" + +#import + +#import "PFACLPrivate.h" +#import "PFAsyncTaskQueue.h" +#import "PFCoreManager.h" +#import "PFCurrentUserController.h" +#import "Parse_Private.h" + +@implementation PFDefaultACLController { + PFAsyncTaskQueue *_taskQueue; + + PFACL *_defaultACL; + BOOL _useCurrentUser; + + PFUser *_lastCurrentUser; + PFACL *_defaultACLWithCurrentUser; +} + +static PFDefaultACLController *defaultController_; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)defaultController { + if (!defaultController_) { + defaultController_ = [[self alloc] init]; + } + return defaultController_; +} + ++ (void)clearDefaultController { + defaultController_ = nil; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _taskQueue = [[PFAsyncTaskQueue alloc] init]; + + return self; +} + +///-------------------------------------- +#pragma mark - ACL +///-------------------------------------- + +- (BFTask *)getDefaultACLAsync { + return [_taskQueue enqueue:^id(BFTask *task) { + if (!_defaultACL || !_useCurrentUser) { + return _defaultACL; + } + + PFCurrentUserController *currentUserController = [Parse _currentManager].coreManager.currentUserController; + return [[currentUserController getCurrentObjectAsync] continueWithBlock:^id(BFTask *task) { + PFUser *currentUser = task.result; + if (!currentUser) { + return _defaultACL; + } + + if (currentUser != _lastCurrentUser) { + _defaultACLWithCurrentUser = [_defaultACL createUnsharedCopy]; + [_defaultACLWithCurrentUser setShared:YES]; + [_defaultACLWithCurrentUser setReadAccess:YES forUser:currentUser]; + [_defaultACLWithCurrentUser setWriteAccess:YES forUser:currentUser]; + _lastCurrentUser = currentUser; + } + return _defaultACLWithCurrentUser; + }]; + }]; +} + +- (BFTask *)setDefaultACLAsync:(PFACL *)acl withCurrentUserAccess:(BOOL)accessForCurrentUser { + return [_taskQueue enqueue:^id(BFTask *task) { + _defaultACLWithCurrentUser = nil; + _lastCurrentUser = nil; + + _defaultACL = [acl createUnsharedCopy]; + [_defaultACL setShared:YES]; + + _useCurrentUser = accessForCurrentUser; + + return _defaultACL; + }]; +} + +@end diff --git a/Parse/Internal/ACL/PFACLPrivate.h b/Parse/Internal/ACL/PFACLPrivate.h new file mode 100644 index 000000000..6a641d679 --- /dev/null +++ b/Parse/Internal/ACL/PFACLPrivate.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFACL.h" + +@class PFUser; + +@interface PFACL (Private) + +// Internal commands + +/* + Gets the encoded format for an ACL. + */ +- (NSDictionary *)encodeIntoDictionary; + +/* + Creates a new ACL object from an existing dictionary. + */ +- (instancetype)initWithDictionary:(NSDictionary *)dictionary; + +/*! + Creates an ACL from its encoded format. + */ ++ (instancetype)ACLWithDictionary:(NSDictionary *)dictionary; + +- (void)setShared:(BOOL)shared; +- (BOOL)isShared; +- (instancetype)createUnsharedCopy; +- (BOOL)hasUnresolvedUser; + ++ (instancetype)defaultACL; + +@end diff --git a/Parse/Internal/ACL/State/PFACLState.h b/Parse/Internal/ACL/State/PFACLState.h new file mode 100644 index 000000000..90e06aff0 --- /dev/null +++ b/Parse/Internal/ACL/State/PFACLState.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFBaseState.h" + +NS_ASSUME_NONNULL_BEGIN + +@class PFMutableACLState; + +typedef void (^PFACLStateMutationBlock)(PFMutableACLState *); + +@interface PFACLState : PFBaseState + +@property (nonatomic, copy, readonly) NSDictionary *permissions; +@property (nonatomic, assign, readonly, getter=isShared) BOOL shared; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithState:(PFACLState *)otherState NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithState:(PFACLState *)otherState mutatingBlock:(PFACLStateMutationBlock)mutatingBlock; + ++ (instancetype)stateWithState:(PFACLState *)otherState; ++ (instancetype)stateWithState:(PFACLState *)otherState mutatingBlock:(PFACLStateMutationBlock)mutatingBlock; + +///-------------------------------------- +/// @name Mutating +///-------------------------------------- + +- (instancetype)copyByMutatingWithBlock:(PFACLStateMutationBlock)mutatingBlock NS_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/ACL/State/PFACLState.m b/Parse/Internal/ACL/State/PFACLState.m new file mode 100644 index 000000000..a80649e34 --- /dev/null +++ b/Parse/Internal/ACL/State/PFACLState.m @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFACLState_Private.h" + +#import "PFMutableACLState.h" + +@implementation PFACLState + +///-------------------------------------- +#pragma mark - PFBaseStateSubclass +///-------------------------------------- + ++ (NSDictionary *)propertyAttributes { + return @{ + @"permissions": [PFPropertyAttributes attributes], + @"shared": [PFPropertyAttributes attributes], + }; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _permissions = [NSDictionary dictionary]; + _shared = NO; + + return self; +} + +- (instancetype)initWithState:(PFACLState *)otherState { + return [super initWithState:otherState]; +} + +- (instancetype)initWithState:(PFACLState *)otherState mutatingBlock:(PFACLStateMutationBlock)mutatingBlock { + self = [self initWithState:otherState]; + if (!self) return nil; + + // Make permissions mutable for the duration of the block. + _permissions = [_permissions mutableCopy]; + + mutatingBlock((PFMutableACLState *)self); + + _permissions = [_permissions copy]; + + return self; +} + ++ (instancetype)stateWithState:(PFACLState *)otherState { + return [super stateWithState:otherState]; +} + ++ (instancetype)stateWithState:(PFACLState *)otherState mutatingBlock:(PFACLStateMutationBlock)mutatingBlock { + return [[self alloc] initWithState:otherState mutatingBlock:mutatingBlock]; +} + +///-------------------------------------- +#pragma mark - Copying +///-------------------------------------- + +- (instancetype)copyWithZone:(NSZone *)zone { + return [[PFACLState allocWithZone:zone] initWithState:self]; +} + +- (instancetype)mutableCopyWithZone:(NSZone *)zone { + return [[PFMutableACLState allocWithZone:zone] initWithState:self]; +} + +///-------------------------------------- +#pragma mark - Mutating +///-------------------------------------- + +- (instancetype)copyByMutatingWithBlock:(PFACLStateMutationBlock)mutationsBlock { + return [[PFACLState alloc] initWithState:self mutatingBlock:mutationsBlock]; +} + +@end diff --git a/Parse/Internal/ACL/State/PFACLState_Private.h b/Parse/Internal/ACL/State/PFACLState_Private.h new file mode 100644 index 000000000..a3ed153b1 --- /dev/null +++ b/Parse/Internal/ACL/State/PFACLState_Private.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFACLState.h" + +@interface PFACLState () { +@protected + NSDictionary *_permissions; + BOOL _shared; +} + +@property (nonatomic, copy, readwrite) NSDictionary *permissions; +@property (nonatomic, assign, readwrite, getter=isShared) BOOL shared; + +@end diff --git a/Parse/Internal/ACL/State/PFMutableACLState.h b/Parse/Internal/ACL/State/PFMutableACLState.h new file mode 100644 index 000000000..d2bda0cfc --- /dev/null +++ b/Parse/Internal/ACL/State/PFMutableACLState.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFACLState.h" + +@interface PFMutableACLState : PFACLState + +@property (nonatomic, copy, readwrite) NSMutableDictionary *permissions; +@property (nonatomic, assign, readwrite, getter=isShared) BOOL shared; + +@end diff --git a/Parse/Internal/ACL/State/PFMutableACLState.m b/Parse/Internal/ACL/State/PFMutableACLState.m new file mode 100644 index 000000000..5361b8d14 --- /dev/null +++ b/Parse/Internal/ACL/State/PFMutableACLState.m @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableACLState.h" + +#import "PFACLState_Private.h" + +@implementation PFMutableACLState + +@dynamic permissions; +@dynamic shared; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _permissions = [NSMutableDictionary dictionary]; + + return self; +} + +@end diff --git a/Parse/Internal/Analytics/Controller/PFAnalyticsController.h b/Parse/Internal/Analytics/Controller/PFAnalyticsController.h new file mode 100644 index 000000000..02560dc72 --- /dev/null +++ b/Parse/Internal/Analytics/Controller/PFAnalyticsController.h @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFEventuallyQueue; + +@interface PFAnalyticsController : NSObject + +@property (nonatomic, strong, readonly) PFEventuallyQueue *eventuallyQueue; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithEventuallyQueue:(PFEventuallyQueue *)eventuallyQueue NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithEventuallyQueue:(PFEventuallyQueue *)eventuallyQueue; + +///-------------------------------------- +/// @name Track Event +///-------------------------------------- + +/*! + @abstract Tracks this application being launched. If this happened as the result of the + user opening a push notification, this method sends along information to + correlate this open with that push. + + @param payload The Remote Notification payload. + @param sessionToken Current user session token. + + @returns `BFTask` with result set to `@YES`. + */ +- (BFTask *)trackAppOpenedEventAsyncWithRemoteNotificationPayload:(NSDictionary *)payload + sessionToken:(NSString *)sessionToken; + +/*! + @abstract Tracks the occurrence of a custom event with additional dimensions. + + @param name Event name. + @param dimensions `NSDictionary` of information by which to segment this event. + @param sessionToken Current user session token. + + @returns `BFTask` with result set to `@YES`. + */ +- (BFTask *)trackEventAsyncWithName:(NSString *)name + dimensions:(NSDictionary *)dimensions + sessionToken:(NSString *)sessionToken; + +@end diff --git a/Parse/Internal/Analytics/Controller/PFAnalyticsController.m b/Parse/Internal/Analytics/Controller/PFAnalyticsController.m new file mode 100644 index 000000000..9eabca3b9 --- /dev/null +++ b/Parse/Internal/Analytics/Controller/PFAnalyticsController.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAnalyticsController.h" + +#import "BFTask+Private.h" +#import "PFAnalyticsUtilities.h" +#import "PFAssert.h" +#import "PFEventuallyQueue.h" +#import "PFRESTAnalyticsCommand.h" + +@implementation PFAnalyticsController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithEventuallyQueue:(PFEventuallyQueue *)eventuallyQueue { + self = [super init]; + if (!self) return nil; + + _eventuallyQueue = eventuallyQueue; + + return self; +} + ++ (instancetype)controllerWithEventuallyQueue:(PFEventuallyQueue *)eventuallyQueue { + return [[self alloc] initWithEventuallyQueue:eventuallyQueue]; +} + +///-------------------------------------- +#pragma mark - Track Event +///-------------------------------------- + +- (BFTask *)trackAppOpenedEventAsyncWithRemoteNotificationPayload:(NSDictionary *)payload + sessionToken:(NSString *)sessionToken { + // If the Remote Notification payload had a message sent along with it, make + // sure to send that along so the server can identify "app opened from push" + // instead. + id alert = payload[@"aps"][@"alert"]; + NSString *pushDigest = (alert ? [PFAnalyticsUtilities md5DigestFromPushPayload:alert] : nil); + + PFRESTCommand *command = [PFRESTAnalyticsCommand trackAppOpenedEventCommandWithPushHash:pushDigest + sessionToken:sessionToken]; + return [[self.eventuallyQueue enqueueCommandInBackground:command] continueWithSuccessResult:@YES]; +} + +- (BFTask *)trackEventAsyncWithName:(NSString *)name + dimensions:(NSDictionary *)dimensions + sessionToken:(NSString *)sessionToken { + PFParameterAssert([[name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length], + @"A name for the custom event must be provided."); + + if (dimensions) { + [dimensions enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + PFParameterAssert([key isKindOfClass:[NSString class]] && [obj isKindOfClass:[NSString class]], + @"trackEvent dimensions expect keys and values of type NSString."); + }]; + } + + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + NSDictionary *encodedDimensions = [[PFNoObjectEncoder objectEncoder] encodeObject:dimensions]; + PFRESTCommand *command = [PFRESTAnalyticsCommand trackEventCommandWithEventName:name + dimensions:encodedDimensions + sessionToken:sessionToken]; + return [[self.eventuallyQueue enqueueCommandInBackground:command] continueWithSuccessResult:@YES]; + }]; +} + +@end diff --git a/Parse/Internal/Analytics/PFAnalytics_Private.h b/Parse/Internal/Analytics/PFAnalytics_Private.h new file mode 100644 index 000000000..240335738 --- /dev/null +++ b/Parse/Internal/Analytics/PFAnalytics_Private.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +/*! + Predefined events - AppOpened, CrashReport + Coming soon - Log, ... + */ +extern NSString *const PFAnalyticsEventAppOpened; +extern NSString *const PFAnalyticsEventCrashReport; diff --git a/Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.h b/Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.h new file mode 100644 index 000000000..69f581f9d --- /dev/null +++ b/Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFAnalyticsUtilities : NSObject + +/*! + Serializes and hexdigests an alert payload into a "push_hash" identifier + for use in Analytics. + Limitedly flexible - the payload is the value under the "alert" key in the + "aps" hash of a remote notification, so we can reasonably assume that the + complexity of its structure is limited to that accepted by Apple (in its + "The Notification Payload" docs) + + @param payload `alert` value from a push notification. + + @returns md5 identifier. + */ ++ (NSString *)md5DigestFromPushPayload:(id)payload; + +@end diff --git a/Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.m b/Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.m new file mode 100644 index 000000000..9107d1873 --- /dev/null +++ b/Parse/Internal/Analytics/Utilities/PFAnalyticsUtilities.m @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAnalyticsUtilities.h" + +#import "PFHash.h" + +@implementation PFAnalyticsUtilities + ++ (NSString *)md5DigestFromPushPayload:(id)payload { + if (!payload || payload == [NSNull null]) { + payload = @""; + } else if ([payload isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = payload; + NSArray *keys = [[dict allKeys] sortedArrayUsingSelector:@selector(compare:)]; + NSMutableArray *components = [NSMutableArray arrayWithCapacity:[dict count] * 2]; + [keys enumerateObjectsUsingBlock:^(id key, NSUInteger idx, BOOL *stop) { + [components addObject:key]; + + // alert[@"loc-args"] can be an NSArray + id value = [dict objectForKey:key]; + if ([value isKindOfClass:[NSArray class]]) { + value = [value componentsJoinedByString:@""]; + } + [components addObject:value]; + }]; + payload = [components componentsJoinedByString:@""]; + } + return PFMD5HashFromString([payload description]); +} + +@end diff --git a/Parse/Internal/BFTask+Private.h b/Parse/Internal/BFTask+Private.h new file mode 100644 index 000000000..0d01c8225 --- /dev/null +++ b/Parse/Internal/BFTask+Private.h @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import "PFInternalUtils.h" + +@interface BFExecutor (Background) + ++ (instancetype)defaultPriorityBackgroundExecutor; + +@end + +@interface BFTask (Private) + +- (instancetype)continueAsyncWithBlock:(BFContinuationBlock)block; +- (instancetype)continueAsyncWithSuccessBlock:(BFContinuationBlock)block; + +- (instancetype)continueWithResult:(id)result; +- (instancetype)continueWithSuccessResult:(id)result; + +- (instancetype)continueWithMainThreadResultBlock:(PFIdResultBlock)resultBlock + executeIfCancelled:(BOOL)executeIfCancelled; +- (instancetype)continueWithMainThreadBooleanResultBlock:(PFBooleanResultBlock)resultBlock + executeIfCancelled:(BOOL)executeIfCancelled; + +/*! + Adds a continuation to the task that will run the given block on the main + thread sometime after this task has finished. If the task was cancelled, + the block will never be called. If the task had an exception, the exception + will be throw on the main thread instead of running the block. Otherwise, + the block will be given the result and error of this task. + @returns A new task that will be finished once the block has run. + */ +- (BFTask *)thenCallBackOnMainThreadAsync:(void(^)(id result, NSError *error))block; + +/*! + Identical to thenCallBackOnMainThreadAsync:, except that the result of a successful + task will be converted to a BOOL using the boolValue method, and that will + be passed to the block instead of the original result. + */ +- (BFTask *)thenCallBackOnMainThreadWithBoolValueAsync:(void(^)(BOOL result, NSError *error))block; + +/*! + Same as `waitForResult:error withMainThreadWarning:YES` + */ +- (id)waitForResult:(NSError **)error; + +/*! + Waits until this operation is completed, then returns its value. + This method is inefficient and consumes a thread resource while its running. + + @param error If an error occurs, upon return contains an `NSError` object that describes the problem. + @param warningEnabled `BOOL` value that + + @return Returns a `self.result` if task completed. `nil` - if cancelled. + */ +- (id)waitForResult:(NSError **)error withMainThreadWarning:(BOOL)warningEnabled; + +@end + +extern void forceLoadCategory_BFTask_Private(); diff --git a/Parse/Internal/BFTask+Private.m b/Parse/Internal/BFTask+Private.m new file mode 100644 index 000000000..f6d7c6c24 --- /dev/null +++ b/Parse/Internal/BFTask+Private.m @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "BFTask+Private.h" + +#import +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@implementation BFExecutor (Background) + ++ (instancetype)defaultPriorityBackgroundExecutor { + static BFExecutor *executor; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + executor = [BFExecutor executorWithDispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + }); + return executor; +} + +@end + +@implementation BFTask (Private) + +- (instancetype)continueAsyncWithBlock:(BFContinuationBlock)block { + return [self continueWithExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:block]; +} + +- (instancetype)continueAsyncWithSuccessBlock:(BFContinuationBlock)block { + return [self continueWithExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withSuccessBlock:block]; +} + +- (instancetype)continueWithResult:(id)result { + return [self continueWithBlock:^id(BFTask *task) { + return result; + }]; +} + +- (instancetype)continueWithSuccessResult:(id)result { + return [self continueWithSuccessBlock:^id(BFTask *task) { + return result; + }]; +} + +- (instancetype)continueWithMainThreadResultBlock:(PFIdResultBlock)resultBlock + executeIfCancelled:(BOOL)executeIfCancelled { + if (!resultBlock) { + return self; + } + return [self continueWithExecutor:[BFExecutor mainThreadExecutor] + withBlock:^id(BFTask *task) { + BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; + + @try { + if (self.exception) { + //TODO: (nlutsenko) Add more context, by passing a `_cmd` from the caller method + PFLogException(self.exception); + @throw self.exception; + } + + if (!self.cancelled || executeIfCancelled) { + resultBlock(self.result, self.error); + } + } @finally { + tcs.result = nil; + } + + return tcs.task; + }]; +} + +- (instancetype)continueWithMainThreadBooleanResultBlock:(PFBooleanResultBlock)resultBlock + executeIfCancelled:(BOOL)executeIfCancelled { + return [self continueWithMainThreadResultBlock:^(id object, NSError *error) { + resultBlock([object boolValue], error); + } executeIfCancelled:executeIfCancelled]; +} + +- (BFTask *)thenCallBackOnMainThreadAsync:(void(^)(id result, NSError *error))block { + return [self continueWithMainThreadResultBlock:block executeIfCancelled:NO]; +} + +- (BFTask *)thenCallBackOnMainThreadWithBoolValueAsync:(void(^)(BOOL result, NSError *error))block { + if (!block) { + return self; + } + return [self thenCallBackOnMainThreadAsync:^(id blockResult, NSError *blockError) { + block([blockResult boolValue], blockError); + }]; +} + +- (id)waitForResult:(NSError **)error { + return [self waitForResult:error withMainThreadWarning:YES]; +} + +- (id)waitForResult:(NSError **)error withMainThreadWarning:(BOOL)warningEnabled { + if (warningEnabled) { + [self waitUntilFinished]; + } else { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self continueWithBlock:^id(BFTask *task) { + dispatch_semaphore_signal(semaphore); + return nil; + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + } + if (self.cancelled) { + return nil; + } else if (self.exception) { + @throw self.exception; + } + if (self.error && error) { + *error = self.error; + } + return self.result; +} + +@end + +void forceLoadCategory_BFTask_Private() { + NSString *string = nil; + [string description]; +} diff --git a/Parse/Internal/CloudCode/PFCloudCodeController.h b/Parse/Internal/CloudCode/PFCloudCodeController.h new file mode 100644 index 000000000..4634fb737 --- /dev/null +++ b/Parse/Internal/CloudCode/PFCloudCodeController.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@protocol PFCommandRunning; + +@interface PFCloudCodeController : NSObject + +@property (nonatomic, strong, readonly) id commandRunner; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCommandRunner:(id)commandRunner NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithCommandRunner:(id)commandRunner; + +///-------------------------------------- +/// @name Cloud Functions +///-------------------------------------- + +/*! + Calls a Cloud Code function and returns a result of it's execution. + + @param functionName Function name to call. + @param parameters Parameters to pass. (can't be nil). + @param sessionToken Session token to use. + + @returns `BFTask` with a result set to a result of Cloud Function. + */ +- (BFTask *)callCloudCodeFunctionAsync:(NSString *)functionName + withParameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken; + +@end diff --git a/Parse/Internal/CloudCode/PFCloudCodeController.m b/Parse/Internal/CloudCode/PFCloudCodeController.m new file mode 100644 index 000000000..212e47ae7 --- /dev/null +++ b/Parse/Internal/CloudCode/PFCloudCodeController.m @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCloudCodeController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFDecoder.h" +#import "PFEncoder.h" +#import "PFInternalUtils.h" +#import "PFRESTCloudCommand.h" + +@implementation PFCloudCodeController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommandRunner:(id)commandRunner { + self = [super init]; + if (!self) return nil; + + _commandRunner = commandRunner; + + return self; +} + ++ (instancetype)controllerWithCommandRunner:(id)commandRunner { + return [[self alloc] initWithCommandRunner:commandRunner]; +} + +///-------------------------------------- +#pragma mark - Cloud Functions +///-------------------------------------- + +- (BFTask *)callCloudCodeFunctionAsync:(NSString *)functionName + withParameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken { + @weakify(self); + return [[[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + NSDictionary *encodedParameters = [[PFNoObjectEncoder objectEncoder] encodeObject:parameters]; + PFRESTCloudCommand *command = [PFRESTCloudCommand commandForFunction:functionName + withParameters:encodedParameters + sessionToken:sessionToken]; + return [self.commandRunner runCommandAsync:command withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + return ((PFCommandResult *)(task.result)).result[@"result"]; + }] continueWithSuccessBlock:^id(BFTask *task) { + return [[PFDecoder objectDecoder] decodeObject:task.result]; + }]; +} + +@end diff --git a/Parse/Internal/Commands/CommandRunner/PFCommandRunning.h b/Parse/Internal/Commands/CommandRunner/PFCommandRunning.h new file mode 100644 index 000000000..c698a91da --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/PFCommandRunning.h @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@class BFCancellationToken; +@class BFTask; +@class PFRESTCommand; +@protocol PFNetworkCommand; + +typedef NS_OPTIONS(NSUInteger, PFCommandRunningOptions) { + PFCommandRunningOptionRetryIfFailed = 1 << 0, +}; + +extern NSTimeInterval const PFCommandRunningDefaultRetryDelay; + +NS_ASSUME_NONNULL_BEGIN + +@protocol PFCommandRunning + +@property (nonatomic, weak, readonly) id dataSource; + +@property (nonatomic, copy, readonly) NSString *applicationId; +@property (nonatomic, copy, readonly) NSString *clientKey; + +@property (nonatomic, assign) NSTimeInterval initialRetryDelay; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithDataSource:(id)dataSource + applicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey; ++ (instancetype)commandRunnerWithDataSource:(id)dataSource + applicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey; + +///-------------------------------------- +/// @name Data Commands +///-------------------------------------- + +/*! + Run command. + + @param command Command to run. + @param options Options to use to run command. + + @returns `BFTask` with result set to `PFCommandResult`. + */ +- (BFTask *)runCommandAsync:(PFRESTCommand *)command + withOptions:(PFCommandRunningOptions)options; + +/*! + Run command. + + @param command Command to run. + @param options Options to use to run command. + @param cancellationToken Operation to use as a cancellation token. + + @returns `BFTask` with result set to `PFCommandResult`. + */ +- (BFTask *)runCommandAsync:(PFRESTCommand *)command + withOptions:(PFCommandRunningOptions)options + cancellationToken:(nullable BFCancellationToken *)cancellationToken; + +///-------------------------------------- +/// @name File Commands +///-------------------------------------- + +- (BFTask *)runFileUploadCommandAsync:(PFRESTCommand *)command + withContentType:(NSString *)contentType + contentSourceFilePath:(NSString *)sourceFilePath + options:(PFCommandRunningOptions)options + cancellationToken:(nullable BFCancellationToken *)cancellationToken + progressBlock:(nullable PFProgressBlock)progressBlock; + +- (BFTask *)runFileDownloadCommandAsyncWithFileURL:(NSURL *)url + targetFilePath:(NSString *)filePath + cancellationToken:(nullable BFCancellationToken *)cancellationToken + progressBlock:(nullable PFProgressBlock)progressBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/PFCommandRunning.m b/Parse/Internal/Commands/CommandRunner/PFCommandRunning.m new file mode 100644 index 000000000..6b0c49fc4 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/PFCommandRunning.m @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCommandRunning.h" + +NSTimeInterval const PFCommandRunningDefaultRetryDelay = 1.0; diff --git a/Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.h b/Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.h new file mode 100644 index 000000000..7531fde0a --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +///-------------------------------------- +/// @name Running +///-------------------------------------- + +extern uint8_t const PFCommandRunningDefaultMaxAttemptsCount; + +///-------------------------------------- +/// @name Headers +///-------------------------------------- + +extern NSString *const PFCommandHeaderNameApplicationId; +extern NSString *const PFCommandHeaderNameClientKey; +extern NSString *const PFCommandHeaderNameClientVersion; +extern NSString *const PFCommandHeaderNameInstallationId; +extern NSString *const PFCommandHeaderNameAppBuildVersion; +extern NSString *const PFCommandHeaderNameAppDisplayVersion; +extern NSString *const PFCommandHeaderNameOSVersion; +extern NSString *const PFCommandHeaderNameSessionToken; + +///-------------------------------------- +/// @name HTTP Method Override +///-------------------------------------- + +extern NSString *const PFCommandParameterNameMethodOverride; diff --git a/Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.m b/Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.m new file mode 100644 index 000000000..3ea747f49 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/PFCommandRunningConstants.m @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCommandRunningConstants.h" + +uint8_t const PFCommandRunningDefaultMaxAttemptsCount = 5; + +NSString *const PFCommandHeaderNameApplicationId = @"X-Parse-Application-Id"; +NSString *const PFCommandHeaderNameClientKey = @"X-Parse-Client-Key"; +NSString *const PFCommandHeaderNameClientVersion = @"X-Parse-Client-Version"; +NSString *const PFCommandHeaderNameInstallationId = @"X-Parse-Installation-Id"; +NSString *const PFCommandHeaderNameAppBuildVersion = @"X-Parse-App-Build-Version"; +NSString *const PFCommandHeaderNameAppDisplayVersion = @"X-Parse-App-Display-Version"; +NSString *const PFCommandHeaderNameOSVersion = @"X-Parse-OS-Version"; +NSString *const PFCommandHeaderNameSessionToken = @"X-Parse-Session-Token"; + +NSString *const PFCommandParameterNameMethodOverride = @"_method"; diff --git a/Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.h b/Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.h new file mode 100644 index 000000000..6583f5b29 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" + +@class PFRESTCommand; + +@interface PFCommandURLRequestConstructor : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)constructorWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Data +///-------------------------------------- + +- (NSURLRequest *)dataURLRequestForCommand:(PFRESTCommand *)command; + +///-------------------------------------- +/// @name File Upload +///-------------------------------------- + +- (NSURLRequest *)fileUploadURLRequestForCommand:(PFRESTCommand *)command + withContentType:(NSString *)contentType + contentSourceFilePath:(NSString *)contentFilePath; + +///-------------------------------------- +/// @name Headers +///-------------------------------------- + ++ (NSDictionary *)defaultURLRequestHeadersForApplicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey + bundle:(NSBundle *)bundle; + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.m b/Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.m new file mode 100644 index 000000000..5ade877fa --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLRequestConstructor/PFCommandURLRequestConstructor.m @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCommandURLRequestConstructor.h" + +#import "PFAssert.h" +#import "PFCommandRunningConstants.h" +#import "PFDevice.h" +#import "PFHTTPRequest.h" +#import "PFHTTPURLRequestConstructor.h" +#import "PFInstallationIdentifierStore.h" +#import "PFInternalUtils.h" +#import "PFRESTCommand.h" +#import "PFURLConstructor.h" +#import "Parse_Private.h" + +@implementation PFCommandURLRequestConstructor + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)constructorWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Data +///-------------------------------------- + +- (NSURLRequest *)dataURLRequestForCommand:(PFRESTCommand *)command { + NSURL *url = [PFURLConstructor URLFromBaseURL:[NSURL URLWithString:[PFInternalUtils parseServerURLString]] + path:[NSString stringWithFormat:@"/1/%@", command.httpPath]]; + NSDictionary *headers = [self _URLRequestHeadersForCommand:command]; + + NSString *requestMethod = command.httpMethod; + NSDictionary *requestParameters = nil; + if (command.parameters) { + NSDictionary *parameters = nil; + + // The request URI may be too long to include parameters in the URI. + // To avoid this problem we send the parameters in a POST request json-encoded body + // and add a custom parameter that overrides the method in a request. + if ([requestMethod isEqualToString:PFHTTPRequestMethodGET] || + [requestMethod isEqualToString:PFHTTPRequestMethodHEAD] || + [requestMethod isEqualToString:PFHTTPRequestMethodDELETE]) { + NSMutableDictionary *mutableParameters = [command.parameters mutableCopy]; + mutableParameters[PFCommandParameterNameMethodOverride] = command.httpMethod; + + requestMethod = PFHTTPRequestMethodPOST; + parameters = [mutableParameters copy]; + } else { + parameters = command.parameters; + } + requestParameters = [[PFPointerObjectEncoder objectEncoder] encodeObject:parameters]; + } + + return [PFHTTPURLRequestConstructor urlRequestWithURL:url + httpMethod:requestMethod + httpHeaders:headers + parameters:requestParameters]; +} + +///-------------------------------------- +#pragma mark - File +///-------------------------------------- + +- (NSURLRequest *)fileUploadURLRequestForCommand:(PFRESTCommand *)command + withContentType:(NSString *)contentType + contentSourceFilePath:(NSString *)contentFilePath { + NSMutableURLRequest *request = [[self dataURLRequestForCommand:command] mutableCopy]; + + if (contentType) { + [request setValue:contentType forHTTPHeaderField:PFHTTPRequestHeaderNameContentType]; + } + + //TODO (nlutsenko): Check for error here. + NSNumber *fileSize = [PFInternalUtils fileSizeOfFileAtPath:contentFilePath error:nil]; + [request setValue:[fileSize stringValue] forHTTPHeaderField:PFHTTPRequestHeaderNameContentLength]; + + return request; +} + +///-------------------------------------- +#pragma mark - Headers +///-------------------------------------- + ++ (NSDictionary *)defaultURLRequestHeadersForApplicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey + bundle:(NSBundle *)bundle { +#if TARGET_OS_IPHONE + NSString *versionPrefix = @"i"; +#else + NSString *versionPrefix = @"osx"; +#endif + + NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; + + mutableHeaders[PFCommandHeaderNameApplicationId] = applicationId; + mutableHeaders[PFCommandHeaderNameClientKey] = clientKey; + + mutableHeaders[PFCommandHeaderNameClientVersion] = [versionPrefix stringByAppendingString:PARSE_VERSION]; + mutableHeaders[PFCommandHeaderNameOSVersion] = [PFDevice currentDevice].operatingSystemFullVersion; + + // Bundle Version and Display Version can be null, when running tests + NSString *bundleVersion = [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; + if (bundleVersion) { + mutableHeaders[PFCommandHeaderNameAppBuildVersion] = bundleVersion; + } + NSString *displayVersion = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if (displayVersion) { + mutableHeaders[PFCommandHeaderNameAppDisplayVersion] = displayVersion; + } + + return [mutableHeaders copy]; +} + +- (NSDictionary *)_URLRequestHeadersForCommand:(PFRESTCommand *)command { + NSMutableDictionary *headers = [NSMutableDictionary dictionary]; + [headers addEntriesFromDictionary:command.additionalRequestHeaders]; + PFInstallationIdentifierStore *installationIdentifierStore = self.dataSource.installationIdentifierStore; + headers[PFCommandHeaderNameInstallationId] = installationIdentifierStore.installationIdentifier; + if (command.sessionToken) { + headers[PFCommandHeaderNameSessionToken] = command.sessionToken; + } + return [headers copy]; +} + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.h b/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.h new file mode 100644 index 000000000..7f75dfd56 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCommandRunning.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSessionCommandRunner : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.m b/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.m new file mode 100644 index 000000000..32ce9022e --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner.m @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionCommandRunner.h" +#import "PFURLSessionCommandRunner_Private.h" + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunningConstants.h" +#import "PFCommandURLRequestConstructor.h" +#import "PFConstants.h" +#import "PFDevice.h" +#import "PFEncoder.h" +#import "PFHTTPRequest.h" +#import "PFHTTPURLRequestConstructor.h" +#import "PFInstallationIdentifierStore.h" +#import "PFInternalUtils.h" +#import "PFLogging.h" +#import "PFMacros.h" +#import "PFRESTCommand.h" +#import "PFURLConstructor.h" +#import "PFURLSession.h" + +@implementation PFURLSessionCommandRunner + +@synthesize applicationId = _applicationId; +@synthesize clientKey = _clientKey; +@synthesize initialRetryDelay = _initialRetryDelay; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource + applicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey { + NSURLSessionConfiguration *configuration = [[self class] _urlSessionConfigurationForApplicationId:applicationId + clientKey:clientKey]; + PFURLSession *session = [PFURLSession sessionWithConfiguration:configuration]; + PFCommandURLRequestConstructor *constructor = [PFCommandURLRequestConstructor constructorWithDataSource:dataSource]; + self = [self initWithDataSource:dataSource + session:session + requestConstructor:constructor]; + if (!self) return nil; + + _applicationId = [applicationId copy]; + _clientKey = [clientKey copy]; + + return self; +} + +- (instancetype)initWithDataSource:(id)dataSource + session:(PFURLSession *)session + requestConstructor:(PFCommandURLRequestConstructor *)requestConstructor { + self = [super init]; + if (!self) return nil; + + _initialRetryDelay = PFCommandRunningDefaultRetryDelay; + + _requestConstructor = requestConstructor; + _session = session; + + return self; +} + ++ (instancetype)commandRunnerWithDataSource:(id)dataSource + applicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey { + return [[self alloc] initWithDataSource:dataSource applicationId:applicationId clientKey:clientKey]; +} + +///-------------------------------------- +#pragma mark - Dealloc +///-------------------------------------- + +- (void)dealloc { + // This is required to call, since session will continue to be present in memory and running otherwise. + [_session invalidateAndCancel]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (id)dataSource { + return _requestConstructor.dataSource; +} + +///-------------------------------------- +#pragma mark - Data Commands +///-------------------------------------- + +- (BFTask *)runCommandAsync:(PFRESTCommand *)command withOptions:(PFCommandRunningOptions)options { + return [self runCommandAsync:command withOptions:options cancellationToken:nil]; +} + +- (BFTask *)runCommandAsync:(PFRESTCommand *)command + withOptions:(PFCommandRunningOptions)options + cancellationToken:(BFCancellationToken *)cancellationToken { + return [self _performCommandRunningBlock:^id{ + [command resolveLocalIds]; + NSURLRequest *request = [self.requestConstructor dataURLRequestForCommand:command]; + return [_session performDataURLRequestAsync:request forCommand:command cancellationToken:cancellationToken]; + } withOptions:options cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - File Commands +///-------------------------------------- + +- (BFTask *)runFileUploadCommandAsync:(PFRESTCommand *)command + withContentType:(NSString *)contentType + contentSourceFilePath:(NSString *)sourceFilePath + options:(PFCommandRunningOptions)options + cancellationToken:(nullable BFCancellationToken *)cancellationToken + progressBlock:(nullable PFProgressBlock)progressBlock { + @weakify(self); + return [self _performCommandRunningBlock:^id{ + @strongify(self); + + [command resolveLocalIds]; + NSURLRequest *request = [self.requestConstructor fileUploadURLRequestForCommand:command + withContentType:contentType + contentSourceFilePath:sourceFilePath]; + return [_session performFileUploadURLRequestAsync:request + forCommand:command + withContentSourceFilePath:sourceFilePath + cancellationToken:cancellationToken + progressBlock:progressBlock]; + + } withOptions:options cancellationToken:cancellationToken]; +} + +- (BFTask *)runFileDownloadCommandAsyncWithFileURL:(NSURL *)url + targetFilePath:(NSString *)filePath + cancellationToken:(nullable BFCancellationToken *)cancellationToken + progressBlock:(nullable PFProgressBlock)progressBlock { + return [self _performCommandRunningBlock:^id{ + NSURLRequest *request = [NSURLRequest requestWithURL:url]; + return [_session performFileDownloadURLRequestAsync:request + toFileAtPath:filePath + withCancellationToken:cancellationToken + progressBlock:progressBlock]; + } withOptions:PFCommandRunningOptionRetryIfFailed cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - Retrying +///-------------------------------------- + +- (BFTask *)_performCommandRunningBlock:(nonnull id (^)())block + withOptions:(PFCommandRunningOptions)options + cancellationToken:(BFCancellationToken *)cancellationToken { + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + if (!(options & PFCommandRunningOptionRetryIfFailed)) { + return block(); + } + + NSTimeInterval delay = self.initialRetryDelay; // Delay (secs) of next retry attempt + + // Set the initial delay to something between 1 and 2 seconds. We want it to be + // random so that clients that fail simultaneously don't retry on simultaneous + // intervals. + delay += self.initialRetryDelay * ((double)(arc4random() & 0x0FFFF) / (double)0x0FFFF); + return [self _performCommandRunningBlock:block + withCancellationToken:cancellationToken + delay:delay + forAttempts:PFCommandRunningDefaultMaxAttemptsCount]; +} + +- (BFTask *)_performCommandRunningBlock:(nonnull id (^)())block + withCancellationToken:(BFCancellationToken *)cancellationToken + delay:(NSTimeInterval)delay + forAttempts:(NSUInteger)attempts { + @weakify(self); + return [block() continueWithBlock:^id(BFTask *task) { + @strongify(self); + if (task.cancelled) { + return task; + } + + if ([[task.error userInfo][@"temporary"] boolValue] && attempts > 1) { + PFLogError(PFLoggingTagCommon, + @"Network connection failed. Making attempt %lu after sleeping for %f seconds.", + (unsigned long)(PFCommandRunningDefaultMaxAttemptsCount - attempts + 1), (double)delay); + + return [[BFTask taskWithDelay:(int)(delay * 1000)] continueWithBlock:^id(BFTask *task) { + return [self _performCommandRunningBlock:block + withCancellationToken:cancellationToken + delay:delay * 2.0 + forAttempts:attempts - 1]; + } cancellationToken:cancellationToken]; + } + return task; + } cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - NSURLSessionConfiguration +///-------------------------------------- + ++ (NSURLSessionConfiguration *)_urlSessionConfigurationForApplicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + + // No cookies, they are bad for you. + configuration.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever; + configuration.HTTPShouldSetCookies = NO; + + // Completely disable caching of responses for security reasons. + configuration.URLCache = [[NSURLCache alloc] initWithMemoryCapacity:[NSURLCache sharedURLCache].memoryCapacity + diskCapacity:0 + diskPath:nil]; + + NSBundle *bundle = [NSBundle mainBundle]; + NSDictionary *headers = [PFCommandURLRequestConstructor defaultURLRequestHeadersForApplicationId:applicationId + clientKey:clientKey + bundle:bundle]; + configuration.HTTPAdditionalHeaders = headers; + + return configuration; +} + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner_Private.h b/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner_Private.h new file mode 100644 index 000000000..bca97674d --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/PFURLSessionCommandRunner_Private.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionCommandRunner.h" + +@class PFCommandURLRequestConstructor; +@class PFURLSession; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSessionCommandRunner () + +@property (nonatomic, strong, readonly) PFURLSession *session; +@property (nonatomic, strong, readonly) PFCommandURLRequestConstructor *requestConstructor; + +- (instancetype)initWithDataSource:(id)dataSource + session:(PFURLSession *)session + requestConstructor:(PFCommandURLRequestConstructor *)requestConstructor NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.h b/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.h new file mode 100644 index 000000000..0232cf44c --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.h @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@class BFCancellationToken; +@class BFTask; +@class PFRESTCommand; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSession : NSObject + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration NS_DESIGNATED_INITIALIZER; ++ (instancetype)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration; + +///-------------------------------------- +/// @name Teardown +///-------------------------------------- + +- (void)invalidateAndCancel; + +///-------------------------------------- +/// @name Network Requests +///-------------------------------------- + +- (BFTask *)performDataURLRequestAsync:(NSURLRequest *)request + forCommand:(PFRESTCommand *)command + cancellationToken:(nullable BFCancellationToken *)cancellationToken; + +- (BFTask *)performFileUploadURLRequestAsync:(NSURLRequest *)request + forCommand:(PFRESTCommand *)command + withContentSourceFilePath:(NSString *)sourceFilePath + cancellationToken:(nullable BFCancellationToken *)cancellationToken + progressBlock:(nullable PFProgressBlock)progressBlock; + +- (BFTask *)performFileDownloadURLRequestAsync:(NSURLRequest *)request + toFileAtPath:(NSString *)filePath + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + progressBlock:(nullable PFProgressBlock)progressBlock; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.m b/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.m new file mode 100644 index 000000000..0ac976378 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession.m @@ -0,0 +1,251 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSession.h" +#import "PFURLSession_Private.h" + +#import + +#import "BFTask+Private.h" +#import "PFCommandResult.h" +#import "PFMacros.h" +#import "PFAssert.h" +#import "PFURLSessionJSONDataTaskDelegate.h" +#import "PFURLSessionUploadTaskDelegate.h" +#import "PFURLSessionFileDownloadTaskDelegate.h" + +typedef void (^PFURLSessionTaskCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + +@interface PFURLSession () { + dispatch_queue_t _sessionTaskQueue; + NSURLSession *_urlSession; + NSMutableDictionary *_delegatesDictionary; + dispatch_queue_t _delegatesAccessQueue; +} + +@end + +@implementation PFURLSession + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration { + // NOTE: cast to id suppresses warning about designated initializer. + return [(id)self initWithURLSession:[NSURLSession sessionWithConfiguration:configuration + delegate:self + delegateQueue:nil]]; +} + +- (instancetype)initWithURLSession:(NSURLSession *)session { + self = [super init]; + if (!self) return nil; + + _urlSession = session; + + _sessionTaskQueue = dispatch_queue_create("com.parse.urlSession.tasks", DISPATCH_QUEUE_SERIAL); + + _delegatesDictionary = [NSMutableDictionary dictionary]; + _delegatesAccessQueue = dispatch_queue_create("com.parse.urlSession.delegates", DISPATCH_QUEUE_CONCURRENT); + + return self; +} + ++ (instancetype)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration { + return [[self alloc] initWithConfiguration:configuration]; +} + ++ (instancetype)sessionWithURLSession:(nonnull NSURLSession *)session { + return [[self alloc] initWithURLSession:session]; +} + +///-------------------------------------- +#pragma mark - Teardown +///-------------------------------------- + +- (void)invalidateAndCancel { + [_urlSession invalidateAndCancel]; +} + +///-------------------------------------- +#pragma mark - Network Requests +///-------------------------------------- + +- (BFTask *)performDataURLRequestAsync:(NSURLRequest *)request + forCommand:(PFRESTCommand *)command + cancellationToken:(BFCancellationToken *)cancellationToken { + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + __block NSURLSessionDataTask *task = nil; + dispatch_sync(_sessionTaskQueue, ^{ + task = [_urlSession dataTaskWithRequest:request]; + }); + PFURLSessionDataTaskDelegate *delegate = [PFURLSessionJSONDataTaskDelegate taskDelegateForDataTask:task + withCancellationToken:cancellationToken]; + return [self _performDataTask:task withDelegate:delegate]; + }]; +} + +- (BFTask *)performFileUploadURLRequestAsync:(NSURLRequest *)request + forCommand:(PFRESTCommand *)command + withContentSourceFilePath:(NSString *)sourceFilePath + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock { + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + __block NSURLSessionDataTask *task = nil; + dispatch_sync(_sessionTaskQueue, ^{ + task = [_urlSession uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:sourceFilePath]]; + }); + PFURLSessionUploadTaskDelegate *delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:task + withCancellationToken:cancellationToken + uploadProgressBlock:progressBlock]; + return [self _performDataTask:task withDelegate:delegate]; + }]; +} + +- (BFTask *)performFileDownloadURLRequestAsync:(NSURLRequest *)request + toFileAtPath:(NSString *)filePath + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + progressBlock:(nullable PFProgressBlock)progressBlock { + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + __block NSURLSessionDataTask *task = nil; + dispatch_sync(_sessionTaskQueue, ^{ + task = [_urlSession dataTaskWithRequest:request]; + }); + PFURLSessionFileDownloadTaskDelegate *delegate = [PFURLSessionFileDownloadTaskDelegate taskDelegateForDataTask:task + withCancellationToken:cancellationToken + targetFilePath:filePath + progressBlock:progressBlock]; + return [self _performDataTask:task withDelegate:delegate]; + }]; +} + +- (BFTask *)_performDataTask:(NSURLSessionDataTask *)dataTask withDelegate:(PFURLSessionDataTaskDelegate *)delegate { + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultExecutor] withBlock:^id{ + @strongify(self); + NSNumber *taskIdentifier = @(dataTask.taskIdentifier); + [self setDelegate:delegate forDataTask:dataTask]; + + BFTask *resultTask = [delegate.resultTask continueWithBlock:^id(BFTask *task) { + @strongify(self); + [self _removeDelegateForTaskWithIdentifier:taskIdentifier]; + return task; + }]; + [dataTask resume]; + + return resultTask; + }]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (PFURLSessionDataTaskDelegate *)_taskDelegateForDataTask:(NSURLSessionDataTask *)task { + __block PFURLSessionDataTaskDelegate *delegate = nil; + dispatch_sync(_delegatesAccessQueue, ^{ + delegate = _delegatesDictionary[@(task.taskIdentifier)]; + }); + return delegate; +} + +- (void)setDelegate:(PFURLSessionDataTaskDelegate *)delegate forDataTask:(NSURLSessionDataTask *)task { + dispatch_barrier_async(_delegatesAccessQueue, ^{ + _delegatesDictionary[@(task.taskIdentifier)] = delegate; + }); +} + +- (void)_removeDelegateForTaskWithIdentifier:(NSNumber *)identifier { + dispatch_barrier_async(_delegatesAccessQueue, ^{ + [_delegatesDictionary removeObjectForKey:identifier]; + }); +} + +///-------------------------------------- +#pragma mark - NSURLSessionTaskDelegate +///-------------------------------------- + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionDataTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent +totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { + PFURLSessionDataTaskDelegate *delegate = [self _taskDelegateForDataTask:task]; + [delegate URLSession:session + task:task + didSendBodyData:bytesSent + totalBytesSent:totalBytesSent +totalBytesExpectedToSend:totalBytesExpectedToSend]; +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionDataTask *)task didCompleteWithError:(NSError *)error { + PFURLSessionDataTaskDelegate *delegate = [self _taskDelegateForDataTask:task]; + [delegate URLSession:session task:task didCompleteWithError:error]; +} + +///-------------------------------------- +#pragma mark - NSURLSessionDataDelegate +///-------------------------------------- + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { + PFURLSessionDataTaskDelegate *delegate = [self _taskDelegateForDataTask:dataTask]; + [delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler]; +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { + PFURLSessionDataTaskDelegate *delegate = [self _taskDelegateForDataTask:dataTask]; + [delegate URLSession:session dataTask:dataTask didReceiveData:data]; +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + willCacheResponse:(NSCachedURLResponse *)proposedResponse + completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { + completionHandler(nil); // Prevent any caching for security reasons +} + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession_Private.h b/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession_Private.h new file mode 100644 index 000000000..e7068256f --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/PFURLSession_Private.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSession.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSession () + +- (instancetype)initWithURLSession:(NSURLSession *)session NS_DESIGNATED_INITIALIZER; + ++ (instancetype)sessionWithURLSession:(NSURLSession *)session; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.h b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.h new file mode 100644 index 000000000..af9a37238 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFCancellationToken; +@class BFTask; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSessionDataTaskDelegate : NSObject + +@property (nonatomic, strong, readonly) NSURLSessionDataTask *dataTask; +@property (nonatomic, strong, readonly) BFTask *resultTask; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken NS_DESIGNATED_INITIALIZER; + ++ (instancetype)taskDelegateForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.m b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.m new file mode 100644 index 000000000..f81e9d6b9 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate.m @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionDataTaskDelegate.h" +#import "PFURLSessionDataTaskDelegate_Private.h" + +#import +#import + +#import "PFAssert.h" +#import "PFMacros.h" + +@interface PFURLSessionDataTaskDelegate () { + BFTaskCompletionSource *_taskCompletionSource; +} + +@end + +@implementation PFURLSessionDataTaskDelegate + +@synthesize dataOutputStream = _dataOutputStream; +@synthesize downloadedBytes = _downloadedBytes; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(BFCancellationToken *)cancellationToken { + self = [super init]; + if (!self) return nil; + + _taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + + _dataTask = dataTask; + @weakify(self); + [cancellationToken registerCancellationObserverWithBlock:^{ + @strongify(self); + [self _cancel]; + }]; + + return self; +} + ++ (instancetype)taskDelegateForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken { + return [[self alloc] initForDataTask:dataTask withCancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (BFTask *)resultTask { + return _taskCompletionSource.task; +} + +- (NSOutputStream *)dataOutputStream { + if (!_dataOutputStream) { + _dataOutputStream = [NSOutputStream outputStreamToMemory]; + } + return _dataOutputStream; +} + +///-------------------------------------- +#pragma mark - Task +///-------------------------------------- + +- (void)_taskDidFinish { + [self _closeDataOutputStream]; + if (self.error) { + [_taskCompletionSource trySetError:self.error]; + } else { + [_taskCompletionSource trySetResult:self.result]; + } +} + +- (void)_taskDidCancel { + [self _closeDataOutputStream]; + [_taskCompletionSource trySetCancelled]; +} + +- (void)_cancel { + [self.dataTask cancel]; +} + +///-------------------------------------- +#pragma mark - Stream +///-------------------------------------- + +- (void)_openDataOutputStream { + [self.dataOutputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [self.dataOutputStream open]; +} + +- (void)_writeDataOutputStreamData:(NSData *)data { + NSInteger length = [data length]; + while (YES) { + NSInteger bytesWritten = 0; + if ([self.dataOutputStream hasSpaceAvailable]) { + const uint8_t *dataBuffer = (uint8_t *)[data bytes]; + + NSInteger numberOfBytesWritten = 0; + while (bytesWritten < length) { + numberOfBytesWritten = [self.dataOutputStream write:&dataBuffer[bytesWritten] + maxLength:(length - bytesWritten)]; + if (numberOfBytesWritten == -1) { + break; + } + + bytesWritten += numberOfBytesWritten; + } + break; + } + + if (self.dataOutputStream.streamError) { + [self.dataTask cancel]; + self.error = self.dataOutputStream.streamError; + // Don't finish the delegate here, as we will finish when NSURLSessionTask calls back about cancellation. + return; + } + } + _downloadedBytes += length; +} + +- (void)_closeDataOutputStream { + [self.dataOutputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [self.dataOutputStream close]; +} + +///-------------------------------------- +#pragma mark - NSURLSessionTaskDelegate +///-------------------------------------- + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent +totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { + // No-op, we don't care about progress here. +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { + if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { + [self _taskDidCancel]; + } else { + self.error = self.error ?: error; + [self _taskDidFinish]; + } +} + +///-------------------------------------- +#pragma mark - NSURLSessionDataDelegate +///-------------------------------------- + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { + _response = (NSHTTPURLResponse *)response; + [self _openDataOutputStream]; + + completionHandler(NSURLSessionResponseAllow); +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { + [self _writeDataOutputStreamData:data]; +} + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate_Private.h b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate_Private.h new file mode 100644 index 000000000..bd2d1c963 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionDataTaskDelegate_Private.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionDataTaskDelegate.h" + +@interface PFURLSessionDataTaskDelegate () + +@property (nonatomic, strong, readonly) dispatch_queue_t dataQueue; + +@property (nonatomic, strong, readonly) NSHTTPURLResponse *response; + +/*! + @abstract Defaults to to-memory output stream if not overwritten. + */ +@property (nonatomic, strong, readonly) NSOutputStream *dataOutputStream; +@property (nonatomic, assign, readonly) uint64_t downloadedBytes; + +@property (nonatomic, strong) id result; +@property (nonatomic, strong) NSError *error; + +- (void)_taskDidFinish NS_REQUIRES_SUPER; +- (void)_taskDidCancel NS_REQUIRES_SUPER; + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.h b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.h new file mode 100644 index 000000000..5a791d949 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionDataTaskDelegate.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSessionFileDownloadTaskDelegate : PFURLSessionDataTaskDelegate + +@property (nonatomic, copy, readonly) NSString *targetFilePath; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + targetFilePath:(NSString *)targetFilePath + progressBlock:(nullable PFProgressBlock)progressBlock; ++ (instancetype)taskDelegateForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + targetFilePath:(NSString *)targetFilePath + progressBlock:(nullable PFProgressBlock)progressBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.m b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.m new file mode 100644 index 000000000..799e67980 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionFileDownloadTaskDelegate.m @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionFileDownloadTaskDelegate.h" + +#import "PFErrorUtilities.h" +#import "PFHash.h" +#import "PFURLSessionDataTaskDelegate_Private.h" + +@interface PFURLSessionFileDownloadTaskDelegate () { + NSOutputStream *_fileDataOutputStream; + PFProgressBlock _progressBlock; +} + +@end + +@implementation PFURLSessionFileDownloadTaskDelegate + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(BFCancellationToken *)cancellationToken + targetFilePath:(NSString *)targetFilePath + progressBlock:(PFProgressBlock)progressBlock { + self = [super initForDataTask:dataTask withCancellationToken:cancellationToken]; + if (!self) return nil; + + _targetFilePath = targetFilePath; + _fileDataOutputStream = [NSOutputStream outputStreamToFileAtPath:_targetFilePath append:NO]; + _progressBlock = progressBlock; + + return self; +} + ++ (instancetype)taskDelegateForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(BFCancellationToken *)cancellationToken + targetFilePath:(NSString *)targetFilePath + progressBlock:(PFProgressBlock)progressBlock { + return [[self alloc] initForDataTask:dataTask + withCancellationToken:cancellationToken + targetFilePath:targetFilePath + progressBlock:progressBlock]; +} + +///-------------------------------------- +#pragma mark - Progress +///-------------------------------------- + +- (void)_reportProgress { + if (!_progressBlock) { + return; + } + + int progress = (int)(self.downloadedBytes / (CGFloat)self.response.expectedContentLength * 100); + _progressBlock(progress); +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (NSOutputStream *)dataOutputStream { + return _fileDataOutputStream; +} + +///-------------------------------------- +#pragma mark - Task +///-------------------------------------- + +- (void)_taskDidFinish { + if (self.error) { + // TODO: (nlutsenko) Unify this with code from PFURLSessionJSONDataTaskDelegate + NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary]; + errorDictionary[@"code"] = @(kPFErrorConnectionFailed); + errorDictionary[@"error"] = [self.error localizedDescription]; + errorDictionary[@"originalError"] = self.error; + errorDictionary[@"temporary"] = @(self.response.statusCode >= 500 || self.response.statusCode < 400); + self.error = [PFErrorUtilities errorFromResult:errorDictionary]; + } + [super _taskDidFinish]; +} + +///-------------------------------------- +#pragma mark - NSURLSessionDataDelegate +///-------------------------------------- + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { + [super URLSession:session dataTask:dataTask didReceiveData:data]; + [self _reportProgress]; +} + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.h b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.h new file mode 100644 index 000000000..66a06d1d0 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFURLSessionDataTaskDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSessionJSONDataTaskDelegate : PFURLSessionDataTaskDelegate + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.m b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.m new file mode 100644 index 000000000..4fbc9d574 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionJSONDataTaskDelegate.m @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionJSONDataTaskDelegate.h" + +#import +#import +#import + +#import "PFCommandResult.h" +#import "PFConstants.h" +#import "PFErrorUtilities.h" +#import "PFMacros.h" +#import "PFURLSessionDataTaskDelegate_Private.h" + +@interface PFURLSessionJSONDataTaskDelegate () + +@end + +@implementation PFURLSessionJSONDataTaskDelegate + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (void)_taskDidFinish { + NSData *data = [self.dataOutputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]; + + NSString *resultString = nil; + id result = nil; + + NSError *jsonError = nil; + if (data) { + resultString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + result = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&jsonError]; + + if (jsonError && !self.error) { + self.error = jsonError; + [super _taskDidFinish]; + return; + } + } + + if (self.error) { + NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionary]; + errorDictionary[@"code"] = @(kPFErrorConnectionFailed); + errorDictionary[@"error"] = [self.error localizedDescription]; + errorDictionary[@"originalError"] = self.error; + errorDictionary[@"temporary"] = @(self.response.statusCode >= 500 || self.response.statusCode < 400); + self.error = [PFErrorUtilities errorFromResult:errorDictionary]; + [super _taskDidFinish]; + return; + } + + if ([result isKindOfClass:[NSDictionary class]]) { + NSDictionary *resultDictionary = (NSDictionary *)result; + if (resultDictionary[@"error"]) { + NSMutableDictionary *errorDictionary = [NSMutableDictionary dictionaryWithDictionary:resultDictionary]; + errorDictionary[@"temporary"] = @(self.response.statusCode >= 500 || self.response.statusCode < 400); + self.error = [PFErrorUtilities errorFromResult:errorDictionary]; + [super _taskDidFinish]; + return; + } + } + + PFCommandResult *commandResult = [PFCommandResult commandResultWithResult:result + resultString:resultString + httpResponse:self.response]; + self.result = commandResult; + [super _taskDidFinish]; +} + +@end diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.h b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.h new file mode 100644 index 000000000..22667ebaf --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFURLSessionJSONDataTaskDelegate.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface PFURLSessionUploadTaskDelegate : PFURLSessionJSONDataTaskDelegate + +- (instancetype)initForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + uploadProgressBlock:(nullable PFProgressBlock)progressBlock; ++ (instancetype)taskDelegateForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + uploadProgressBlock:(nullable PFProgressBlock)progressBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.m b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.m new file mode 100644 index 000000000..8cf969823 --- /dev/null +++ b/Parse/Internal/Commands/CommandRunner/URLSession/Session/TaskDelegate/PFURLSessionUploadTaskDelegate.m @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLSessionUploadTaskDelegate.h" + +@implementation PFURLSessionUploadTaskDelegate { + __nullable PFProgressBlock _progressBlock; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + uploadProgressBlock:(nullable PFProgressBlock)progressBlock { + self = [self initForDataTask:dataTask withCancellationToken:cancellationToken]; + if (!self) return nil; + + _progressBlock = [progressBlock copy]; + + return self; +} + ++ (instancetype)taskDelegateForDataTask:(NSURLSessionDataTask *)dataTask + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + uploadProgressBlock:(nullable PFProgressBlock)progressBlock { + return [[self alloc] initForDataTask:dataTask + withCancellationToken:cancellationToken + uploadProgressBlock:progressBlock]; +} + +///-------------------------------------- +#pragma mark - NSURLSessionTaskDelegate +///-------------------------------------- + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent +totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { + int progress = (int)round(totalBytesSent / (CGFloat)totalBytesExpectedToSend * 100); + dispatch_async(dispatch_get_main_queue(), ^{ + if (_progressBlock) { + _progressBlock(progress); + } + }); +} + +@end diff --git a/Parse/Internal/Commands/PFRESTAnalyticsCommand.h b/Parse/Internal/Commands/PFRESTAnalyticsCommand.h new file mode 100644 index 000000000..585b1417a --- /dev/null +++ b/Parse/Internal/Commands/PFRESTAnalyticsCommand.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const PFRESTAnalyticsEventNameAppOpened; +extern NSString *const PFRESTAnalyticsEventNameCrashReport; + +@interface PFRESTAnalyticsCommand : PFRESTCommand + ++ (instancetype)trackAppOpenedEventCommandWithPushHash:(nullable NSString *)pushHash + sessionToken:(nullable NSString *)sessionToken; + ++ (instancetype)trackEventCommandWithEventName:(NSString *)eventName + dimensions:(nullable NSDictionary *)dimensions + sessionToken:(nullable NSString *)sessionToken; + ++ (instancetype)trackCrashReportCommandWithBreakpadDumpParameters:(NSDictionary *)parameters + sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTAnalyticsCommand.m b/Parse/Internal/Commands/PFRESTAnalyticsCommand.m new file mode 100644 index 000000000..6badd350e --- /dev/null +++ b/Parse/Internal/Commands/PFRESTAnalyticsCommand.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTAnalyticsCommand.h" + +#import "PFHTTPRequest.h" + +/** + * Predefined events - AppOpened, CrashReport + * Coming soon - Log, ... + */ +NSString *const PFRESTAnalyticsEventNameAppOpened = @"AppOpened"; +NSString *const PFRESTAnalyticsEventNameCrashReport = @"_CrashReport"; + +@implementation PFRESTAnalyticsCommand + ++ (instancetype)trackAppOpenedEventCommandWithPushHash:(NSString *)pushHash + sessionToken:(NSString *)sessionToken { + NSDictionary *parameters = (pushHash ? @{ @"push_hash" : pushHash } : nil); + return [self _trackEventCommandWithEventName:PFRESTAnalyticsEventNameAppOpened + parameters:parameters + sessionToken:sessionToken]; +} + ++ (instancetype)trackEventCommandWithEventName:(NSString *)eventName + dimensions:(NSDictionary *)dimensions + sessionToken:(NSString *)sessionToken { + NSDictionary *parameters = (dimensions ? @{ @"dimensions" : dimensions } : nil); + return [self _trackEventCommandWithEventName:eventName parameters:parameters sessionToken:sessionToken]; +} + ++ (instancetype)trackCrashReportCommandWithBreakpadDumpParameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken { + return [self _trackEventCommandWithEventName:PFRESTAnalyticsEventNameCrashReport + parameters:@{ @"breakpadDump" : parameters } + sessionToken:sessionToken]; +} + ++ (instancetype)_trackEventCommandWithEventName:(NSString *)eventName + parameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken { + NSString *httpPath = [NSString stringWithFormat:@"events/%@", eventName]; + + NSMutableDictionary *dictionary = (parameters ? [parameters mutableCopy] : [NSMutableDictionary dictionary]); + if (!dictionary[@"at"]) { + dictionary[@"at"] = [NSDate date]; + } + + return [self commandWithHTTPPath:httpPath + httpMethod:PFHTTPRequestMethodPOST + parameters:dictionary + sessionToken:sessionToken]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTCloudCommand.h b/Parse/Internal/Commands/PFRESTCloudCommand.h new file mode 100644 index 000000000..b15bf7a9d --- /dev/null +++ b/Parse/Internal/Commands/PFRESTCloudCommand.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTCloudCommand : PFRESTCommand + ++ (instancetype)commandForFunction:(NSString *)function + withParameters:(nullable NSDictionary *)parameters + sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTCloudCommand.m b/Parse/Internal/Commands/PFRESTCloudCommand.m new file mode 100644 index 000000000..5bd85a986 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTCloudCommand.m @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCloudCommand.h" + +#import "PFAssert.h" +#import "PFHTTPRequest.h" + +@implementation PFRESTCloudCommand + ++ (instancetype)commandForFunction:(NSString *)function + withParameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken { + NSString *path = [NSString stringWithFormat:@"functions/%@", function]; + return [self commandWithHTTPPath:path + httpMethod:PFHTTPRequestMethodPOST + parameters:parameters + sessionToken:sessionToken]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTCommand.h b/Parse/Internal/Commands/PFRESTCommand.h new file mode 100644 index 000000000..0e6ad3e6e --- /dev/null +++ b/Parse/Internal/Commands/PFRESTCommand.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFNetworkCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTCommand : NSObject + +@property (nonatomic, copy, readonly) NSString *httpPath; +@property (nonatomic, copy, readonly) NSString *httpMethod; + +@property (nullable, nonatomic, copy, readonly) NSDictionary *parameters; +@property (nullable, nonatomic, copy) NSDictionary *additionalRequestHeaders; + +@property (nonatomic, copy, readonly) NSString *cacheKey; + +@property (nullable, nonatomic, copy) NSString *localId; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + ++ (instancetype)commandWithHTTPPath:(NSString *)path + httpMethod:(NSString *)httpMethod + parameters:(nullable NSDictionary *)parameters + sessionToken:(nullable NSString *)sessionToken; + ++ (instancetype)commandWithHTTPPath:(NSString *)path + httpMethod:(NSString *)httpMethod + parameters:(nullable NSDictionary *)parameters + operationSetUUID:(nullable NSString *)operationSetIdentifier + sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTCommand.m b/Parse/Internal/Commands/PFRESTCommand.m new file mode 100644 index 000000000..4bebec54a --- /dev/null +++ b/Parse/Internal/Commands/PFRESTCommand.m @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" +#import "PFRESTCommand_Private.h" + +#import "PFCoreManager.h" +#import "PFFieldOperation.h" +#import "PFHTTPRequest.h" +#import "PFHash.h" +#import "PFInternalUtils.h" +#import "PFObjectLocalIdStore.h" +#import "PFObjectPrivate.h" +#import "Parse_Private.h" + +static NSString *const PFRESTCommandHTTPPathEncodingKey = @"httpPath"; +static NSString *const PFRESTCommandHTTPMethodEncodingKey = @"httpMethod"; +static NSString *const PFRESTCommandParametersEncodingKey = @"parameters"; +static NSString *const PFRESTCommandSessionTokenEncodingKey = @"sessionToken"; +static NSString *const PFRESTCommandLocalIdEncodingKey = @"localId"; + +// Increment this when you change the format of cache values. +static const int PFRESTCommandCacheKeyVersion = 1; + +@implementation PFRESTCommand + +@synthesize sessionToken = _sessionToken; +@synthesize operationSetUUID = _operationSetUUID; +@synthesize localId = _localId; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)commandWithHTTPPath:(NSString *)path + httpMethod:(NSString *)httpMethod + parameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken { + return [self commandWithHTTPPath:path + httpMethod:httpMethod + parameters:parameters + operationSetUUID:nil + sessionToken:sessionToken]; +} + ++ (instancetype)commandWithHTTPPath:(NSString *)path + httpMethod:(NSString *)httpMethod + parameters:(NSDictionary *)parameters + operationSetUUID:(NSString *)operationSetIdentifier + sessionToken:(NSString *)sessionToken { + PFRESTCommand *command = [[self alloc] init]; + command.httpPath = path; + command.httpMethod = httpMethod; + command.parameters = parameters; + command.operationSetUUID = operationSetIdentifier; + command.sessionToken = sessionToken; + return command; +} + +///-------------------------------------- +#pragma mark - CacheKey +///-------------------------------------- + +- (NSString *)cacheKey { + if (_cacheKey) { + return _cacheKey; + } + + NSMutableDictionary *cacheParameters = [NSMutableDictionary dictionaryWithCapacity:2]; + if (self.parameters) { + cacheParameters[PFRESTCommandParametersEncodingKey] = self.parameters; + } + if (self.sessionToken) { + cacheParameters[PFRESTCommandSessionTokenEncodingKey] = self.sessionToken; + } + + NSString *parametersCacheKey = [PFInternalUtils cacheKeyForObject:cacheParameters]; + + _cacheKey = [NSString stringWithFormat:@"PFRESTCommand.%i.%@.%@.%ld.%@", + PFRESTCommandCacheKeyVersion, self.httpMethod, PFMD5HashFromString(self.httpPath), + // We use MD5 instead of native hash because it collides too much. + (long)PARSE_API_VERSION, PFMD5HashFromString(parametersCacheKey)]; + return _cacheKey; +} + +///-------------------------------------- +#pragma mark - PFNetworkCommand +///-------------------------------------- + +#pragma mark Encoding/Decoding + ++ (instancetype)commandFromDictionaryRepresentation:(NSDictionary *)dictionary { + if (![self isValidDictionaryRepresentation:dictionary]) { + return nil; + } + + PFRESTCommand *command = [self commandWithHTTPPath:dictionary[PFRESTCommandHTTPPathEncodingKey] + httpMethod:dictionary[PFRESTCommandHTTPMethodEncodingKey] + parameters:dictionary[PFRESTCommandParametersEncodingKey] + sessionToken:dictionary[PFRESTCommandSessionTokenEncodingKey]]; + command.localId = dictionary[PFRESTCommandLocalIdEncodingKey]; + return command; +} + +- (NSDictionary *)dictionaryRepresentation { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + if (self.httpPath) { + dictionary[PFRESTCommandHTTPPathEncodingKey] = self.httpPath; + } + if (self.httpMethod) { + dictionary[PFRESTCommandHTTPMethodEncodingKey] = self.httpMethod; + } + if (self.parameters) { + NSDictionary *parameters = [[PFPointerOrLocalIdObjectEncoder objectEncoder] encodeObject:self.parameters]; + dictionary[PFRESTCommandParametersEncodingKey] = parameters; + } + if (self.sessionToken) { + dictionary[PFRESTCommandSessionTokenEncodingKey] = self.sessionToken; + } + if (self.localId) { + dictionary[PFRESTCommandLocalIdEncodingKey] = self.localId; + } + return [dictionary copy]; +} + ++ (BOOL)isValidDictionaryRepresentation:(NSDictionary *)dictionary { + return dictionary[PFRESTCommandHTTPPathEncodingKey] != nil; +} + +#pragma mark Local Identifiers + +/*! + If this was the second save on a new object while offline, then its objectId + wasn't yet set when the command was created, so it would have been considered a + "create". But if the first save succeeded, then there is an objectId now, and it + will be mapped to the localId for this command's result. If so, change the + "create" operation to an "update", and add the objectId to the command. + */ +- (void)maybeChangeServerOperation { + if (self.localId) { + NSString *objectId = [[Parse _currentManager].coreManager.objectLocalIdStore objectIdForLocalId:self.localId]; + if (objectId) { + self.localId = nil; + + NSArray *components = [self.httpPath pathComponents]; + if ([components count] == 2) { + self.httpPath = [NSString pathWithComponents:[components arrayByAddingObject:objectId]]; + } + + if ([self.httpPath hasPrefix:@"classes"] && + [self.httpMethod isEqualToString:PFHTTPRequestMethodPOST]) { + self.httpMethod = PFHTTPRequestMethodPUT; + } + } + + if ([self.httpMethod isEqualToString:PFHTTPRequestMethodDELETE] && !objectId) { + [NSException raise:NSInternalInconsistencyException + format:@"Attempt to delete non-existent object."]; + } + } +} + ++ (BOOL)forEachLocalIdIn:(id)object doBlock:(BOOL(^)(PFObject *pointer))block { + __block BOOL modified = NO; + + // If this is a Pointer with a local id, try to resolve it. + if ([object isKindOfClass:[PFObject class]] && !((PFObject *)object).objectId) { + return block(object); + } + + if ([object isKindOfClass:[NSDictionary class]]) { + [object enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + if ([[self class] forEachLocalIdIn:obj doBlock:block]) { + modified = YES; + } + }]; + } else if ([object isKindOfClass:[NSArray class]]) { + for (id value in object) { + if ([[self class] forEachLocalIdIn:value doBlock:block]) { + modified = YES; + } + } + } else if ([object isKindOfClass:[PFAddOperation class]]) { + for (id value in ((PFAddOperation *)object).objects) { + if ([[self class] forEachLocalIdIn:value doBlock:block]) { + modified = YES; + } + } + } else if ([object isKindOfClass:[PFAddUniqueOperation class]]) { + for (id value in ((PFAddUniqueOperation *)object).objects) { + if ([[self class] forEachLocalIdIn:value doBlock:block]) { + modified = YES; + } + } + } else if ([object isKindOfClass:[PFRemoveOperation class]]) { + for (id value in ((PFRemoveOperation *)object).objects) { + if ([[self class] forEachLocalIdIn:value doBlock:block]) { + modified = YES; + } + } + } + + return modified; +} + +- (void)forEachLocalId:(BOOL(^)(PFObject *pointer))block { + NSDictionary *data = [[PFDecoder objectDecoder] decodeObject:self.parameters]; + if (!data) { + return; + } + + if ([[self class] forEachLocalIdIn:data doBlock:block]) { + self.parameters = [[PFPointerOrLocalIdObjectEncoder objectEncoder] encodeObject:data]; + } +} + +- (void)resolveLocalIds { + [self forEachLocalId:^(PFObject *pointer) { + [pointer resolveLocalId]; + return YES; + }]; + [self maybeChangeServerOperation]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTCommand_Private.h b/Parse/Internal/Commands/PFRESTCommand_Private.h new file mode 100644 index 000000000..f3d1e4e14 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTCommand_Private.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +@interface PFRESTCommand () + +@property (nonatomic, copy, readwrite) NSString *sessionToken; + +@property (nonatomic, copy, readwrite) NSString *httpPath; +@property (nonatomic, copy, readwrite) NSString *httpMethod; + +@property (nonatomic, copy, readwrite) NSDictionary *parameters; + +@property (nonatomic, copy, readwrite) NSString *cacheKey; + +@property (nonatomic, copy, readwrite) NSString *operationSetUUID; + +@end diff --git a/Parse/Internal/Commands/PFRESTConfigCommand.h b/Parse/Internal/Commands/PFRESTConfigCommand.h new file mode 100644 index 000000000..7b523073b --- /dev/null +++ b/Parse/Internal/Commands/PFRESTConfigCommand.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTConfigCommand : PFRESTCommand + ++ (instancetype)configFetchCommandWithSessionToken:(nullable NSString *)sessionToken; ++ (instancetype)configUpdateCommandWithConfigParameters:(NSDictionary *)parameters + sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTConfigCommand.m b/Parse/Internal/Commands/PFRESTConfigCommand.m new file mode 100644 index 000000000..6709990a2 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTConfigCommand.m @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTConfigCommand.h" + +#import "PFAssert.h" +#import "PFHTTPRequest.h" + +@implementation PFRESTConfigCommand + ++ (instancetype)configFetchCommandWithSessionToken:(NSString *)sessionToken { + return [self commandWithHTTPPath:@"config" + httpMethod:PFHTTPRequestMethodGET + parameters:nil + sessionToken:sessionToken]; +} + ++ (instancetype)configUpdateCommandWithConfigParameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken { + NSDictionary *commandParameters = @{ @"params" : parameters }; + return [self commandWithHTTPPath:@"config" + httpMethod:PFHTTPRequestMethodPUT + parameters:commandParameters + sessionToken:sessionToken]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTFileCommand.h b/Parse/Internal/Commands/PFRESTFileCommand.h new file mode 100644 index 000000000..770b2582a --- /dev/null +++ b/Parse/Internal/Commands/PFRESTFileCommand.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTFileCommand : PFRESTCommand + ++ (instancetype)uploadCommandForFileWithName:(NSString *)fileName + sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTFileCommand.m b/Parse/Internal/Commands/PFRESTFileCommand.m new file mode 100644 index 000000000..1de7cc9e3 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTFileCommand.m @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTFileCommand.h" + +#import "PFAssert.h" +#import "PFHTTPRequest.h" + +@implementation PFRESTFileCommand + ++ (instancetype)uploadCommandForFileWithName:(NSString *)fileName + sessionToken:(NSString *)sessionToken { + NSMutableString *httpPath = [@"files/" mutableCopy]; + if (fileName) { + [httpPath appendString:fileName]; + } + return [self commandWithHTTPPath:httpPath + httpMethod:PFHTTPRequestMethodPOST + parameters:nil + sessionToken:sessionToken]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTObjectBatchCommand.h b/Parse/Internal/Commands/PFRESTObjectBatchCommand.h new file mode 100644 index 000000000..8b28e34fe --- /dev/null +++ b/Parse/Internal/Commands/PFRESTObjectBatchCommand.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +extern NSUInteger const PFRESTObjectBatchCommandSubcommandsLimit; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTObjectBatchCommand : PFRESTCommand + ++ (instancetype)batchCommandWithCommands:(NSArray *)commands sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTObjectBatchCommand.m b/Parse/Internal/Commands/PFRESTObjectBatchCommand.m new file mode 100644 index 000000000..7aa407f0d --- /dev/null +++ b/Parse/Internal/Commands/PFRESTObjectBatchCommand.m @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTObjectBatchCommand.h" + +#import "PFAssert.h" +#import "PFHTTPRequest.h" + +NSUInteger const PFRESTObjectBatchCommandSubcommandsLimit = 50; + +@implementation PFRESTObjectBatchCommand + ++ (nonnull instancetype)batchCommandWithCommands:(NSArray *)commands + sessionToken:(NSString *)sessionToken { + PFParameterAssert([commands count] <= PFRESTObjectBatchCommandSubcommandsLimit, + @"Max of %d commands are allowed in a single batch command", + (int)PFRESTObjectBatchCommandSubcommandsLimit); + + NSMutableArray *requests = [NSMutableArray arrayWithCapacity:[commands count]]; + for (PFRESTCommand *command in commands) { + NSMutableDictionary *requestDictionary = [@{ @"method" : command.httpMethod, + @"path" : [NSString stringWithFormat:@"/1/%@", command.httpPath] + } mutableCopy]; + if (command.parameters) { + requestDictionary[@"body"] = command.parameters; + } + + [requests addObject:requestDictionary]; + } + return [self commandWithHTTPPath:@"batch" + httpMethod:PFHTTPRequestMethodPOST + parameters:@{ @"requests" : requests } + sessionToken:sessionToken]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTObjectCommand.h b/Parse/Internal/Commands/PFRESTObjectCommand.h new file mode 100644 index 000000000..67a8ff4cd --- /dev/null +++ b/Parse/Internal/Commands/PFRESTObjectCommand.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@class PFObjectState; + +@interface PFRESTObjectCommand : PFRESTCommand + ++ (instancetype)fetchObjectCommandForObjectState:(PFObjectState *)state + withSessionToken:(nullable NSString *)sessionToken; + ++ (instancetype)createObjectCommandForObjectState:(PFObjectState *)state + changes:(nullable NSDictionary *)changes + operationSetUUID:(nullable NSString *)operationSetIdentifier + sessionToken:(nullable NSString *)sessionToken; + ++ (instancetype)updateObjectCommandForObjectState:(PFObjectState *)state + changes:(nullable NSDictionary *)changes + operationSetUUID:(nullable NSString *)operationSetIdentifier + sessionToken:(nullable NSString *)sessionToken; + ++ (instancetype)deleteObjectCommandForObjectState:(PFObjectState *)state + withSessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTObjectCommand.m b/Parse/Internal/Commands/PFRESTObjectCommand.m new file mode 100644 index 000000000..964273eec --- /dev/null +++ b/Parse/Internal/Commands/PFRESTObjectCommand.m @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTObjectCommand.h" + +#import "PFAssert.h" +#import "PFHTTPRequest.h" +#import "PFObjectState.h" + +@implementation PFRESTObjectCommand + ++ (instancetype)fetchObjectCommandForObjectState:(PFObjectState *)state + withSessionToken:(NSString *)sessionToken { + PFParameterAssert(state.objectId.length, @"objectId should be non nil"); + PFParameterAssert(state.parseClassName.length, @"Class name should be non nil"); + + NSString *httpPath = [NSString stringWithFormat:@"classes/%@/%@", state.parseClassName, state.objectId]; + PFRESTObjectCommand *command = [self commandWithHTTPPath:httpPath + httpMethod:PFHTTPRequestMethodGET + parameters:nil + sessionToken:sessionToken]; + return command; +} + ++ (instancetype)createObjectCommandForObjectState:(PFObjectState *)state + changes:(NSDictionary *)changes + operationSetUUID:(NSString *)operationSetIdentifier + sessionToken:(NSString *)sessionToken { + PFParameterAssert(state.parseClassName.length, @"Class name should be non nil"); + + NSString *httpPath = [NSString stringWithFormat:@"classes/%@", state.parseClassName]; + PFRESTObjectCommand *command = [self commandWithHTTPPath:httpPath + httpMethod:PFHTTPRequestMethodPOST + parameters:changes + operationSetUUID:operationSetIdentifier + sessionToken:sessionToken]; + return command; +} + ++ (instancetype)updateObjectCommandForObjectState:(PFObjectState *)state + changes:(NSDictionary *)changes + operationSetUUID:(NSString *)operationSetIdentifier + sessionToken:(NSString *)sessionToken { + PFParameterAssert(state.parseClassName.length, @"Class name should be non nil"); + PFParameterAssert(state.objectId.length, @"objectId should be non nil"); + + NSString *httpPath = [NSString stringWithFormat:@"classes/%@/%@", state.parseClassName, state.objectId]; + PFRESTObjectCommand *command = [self commandWithHTTPPath:httpPath + httpMethod:PFHTTPRequestMethodPUT + parameters:changes + operationSetUUID:operationSetIdentifier + sessionToken:sessionToken]; + return command; +} + ++ (instancetype)deleteObjectCommandForObjectState:(PFObjectState *)state + withSessionToken:(NSString *)sessionToken { + PFParameterAssert(state.parseClassName.length, @"Class name should be non nil"); + + NSMutableString *httpPath = [NSMutableString stringWithFormat:@"classes/%@", state.parseClassName]; + if (state.objectId) { + [httpPath appendFormat:@"/%@", state.objectId]; + } + PFRESTObjectCommand *command = [self commandWithHTTPPath:httpPath + httpMethod:PFHTTPRequestMethodDELETE + parameters:nil + sessionToken:sessionToken]; + return command; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTPushCommand.h b/Parse/Internal/Commands/PFRESTPushCommand.h new file mode 100644 index 000000000..f9d124e6e --- /dev/null +++ b/Parse/Internal/Commands/PFRESTPushCommand.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +@class PFPushState; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTPushCommand : PFRESTCommand + ++ (instancetype)sendPushCommandWithPushState:(PFPushState *)state + sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTPushCommand.m b/Parse/Internal/Commands/PFRESTPushCommand.m new file mode 100644 index 000000000..2f7601ed9 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTPushCommand.m @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTPushCommand.h" + +#import "PFAssert.h" +#import "PFDateFormatter.h" +#import "PFHTTPRequest.h" +#import "PFInternalUtils.h" +#import "PFPushState.h" +#import "PFQueryState.h" +#import "PFRESTQueryCommand.h" + +@implementation PFRESTPushCommand + ++ (instancetype)sendPushCommandWithPushState:(PFPushState *)state + sessionToken:(NSString *)sessionToken { + NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; + + if (state.queryState) { + NSDictionary *queryParameters = [PFRESTQueryCommand findCommandParametersForQueryState:state.queryState]; + parameters[@"where"] = queryParameters[@"where"]; + } else { + if (state.channels) { + parameters[@"channels"] = [state.channels allObjects]; + } + } + + // If there are no conditions set, then push to everyone by specifying empty query conditions. + if ([parameters count] == 0) { + parameters[@"where"] = @{}; + } + + if (state.expirationDate) { + parameters[@"expiration_time"] = [[PFDateFormatter sharedFormatter] preciseStringFromDate:state.expirationDate]; + } else if (state.expirationTimeInterval) { + parameters[@"expiration_interval"] = state.expirationTimeInterval; + } + + // TODO (nlutsenko): Probably we need an assert here, as there is no reason to send push without message + if (state.payload) { + parameters[@"data"] = state.payload; + } + + return [self commandWithHTTPPath:@"push" + httpMethod:PFHTTPRequestMethodPOST + parameters:parameters + sessionToken:sessionToken]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTQueryCommand.h b/Parse/Internal/Commands/PFRESTQueryCommand.h new file mode 100644 index 000000000..a77a66fe4 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTQueryCommand.h @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +@class PFQueryState; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTQueryCommand : PFRESTCommand + +///-------------------------------------- +/// @name Find +///-------------------------------------- + ++ (instancetype)findCommandForQueryState:(PFQueryState *)queryState withSessionToken:(nullable NSString *)sessionToken; + ++ (instancetype)findCommandForClassWithName:(NSString *)className + order:(nullable NSString *)order + conditions:(nullable NSDictionary *)conditions + selectedKeys:(nullable NSSet *)selectedKeys + includedKeys:(nullable NSSet *)includedKeys + limit:(NSInteger)limit + skip:(NSInteger)skip + extraOptions:(nullable NSDictionary *)extraOptions + tracingEnabled:(BOOL)trace + sessionToken:(nullable NSString *)sessionToken; + +///-------------------------------------- +/// @name Count +///-------------------------------------- + ++ (instancetype)countCommandFromFindCommand:(PFRESTQueryCommand *)findCommand; + +///-------------------------------------- +/// @name Parameters +///-------------------------------------- + ++ (NSDictionary *)findCommandParametersForQueryState:(PFQueryState *)queryState; ++ (NSDictionary *)findCommandParametersWithOrder:(nullable NSString *)order + conditions:(nullable NSDictionary *)conditions + selectedKeys:(nullable NSSet *)selectedKeys + includedKeys:(nullable NSSet *)includedKeys + limit:(NSInteger)limit + skip:(NSInteger)skip + extraOptions:(nullable NSDictionary *)extraOptions + tracingEnabled:(BOOL)trace; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTQueryCommand.m b/Parse/Internal/Commands/PFRESTQueryCommand.m new file mode 100644 index 000000000..f452fb660 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTQueryCommand.m @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTQueryCommand.h" + +#import "PFEncoder.h" +#import "PFHTTPRequest.h" +#import "PFQueryPrivate.h" +#import "PFQueryState.h" + +@implementation PFRESTQueryCommand + +///-------------------------------------- +#pragma mark - Find +///-------------------------------------- + ++ (instancetype)findCommandForQueryState:(PFQueryState *)queryState withSessionToken:(NSString *)sessionToken { + NSDictionary *parameters = [self findCommandParametersForQueryState:queryState]; + return [self _findCommandForClassWithName:queryState.parseClassName + parameters:parameters + sessionToken:sessionToken]; +} + ++ (instancetype)findCommandForClassWithName:(NSString *)className + order:(NSString *)order + conditions:(NSDictionary *)conditions + selectedKeys:(NSSet *)selectedKeys + includedKeys:(NSSet *)includedKeys + limit:(NSInteger)limit + skip:(NSInteger)skip + extraOptions:(NSDictionary *)extraOptions + tracingEnabled:(BOOL)trace + sessionToken:(NSString *)sessionToken { + NSDictionary *parameters = [self findCommandParametersWithOrder:order + conditions:conditions + selectedKeys:selectedKeys + includedKeys:includedKeys + limit:limit + skip:skip + extraOptions:extraOptions + tracingEnabled:trace]; + return [self _findCommandForClassWithName:className + parameters:parameters + sessionToken:sessionToken]; +} + ++ (instancetype)_findCommandForClassWithName:(NSString *)className + parameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken { + NSString *httpPath = [NSString stringWithFormat:@"classes/%@", className]; + PFRESTQueryCommand *command = [self commandWithHTTPPath:httpPath + httpMethod:PFHTTPRequestMethodGET + parameters:parameters + sessionToken:sessionToken]; + return command; +} + +///-------------------------------------- +#pragma mark - Count +///-------------------------------------- + ++ (instancetype)countCommandFromFindCommand:(PFRESTQueryCommand *)findCommand { + NSMutableDictionary *parameters = [findCommand.parameters mutableCopy]; + parameters[@"count"] = @"1"; + parameters[@"limit"] = @"0"; // Set the limit to 0, as we are not interested in results at all. + [parameters removeObjectForKey:@"skip"]; + + return [self commandWithHTTPPath:findCommand.httpPath + httpMethod:findCommand.httpMethod + parameters:[parameters copy] + sessionToken:findCommand.sessionToken]; +} + +///-------------------------------------- +#pragma mark - Parameters +///-------------------------------------- + ++ (NSDictionary *)findCommandParametersForQueryState:(PFQueryState *)queryState { + return [self findCommandParametersWithOrder:queryState.sortOrderString + conditions:queryState.conditions + selectedKeys:queryState.selectedKeys + includedKeys:queryState.includedKeys + limit:queryState.limit + skip:queryState.skip + extraOptions:queryState.extraOptions + tracingEnabled:queryState.trace]; +} + ++ (NSDictionary *)findCommandParametersWithOrder:(NSString *)order + conditions:(NSDictionary *)conditions + selectedKeys:(NSSet *)selectedKeys + includedKeys:(NSSet *)includedKeys + limit:(NSInteger)limit + skip:(NSInteger)skip + extraOptions:(NSDictionary *)extraOptions + tracingEnabled:(BOOL)trace { + NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; + + if ([order length]) { + parameters[@"order"] = order; + } + if (selectedKeys != nil) { + parameters[@"keys"] = [[selectedKeys allObjects] componentsJoinedByString:@","]; + } + if ([includedKeys count] > 0) { + parameters[@"include"] = [[includedKeys allObjects] componentsJoinedByString:@","]; + } + if (limit >= 0) { + parameters[@"limit"] = [NSString stringWithFormat:@"%d", (int)limit]; + } + if (skip > 0) { + parameters[@"skip"] = [NSString stringWithFormat:@"%d", (int)skip]; + } + if (trace) { + // TODO: (nlutsenko) Double check that tracing still works. Maybe create test for it. + parameters[@"trace"] = @"1"; + } + [extraOptions enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + parameters[key] = obj; + }]; + + if ([conditions count] > 0) { + NSMutableDictionary *whereData = [[NSMutableDictionary alloc] init]; + [conditions enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([key isEqualToString:@"$or"]) { + NSArray *array = (NSArray *)obj; + NSMutableArray *newArray = [NSMutableArray array]; + for (PFQuery *subquery in array) { + // TODO: (nlutsenko) Move this validation into PFQuery/PFQueryState. + if (subquery.state.limit >= 0) { + [NSException raise:NSInvalidArgumentException + format:@"OR queries do not support sub queries with limits"]; + } + + if (subquery.state.skip > 0) { + [NSException raise:NSInvalidArgumentException + format:@"OR queries do not support sub queries with skip"]; + } + + if ([subquery.state.sortKeys count]) { + [NSException raise:NSInvalidArgumentException + format:@"OR queries do not support sub queries with order"]; + } + + if ([subquery.state.includedKeys count] > 0) { + [NSException raise:NSInvalidArgumentException + format:@"OR queries do not support sub-queries with includes"]; + } + + if (subquery.state.selectedKeys) { + [NSException raise:NSInvalidArgumentException + format:@"OR queries do not support sub-queries with selectKeys"]; + } + + NSDictionary *queryDict = [self findCommandParametersWithOrder:subquery.state.sortOrderString + conditions:subquery.state.conditions + selectedKeys:subquery.state.selectedKeys + includedKeys:subquery.state.includedKeys + limit:subquery.state.limit + skip:subquery.state.skip + extraOptions:nil + tracingEnabled:NO]; + + queryDict = queryDict[@"where"]; + if ([queryDict count] > 0) { + [newArray addObject:queryDict]; + } else { + [newArray addObject:[NSDictionary dictionary]]; + } + } + whereData[key] = newArray; + } else { + id object = [self _encodeSubqueryIfNeeded:obj]; + whereData[key] = [[PFPointerObjectEncoder objectEncoder] encodeObject:object]; + } + }]; + + parameters[@"where"] = whereData; + } + + return parameters; +} + ++ (id)_encodeSubqueryIfNeeded:(id)object { + if (![object isKindOfClass:[NSDictionary class]]) { + return object; + } + + NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:[object count]]; + [object enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([obj isKindOfClass:[PFQuery class]]) { + PFQuery *subquery = (PFQuery *)obj; + NSMutableDictionary *subqueryParameters = [[self findCommandParametersWithOrder:subquery.state.sortOrderString + conditions:subquery.state.conditions + selectedKeys:subquery.state.selectedKeys + includedKeys:subquery.state.includedKeys + limit:subquery.state.limit + skip:subquery.state.skip + extraOptions:subquery.state.extraOptions + tracingEnabled:NO] mutableCopy]; + subqueryParameters[@"className"] = subquery.parseClassName; + obj = subqueryParameters; + } else if ([obj isKindOfClass:[NSDictionary class]]) { + obj = [self _encodeSubqueryIfNeeded:obj]; + } + + parameters[key] = obj; + }]; + return parameters; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTSessionCommand.h b/Parse/Internal/Commands/PFRESTSessionCommand.h new file mode 100644 index 000000000..d777b0450 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTSessionCommand.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTSessionCommand : PFRESTCommand + ++ (instancetype)getCurrentSessionCommandWithSessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTSessionCommand.m b/Parse/Internal/Commands/PFRESTSessionCommand.m new file mode 100644 index 000000000..c38bd1c6d --- /dev/null +++ b/Parse/Internal/Commands/PFRESTSessionCommand.m @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTSessionCommand.h" + +#import "PFHTTPRequest.h" + +@implementation PFRESTSessionCommand + ++ (instancetype)getCurrentSessionCommandWithSessionToken:(nullable NSString *)sessionToken { + return [self commandWithHTTPPath:@"sessions/me" + httpMethod:PFHTTPRequestMethodGET + parameters:nil + sessionToken:sessionToken]; +} + +@end diff --git a/Parse/Internal/Commands/PFRESTUserCommand.h b/Parse/Internal/Commands/PFRESTUserCommand.h new file mode 100644 index 000000000..93c81db31 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTUserCommand.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTCommand.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFRESTUserCommand : PFRESTCommand + +@property (nonatomic, assign, readonly) BOOL revocableSessionEnabled; + +///-------------------------------------- +/// @name Log In +///-------------------------------------- + ++ (instancetype)logInUserCommandWithUsername:(NSString *)username + password:(NSString *)password + revocableSession:(BOOL)revocableSessionEnabled; ++ (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType + authenticationData:(NSDictionary *)authenticationData + revocableSession:(BOOL)revocableSessionEnabled; ++ (instancetype)serviceLoginUserCommandWithParameters:(NSDictionary *)parameters + revocableSession:(BOOL)revocableSessionEnabled + sessionToken:(nullable NSString *)sessionToken; + +///-------------------------------------- +/// @name Sign Up +///-------------------------------------- + ++ (instancetype)signUpUserCommandWithParameters:(NSDictionary *)parameters + revocableSession:(BOOL)revocableSessionEnabled + sessionToken:(nullable NSString *)sessionToken; + +///-------------------------------------- +/// @name Current User +///-------------------------------------- + ++ (instancetype)getCurrentUserCommandWithSessionToken:(NSString *)sessionToken; ++ (instancetype)upgradeToRevocableSessionCommandWithSessionToken:(NSString *)sessionToken; ++ (instancetype)logOutUserCommandWithSessionToken:(NSString *)sessionToken; + +///-------------------------------------- +/// @name Password Rest +///-------------------------------------- + ++ (instancetype)resetPasswordCommandForUserWithEmail:(NSString *)email; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Commands/PFRESTUserCommand.m b/Parse/Internal/Commands/PFRESTUserCommand.m new file mode 100644 index 000000000..190a5d502 --- /dev/null +++ b/Parse/Internal/Commands/PFRESTUserCommand.m @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRESTUserCommand.h" + +#import "PFAssert.h" +#import "PFHTTPRequest.h" + +static NSString *const PFRESTUserCommandRevocableSessionHeader = @"X-Parse-Revocable-Session"; +static NSString *const PFRESTUserCommandRevocableSessionHeaderEnabledValue = @"1"; + +@interface PFRESTUserCommand () + +@property (nonatomic, assign, readwrite) BOOL revocableSessionEnabled; + +@end + +@implementation PFRESTUserCommand + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)_commandWithHTTPPath:(NSString *)path + httpMethod:(NSString *)httpMethod + parameters:(NSDictionary *)parameters + sessionToken:(NSString *)sessionToken + revocableSession:(BOOL)revocableSessionEnabled { + PFRESTUserCommand *command = [self commandWithHTTPPath:path + httpMethod:httpMethod + parameters:parameters + sessionToken:sessionToken]; + if (revocableSessionEnabled) { + command.additionalRequestHeaders = @{ PFRESTUserCommandRevocableSessionHeader : + PFRESTUserCommandRevocableSessionHeaderEnabledValue}; + } + command.revocableSessionEnabled = revocableSessionEnabled; + return command; +} + +///-------------------------------------- +#pragma mark - Log In +///-------------------------------------- + ++ (instancetype)logInUserCommandWithUsername:(NSString *)username + password:(NSString *)password + revocableSession:(BOOL)revocableSessionEnabled { + NSDictionary *parameters = @{ @"username" : username, + @"password" : password }; + return [self _commandWithHTTPPath:@"login" + httpMethod:PFHTTPRequestMethodGET + parameters:parameters + sessionToken:nil + revocableSession:revocableSessionEnabled]; +} + ++ (instancetype)serviceLoginUserCommandWithAuthenticationType:(NSString *)authenticationType + authenticationData:(NSDictionary *)authenticationData + revocableSession:(BOOL)revocableSessionEnabled { + NSDictionary *parameters = @{ @"authData" : @{ authenticationType : authenticationData } }; + return [self serviceLoginUserCommandWithParameters:parameters + revocableSession:revocableSessionEnabled + sessionToken:nil]; +} + ++ (instancetype)serviceLoginUserCommandWithParameters:(NSDictionary *)parameters + revocableSession:(BOOL)revocableSessionEnabled + sessionToken:(NSString *)sessionToken { + return [self _commandWithHTTPPath:@"users" + httpMethod:PFHTTPRequestMethodPOST + parameters:parameters + sessionToken:sessionToken + revocableSession:revocableSessionEnabled]; +} + +///-------------------------------------- +#pragma mark - Sign Up +///-------------------------------------- + ++ (instancetype)signUpUserCommandWithParameters:(NSDictionary *)parameters + revocableSession:(BOOL)revocableSessionEnabled + sessionToken:(NSString *)sessionToken { + return [self _commandWithHTTPPath:@"users" + httpMethod:PFHTTPRequestMethodPOST + parameters:parameters + sessionToken:sessionToken + revocableSession:revocableSessionEnabled]; +} + +///-------------------------------------- +#pragma mark - Current User +///-------------------------------------- + ++ (instancetype)getCurrentUserCommandWithSessionToken:(NSString *)sessionToken { + return [self commandWithHTTPPath:@"users/me" + httpMethod:PFHTTPRequestMethodGET + parameters:nil + sessionToken:sessionToken]; +} + ++ (instancetype)upgradeToRevocableSessionCommandWithSessionToken:(NSString *)sessionToken { + return [self commandWithHTTPPath:@"upgradeToRevocableSession" + httpMethod:PFHTTPRequestMethodPOST + parameters:nil + sessionToken:sessionToken]; +} + ++ (instancetype)logOutUserCommandWithSessionToken:(NSString *)sessionToken { + return [self commandWithHTTPPath:@"logout" + httpMethod:PFHTTPRequestMethodPOST + parameters:nil + sessionToken:sessionToken]; +} + +///-------------------------------------- +#pragma mark - Additional User Commands +///-------------------------------------- + ++ (instancetype)resetPasswordCommandForUserWithEmail:(NSString *)email { + return [self commandWithHTTPPath:@"requestPasswordReset" + httpMethod:PFHTTPRequestMethodPOST + parameters:@{ @"email" : email } + sessionToken:nil]; +} + +@end diff --git a/Parse/Internal/Config/Controller/PFConfigController.h b/Parse/Internal/Config/Controller/PFConfigController.h new file mode 100644 index 000000000..9e85b50c0 --- /dev/null +++ b/Parse/Internal/Config/Controller/PFConfigController.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFCurrentConfigController; +@class PFFileManager; +@protocol PFCommandRunning; + +@interface PFConfigController : NSObject + +@property (nonatomic, strong, readonly) PFFileManager *fileManager; +@property (nonatomic, strong, readonly) id commandRunner; + +@property (nonatomic, strong, readonly) PFCurrentConfigController *currentConfigController; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFileManager:(PFFileManager *)fileManager + commandRunner:(id)commandRunner NS_DESIGNATED_INITIALIZER; + +///-------------------------------------- +/// @name Fetch +///-------------------------------------- + +/*! + Fetches current config from network async. + + @param sessionToken Current user session token. + + @returns `BFTask` with result set to `PFConfig`. + */ +- (BFTask *)fetchConfigAsyncWithSessionToken:(NSString *)sessionToken; + +@end diff --git a/Parse/Internal/Config/Controller/PFConfigController.m b/Parse/Internal/Config/Controller/PFConfigController.m new file mode 100644 index 000000000..ec26b5ba6 --- /dev/null +++ b/Parse/Internal/Config/Controller/PFConfigController.m @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFConfigController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFConfig_Private.h" +#import "PFCurrentConfigController.h" +#import "PFDecoder.h" +#import "PFRESTConfigCommand.h" + +@interface PFConfigController () +{ + dispatch_queue_t _dataAccessQueue; + dispatch_queue_t _networkQueue; + BFExecutor *_networkExecutor; +} + +@end + +@implementation PFConfigController + +@synthesize currentConfigController = _currentConfigController; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithFileManager:(PFFileManager *)fileManager + commandRunner:(id)commandRunner { + self = [super init]; + if (!self) return nil; + + _fileManager = fileManager; + _commandRunner = commandRunner; + + _dataAccessQueue = dispatch_queue_create("com.parse.config.access", DISPATCH_QUEUE_SERIAL); + + _networkQueue = dispatch_queue_create("com.parse.config.network", DISPATCH_QUEUE_SERIAL); + _networkExecutor = [BFExecutor executorWithDispatchQueue:_networkQueue]; + + return self; +} + +///-------------------------------------- +#pragma mark - Fetch +///-------------------------------------- + +- (BFTask *)fetchConfigAsyncWithSessionToken:(NSString *)sessionToken { + @weakify(self); + return [BFTask taskFromExecutor:_networkExecutor withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [PFRESTConfigCommand configFetchCommandWithSessionToken:sessionToken]; + return [[[self.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed] + continueWithSuccessBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + NSDictionary *fetchedConfig = [[PFDecoder objectDecoder] decodeObject:result.result]; + return [[PFConfig alloc] initWithFetchedConfig:fetchedConfig]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // Roll-forward the config. + return [[self.currentConfigController setCurrentConfigAsync:task.result] continueWithResult:task.result]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (PFCurrentConfigController *)currentConfigController { + __block PFCurrentConfigController *controller = nil; + dispatch_sync(_dataAccessQueue, ^{ + if (!_currentConfigController) { + _currentConfigController = [[PFCurrentConfigController alloc] initWithFileManager:self.fileManager]; + } + controller = _currentConfigController; + }); + return controller; +} + +@end diff --git a/Parse/Internal/Config/Controller/PFCurrentConfigController.h b/Parse/Internal/Config/Controller/PFCurrentConfigController.h new file mode 100644 index 000000000..b08d69c74 --- /dev/null +++ b/Parse/Internal/Config/Controller/PFCurrentConfigController.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFConfig; +@class PFFileManager; + +@interface PFCurrentConfigController : NSObject + +@property (nonatomic, strong, readonly) PFFileManager *fileManager; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFileManager:(PFFileManager *)fileManager NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithFileManager:(PFFileManager *)fileManager; + +///-------------------------------------- +/// @name Accessors +///-------------------------------------- + +- (BFTask *)getCurrentConfigAsync; +- (BFTask *)setCurrentConfigAsync:(PFConfig *)config; + +- (BFTask *)clearCurrentConfigAsync; +- (BFTask *)clearMemoryCachedCurrentConfigAsync; + +@end diff --git a/Parse/Internal/Config/Controller/PFCurrentConfigController.m b/Parse/Internal/Config/Controller/PFCurrentConfigController.m new file mode 100644 index 000000000..759254a41 --- /dev/null +++ b/Parse/Internal/Config/Controller/PFCurrentConfigController.m @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCurrentConfigController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFConfig_Private.h" +#import "PFDecoder.h" +#import "PFFileManager.h" +#import "PFJSONSerialization.h" + +static NSString *const PFConfigCurrentConfigFileName_ = @"config"; + +@interface PFCurrentConfigController () { + dispatch_queue_t _dataQueue; + BFExecutor *_dataExecutor; + PFConfig *_currentConfig; +} + +@property (nonatomic, copy, readonly) NSString *configFilePath; + +@end + +@implementation PFCurrentConfigController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithFileManager:(PFFileManager *)fileManager { + self = [super init]; + if (!self) return nil; + + _dataQueue = dispatch_queue_create("com.parse.config.current", DISPATCH_QUEUE_SERIAL); + _dataExecutor = [BFExecutor executorWithDispatchQueue:_dataQueue]; + + _fileManager = fileManager; + + return self; +} + ++ (instancetype)controllerWithFileManager:(PFFileManager *)fileManager { + return [[self alloc] initWithFileManager:fileManager]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (BFTask *)getCurrentConfigAsync { + return [BFTask taskFromExecutor:_dataExecutor withBlock:^id{ + if (!_currentConfig) { + NSError *error = nil; + NSData *jsonData = [NSData dataWithContentsOfFile:self.configFilePath + options:NSDataReadingMappedIfSafe + error:&error]; + if (error == nil && [jsonData length] != 0) { + NSDictionary *dictionary = [PFJSONSerialization JSONObjectFromData:jsonData]; + NSDictionary *decodedDictionary = [[PFDecoder objectDecoder] decodeObject:dictionary]; + _currentConfig = [[PFConfig alloc] initWithFetchedConfig:decodedDictionary]; + } else { + _currentConfig = [[PFConfig alloc] init]; + } + } + return _currentConfig; + }]; +} + +- (BFTask *)setCurrentConfigAsync:(PFConfig *)config { + @weakify(self); + return [BFTask taskFromExecutor:_dataExecutor withBlock:^id{ + @strongify(self); + _currentConfig = config; + + NSDictionary *configParameters = @{ PFConfigParametersRESTKey : (config.parametersDictionary ?: @{}) }; + id encodedObject = [[PFPointerObjectEncoder objectEncoder] encodeObject:configParameters]; + NSData *jsonData = [PFJSONSerialization dataFromJSONObject:encodedObject]; + return [PFFileManager writeDataAsync:jsonData toFile:self.configFilePath]; + }]; +} + +- (BFTask *)clearCurrentConfigAsync { + @weakify(self); + return [BFTask taskFromExecutor:_dataExecutor withBlock:^id{ + @strongify(self); + _currentConfig = nil; + return [PFFileManager removeItemAtPathAsync:self.configFilePath]; + }]; +} + +- (BFTask *)clearMemoryCachedCurrentConfigAsync { + return [BFTask taskFromExecutor:_dataExecutor withBlock:^id{ + _currentConfig = nil; + return nil; + }]; +} + +- (NSString *)configFilePath { + return [self.fileManager parseDataItemPathForPathComponent:PFConfigCurrentConfigFileName_]; +} + +@end diff --git a/Parse/Internal/Config/PFConfig_Private.h b/Parse/Internal/Config/PFConfig_Private.h new file mode 100644 index 000000000..07a197672 --- /dev/null +++ b/Parse/Internal/Config/PFConfig_Private.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +NSString *const PFConfigParametersRESTKey; + +@interface PFConfig (Private) + +@property (atomic, copy, readonly) NSDictionary *parametersDictionary; + +- (instancetype)initWithFetchedConfig:(NSDictionary *)config; + +@end diff --git a/Parse/Internal/FieldOperation/PFFieldOperation.h b/Parse/Internal/FieldOperation/PFFieldOperation.h new file mode 100644 index 000000000..519353d83 --- /dev/null +++ b/Parse/Internal/FieldOperation/PFFieldOperation.h @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFEncoder.h" + +@class PFDecoder; +@class PFObject; + +///-------------------------------------- +#pragma mark - PFFieldOperation +///-------------------------------------- + +/*! + A PFFieldOperation represents a modification to a value in a PFObject. + For example, setting, deleting, or incrementing a value are all different + kinds of PFFieldOperations. PFFieldOperations themselves can be considered + to be immutable. + */ +@interface PFFieldOperation : NSObject + +/*! + Converts the PFFieldOperation to a data structure (typically an NSDictionary) + that can be converted to JSON and sent to Parse as part of a save operation. + + @param objectEncoder encoder that will be used to encode the object. + @returns An object to be jsonified. + */ +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder; + +/*! + Returns a field operation that is composed of a previous operation followed by + this operation. This will not mutate either operation. However, it may return + self if the current operation is not affected by previous changes. For example: + [{increment by 2} mergeWithPrevious:{set to 5}] -> {set to 7} + [{set to 5} mergeWithPrevious:{increment by 2}] -> {set to 5} + [{add "foo"} mergeWithPrevious:{delete}] -> {set to ["foo"]} + [{delete} mergeWithPrevious:{add "foo"}] -> {delete} + + @param previous The most recent operation on the field, or nil if none. + @returns A new PFFieldOperation or self. + */ +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous; + +/*! + Returns a new estimated value based on a previous value and this operation. This + value is not intended to be sent to Parse, but it used locally on the client to + inspect the most likely current value for a field. + + The key and object are used solely for PFRelation to be able to construct objects + that refer back to its parent. + + @param oldValue The previous value for the field. + @param key The key that this value is for. + + @returns The new value for the field. + */ +- (id)applyToValue:(id)oldValue forKey:(NSString *)key; + +@end + +///-------------------------------------- +#pragma mark - Independent Operations +///-------------------------------------- + +/*! + An operation where a field is set to a given value regardless of + its previous value. + */ +@interface PFSetOperation : PFFieldOperation + +@property (nonatomic, strong, readonly) id value; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithValue:(id)value NS_DESIGNATED_INITIALIZER; ++ (instancetype)setWithValue:(id)value; + +@end + +/*! + An operation where a field is deleted from the object. + */ +@interface PFDeleteOperation : PFFieldOperation + ++ (instancetype)operation; + +@end + +///-------------------------------------- +#pragma mark - Numeric Operations +///-------------------------------------- + +/*! + An operation that increases a numeric field's value by a given amount. + */ +@interface PFIncrementOperation : PFFieldOperation + +@property (nonatomic, strong, readonly) NSNumber *amount; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithAmount:(NSNumber *)amount NS_DESIGNATED_INITIALIZER; ++ (instancetype)incrementWithAmount:(NSNumber *)amount; + +@end + +///-------------------------------------- +#pragma mark - Array Operations +///-------------------------------------- + +/*! + An operation that adds a new element to an array field. + */ +@interface PFAddOperation : PFFieldOperation + +@property (nonatomic, strong, readonly) NSArray *objects; + ++ (instancetype)addWithObjects:(NSArray *)array; + +@end + +/*! + An operation that adds a new element to an array field, + only if it wasn't already present. + */ +@interface PFAddUniqueOperation : PFFieldOperation + +@property (nonatomic, strong, readonly) NSArray *objects; + ++ (instancetype)addUniqueWithObjects:(NSArray *)array; + +@end + +/*! + An operation that removes every instance of an element from + an array field. + */ +@interface PFRemoveOperation : PFFieldOperation + +@property (nonatomic, strong, readonly) NSArray *objects; + ++ (instancetype)removeWithObjects:(NSArray *)array; + +@end + +///-------------------------------------- +#pragma mark - Relation Operations +///-------------------------------------- + +/*! + An operation where a PFRelation's value is modified. + */ +@interface PFRelationOperation : PFFieldOperation + +@property (nonatomic, copy) NSString *targetClass; +@property (nonatomic, strong) NSMutableSet *relationsToAdd; +@property (nonatomic, strong) NSMutableSet *relationsToRemove; + ++ (instancetype)addRelationToObjects:(NSArray *)targets; ++ (instancetype)removeRelationToObjects:(NSArray *)targets; + +@end diff --git a/Parse/Internal/FieldOperation/PFFieldOperation.m b/Parse/Internal/FieldOperation/PFFieldOperation.m new file mode 100644 index 000000000..6bfcffbf1 --- /dev/null +++ b/Parse/Internal/FieldOperation/PFFieldOperation.m @@ -0,0 +1,586 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFieldOperation.h" + +#import "PFAssert.h" +#import "PFDecoder.h" +#import "PFInternalUtils.h" +#import "PFObject.h" +#import "PFOfflineStore.h" +#import "PFRelation.h" +#import "PFRelationPrivate.h" + +///-------------------------------------- +#pragma mark - PFFieldOperation +///-------------------------------------- + +// PFFieldOperation and its subclasses encapsulate operations that can be done on a field. +@implementation PFFieldOperation + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + PFConsistencyAssert(NO, @"Operation is invalid."); + return nil; +} + +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + PFConsistencyAssert(NO, @"Operation is invalid."); + return nil; +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + PFConsistencyAssert(NO, @"Operation is invalid."); + return nil; +} + +@end + +///-------------------------------------- +#pragma mark - Independent Operations +///-------------------------------------- + +@implementation PFSetOperation + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithValue:(id)value { + self = [super init]; + if (!self) return nil; + + PFParameterAssert(value, @"Cannot set a nil value in a PFObject."); + _value = value; + + return self; +} + ++ (id)setWithValue:(id)newValue { + return [[self alloc] initWithValue:newValue]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"set to %@", self.value]; +} + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + return [objectEncoder encodeObject:self.value]; +} + +- (PFSetOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + return self; +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + return self.value; +} + +@end + +@implementation PFDeleteOperation + ++ (instancetype)operation { + return [[self alloc] init]; +} + +- (NSString *)description { + return @"delete"; +} + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + return @{ @"__op" : @"Delete" }; +} + +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + return self; +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + return nil; +} + +@end + +///-------------------------------------- +#pragma mark - Numeric Operations +///-------------------------------------- + +@implementation PFIncrementOperation + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithAmount:(NSNumber *)amount { + self = [super init]; + if (!self) return nil; + + _amount = amount; + + return self; +} + ++ (instancetype)incrementWithAmount:(NSNumber *)newAmount { + return [[self alloc] initWithAmount:newAmount]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"increment by %@", self.amount]; +} + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + return @{ @"__op" : @"Increment", + @"amount" : self.amount }; +} + +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + if (!previous) { + return self; + } else if ([previous isKindOfClass:[PFDeleteOperation class]]) { + return [PFSetOperation setWithValue:self.amount]; + } else if ([previous isKindOfClass:[PFSetOperation class]]) { + id oldValue = ((PFSetOperation *)previous).value; + PFParameterAssert([oldValue isKindOfClass:[NSNumber class]], @"You cannot increment a non-number."); + return [PFSetOperation setWithValue:[PFInternalUtils addNumber:self.amount withNumber:oldValue]]; + } else if ([previous isKindOfClass:[PFIncrementOperation class]]) { + NSNumber *newAmount = [PFInternalUtils addNumber:self.amount + withNumber:((PFIncrementOperation *)previous).amount]; + return [PFIncrementOperation incrementWithAmount:newAmount]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + if (!oldValue) { + return self.amount; + } + + PFParameterAssert([oldValue isKindOfClass:[NSNumber class]], @"You cannot increment a non-number."); + return [PFInternalUtils addNumber:self.amount withNumber:oldValue]; +} + +@end + +///-------------------------------------- +#pragma mark - Array Operations +///-------------------------------------- + +@implementation PFAddOperation + +- (instancetype)initWithObjects:(NSArray *)array { + self = [super init]; + if (!self) return nil; + + _objects = array; + + return self; +} + ++ (instancetype)addWithObjects:(NSArray *)objects { + return [(PFAddOperation *)[self alloc] initWithObjects:objects]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"add %@", self.objects]; +} + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + NSMutableArray *encodedObjects = [objectEncoder encodeObject:self.objects]; + return @{ @"__op" : @"Add", + @"objects" : encodedObjects }; +} + +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + if (!previous) { + return self; + } else if ([previous isKindOfClass:[PFDeleteOperation class]]) { + return [PFSetOperation setWithValue:self.objects]; + } else if ([previous isKindOfClass:[PFSetOperation class]]) { + if ([((PFSetOperation *)previous).value isKindOfClass:[NSArray class]]) { + NSArray *oldArray = (NSArray *)(((PFSetOperation *)previous).value); + NSArray *newArray = [oldArray arrayByAddingObjectsFromArray:self.objects]; + return [PFSetOperation setWithValue:newArray]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"You can't add an item to a non-array." + userInfo:nil]; + } + } else if ([previous isKindOfClass:[PFAddOperation class]]) { + NSMutableArray *newObjects = [((PFAddOperation *)previous).objects mutableCopy]; + [newObjects addObjectsFromArray:self.objects]; + return [[self class] addWithObjects:newObjects]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + if (!oldValue) { + return [self.objects mutableCopy]; + } else if ([oldValue isKindOfClass:[NSArray class]]) { + return [((NSArray *)oldValue)arrayByAddingObjectsFromArray:self.objects]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +@end + +@implementation PFAddUniqueOperation + +- (instancetype)initWithObjects:(NSArray *)array { + self = [super init]; + if (!self) return nil; + + _objects = [[NSSet setWithArray:array] allObjects]; + + return self; +} + ++ (instancetype)addUniqueWithObjects:(NSArray *)objects { + return [(PFAddUniqueOperation *)[self alloc] initWithObjects:objects]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"addToSet %@", self.objects]; +} + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + NSMutableArray *encodedObjects = [objectEncoder encodeObject:self.objects]; + return @{ @"__op" : @"AddUnique", + @"objects" : encodedObjects }; +} + +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + if (!previous) { + return self; + } else if ([previous isKindOfClass:[PFDeleteOperation class]]) { + return [PFSetOperation setWithValue:self.objects]; + } else if ([previous isKindOfClass:[PFSetOperation class]]) { + if ([((PFSetOperation *)previous).value isKindOfClass:[NSArray class]]) { + NSArray *oldArray = (((PFSetOperation *)previous).value); + return [PFSetOperation setWithValue:[self applyToValue:oldArray forKey:nil]]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"You can't add an item to a non-array." + userInfo:nil]; + } + } else if ([previous isKindOfClass:[PFAddUniqueOperation class]]) { + NSArray *previousObjects = ((PFAddUniqueOperation *)previous).objects; + return [[self class] addUniqueWithObjects:[self applyToValue:previousObjects forKey:nil]]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + if (!oldValue) { + return [self.objects mutableCopy]; + } else if ([oldValue isKindOfClass:[NSArray class]]) { + NSMutableArray *newValue = [oldValue mutableCopy]; + for (id objectToAdd in self.objects) { + if ([objectToAdd isKindOfClass:[PFObject class]] && [objectToAdd objectId]) { + // Check uniqueness by objectId instead of equality. If the PFObject + // already exists in the array, replace it with the newer one. + NSUInteger index = [newValue indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { + return [obj isKindOfClass:[PFObject class]] && + [[obj objectId] isEqualToString:[objectToAdd objectId]]; + }]; + if (index == NSNotFound) { + [newValue addObject:objectToAdd]; + } else { + [newValue replaceObjectAtIndex:index withObject:objectToAdd]; + } + } else if (![newValue containsObject:objectToAdd]) { + [newValue addObject:objectToAdd]; + } + } + return newValue; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +@end + +@implementation PFRemoveOperation + +- (instancetype)initWithObjects:(NSArray *)array { + self = [super init]; + + _objects = array; + + return self; +} + ++ (id)removeWithObjects:(NSArray *)objects { + return [(PFRemoveOperation *)[self alloc] initWithObjects:objects]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"remove %@", self.objects]; +} + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + NSMutableArray *encodedObjects = [objectEncoder encodeObject:self.objects]; + return @{ @"__op" : @"Remove", + @"objects" : encodedObjects }; +} + +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + if (!previous) { + return self; + } else if ([previous isKindOfClass:[PFDeleteOperation class]]) { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"You can't remove items from a deleted array." + userInfo:nil]; + } else if ([previous isKindOfClass:[PFSetOperation class]]) { + if ([((PFSetOperation *)previous).value isKindOfClass:[NSArray class]]) { + NSArray *oldArray = ((PFSetOperation *)previous).value; + return [PFSetOperation setWithValue:[self applyToValue:oldArray forKey:nil]]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"You can't add an item to a non-array." + userInfo:nil]; + } + } else if ([previous isKindOfClass:[PFRemoveOperation class]]) { + NSArray *newObjects = [((PFRemoveOperation *)previous).objects arrayByAddingObjectsFromArray:self.objects]; + return [PFRemoveOperation removeWithObjects:newObjects]; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + if (!oldValue) { + return [self.objects mutableCopy]; + } else if ([oldValue isKindOfClass:[NSArray class]]) { + NSMutableArray *newValue = [((NSArray *)oldValue)mutableCopy]; + [newValue removeObjectsInArray:self.objects]; + + // Remove the removed objects from objectsToBeRemoved -- the items + // remaining should be ones that weren't removed by object equality. + NSMutableArray *objectsToBeRemoved = [self.objects mutableCopy]; + [objectsToBeRemoved removeObjectsInArray:newValue]; + for (id objectToRemove in objectsToBeRemoved) { + if ([objectToRemove isKindOfClass:[PFObject class]] && [objectToRemove objectId]) { + NSIndexSet *indexes = [newValue indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { + return ([obj isKindOfClass:[PFObject class]] && + [[obj objectId] isEqualToString:[objectToRemove objectId]]); + }]; + if ([indexes count] != 0) { + [newValue removeObjectsAtIndexes:indexes]; + } + } + } + return newValue; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +@end + +///-------------------------------------- +#pragma mark - Relation Operations +///-------------------------------------- + +@implementation PFRelationOperation +@synthesize targetClass; + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _relationsToAdd = [NSMutableSet set]; + _relationsToRemove = [NSMutableSet set]; + + return self; +} + ++ (instancetype)addRelationToObjects:(NSArray *)targets { + PFRelationOperation *op = [[self alloc] init]; + if (targets.count > 0) { + op.targetClass = [[targets firstObject] parseClassName]; + } + + for (PFObject *target in targets) { + if (![[target parseClassName] isEqualToString:op.targetClass]) { + [NSException raise:NSInvalidArgumentException + format:@"All objects in a relation must be of the same class."]; + } + [op.relationsToAdd addObject:target]; + } + + return op; +} + ++ (instancetype)removeRelationToObjects:(NSArray *)targets { + PFRelationOperation *operation = [[self alloc] init]; + if (targets.count > 0) { + operation.targetClass = [[targets objectAtIndex:0] parseClassName]; + } + + for (PFObject *target in targets) { + if (![[target parseClassName] isEqualToString:operation.targetClass]) { + [NSException raise:NSInvalidArgumentException + format:@"All objects in a relation must be of the same class."]; + } + [operation.relationsToRemove addObject:target]; + } + + return operation; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"PFRelationOperation<%@> add:%@ remove:%@", + self.targetClass, + self.relationsToAdd, + self.relationsToRemove]; +} + +- (NSArray *)_convertToArrayInSet:(NSSet *)set withObjectEncoder:(PFEncoder *)objectEncoder { + NSMutableArray *array = [NSMutableArray arrayWithCapacity:set.count]; + for (PFObject *object in set) { + id encodedDict = [objectEncoder encodeObject:object]; + [array addObject:encodedDict]; + } + return array; +} + +- (id)encodeWithObjectEncoder:(PFEncoder *)objectEncoder { + NSDictionary *addDict = nil; + NSDictionary *removeDict = nil; + if (self.relationsToAdd.count > 0) { + NSArray *array = [self _convertToArrayInSet:self.relationsToAdd withObjectEncoder:objectEncoder]; + addDict = @{ @"__op" : @"AddRelation", + @"objects" : array }; + } + if (self.relationsToRemove.count > 0) { + NSArray *array = [self _convertToArrayInSet:self.relationsToRemove withObjectEncoder:objectEncoder]; + removeDict = @{ @"__op" : @"RemoveRelation", + @"objects" : array }; + } + + if (addDict && removeDict) { + return @{ @"__op" : @"Batch", + @"ops" : @[ addDict, removeDict ] }; + } + + if (addDict) { + return addDict; + } + + if (removeDict) { + return removeDict; + } + + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"A PFRelationOperation was created without any data." + userInfo:nil]; +} + +- (PFFieldOperation *)mergeWithPrevious:(PFFieldOperation *)previous { + if (!previous) { + return self; + } else if ([previous isKindOfClass:[PFDeleteOperation class]]) { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"You can't modify a relation after deleting it." + userInfo:nil]; + } else if ([previous isKindOfClass:[PFRelationOperation class]]) { + PFRelationOperation *previousOperation = (PFRelationOperation *)previous; + if (previousOperation.targetClass && + ![previousOperation.targetClass isEqualToString:self.targetClass]) { + [NSException raise:NSInvalidArgumentException + format:@"Related object object must be of class %@, but %@ was passed in", + previousOperation.targetClass, + self.targetClass]; + } else { + //TODO: (nlutsenko) This logic seems to be messed up. We should return a new operation here, also merging logic seems funky. + NSSet *newRelationsToAdd = [self.relationsToAdd copy]; + NSSet *newRelationsToRemove = [self.relationsToRemove copy]; + [self.relationsToAdd removeAllObjects]; + [self.relationsToRemove removeAllObjects]; + + for (NSString *objectId in previousOperation.relationsToAdd) { + [self.relationsToRemove removeObject:objectId]; + [self.relationsToAdd addObject:objectId]; + } + for (NSString *objectId in previousOperation.relationsToRemove) { + [self.relationsToRemove removeObject:objectId]; + [self.relationsToRemove addObject:objectId]; + } + + for (NSString *objectId in newRelationsToAdd) { + [self.relationsToRemove removeObject:objectId]; + [self.relationsToAdd addObject:objectId]; + } + for (NSString *objectId in newRelationsToRemove) { + [self.relationsToRemove removeObject:objectId]; + [self.relationsToRemove addObject:objectId]; + } + } + return self; + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } +} + +- (id)applyToValue:(id)oldValue forKey:(NSString *)key { + PFRelation *relation = nil; + if (!oldValue) { + relation = [PFRelation relationWithTargetClass:self.targetClass]; + } else if ([oldValue isKindOfClass:[PFRelation class]]) { + relation = oldValue; + if (self.targetClass) { + if (relation.targetClass) { + if (![relation.targetClass isEqualToString:targetClass]) { + [NSException raise:NSInvalidArgumentException + format:@"Related object object must be of class %@, but %@ was passed in", + relation.targetClass, + self.targetClass]; + } + } else { + relation.targetClass = self.targetClass; + } + } + } else { + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Operation is invalid after previous operation." + userInfo:nil]; + } + + for (PFObject *object in self.relationsToAdd) { + [relation _addKnownObject:object]; + } + for (PFObject *object in self.relationsToRemove) { + [relation _removeKnownObject:object]; + } + + return relation; +} + +@end diff --git a/Parse/Internal/FieldOperation/PFFieldOperationDecoder.h b/Parse/Internal/FieldOperation/PFFieldOperationDecoder.h new file mode 100644 index 000000000..831418ac0 --- /dev/null +++ b/Parse/Internal/FieldOperation/PFFieldOperationDecoder.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFDecoder; +@class PFFieldOperation; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFFieldOperationDecoder : NSObject + +///-------------------------------------- +/// @name Init +///-------------------------------------- + ++ (instancetype)defaultDecoder; + +///-------------------------------------- +/// @name Decoding +///-------------------------------------- + +/*! + Converts a parsed JSON object into a PFFieldOperation. + + @param encoded An NSDictionary containing an __op field. + @returns An NSObject that conforms to PFFieldOperation. + */ +- (PFFieldOperation *)decode:(NSDictionary *)encoded withDecoder:(PFDecoder *)decoder; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/FieldOperation/PFFieldOperationDecoder.m b/Parse/Internal/FieldOperation/PFFieldOperationDecoder.m new file mode 100644 index 000000000..1c5a303ea --- /dev/null +++ b/Parse/Internal/FieldOperation/PFFieldOperationDecoder.m @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFieldOperationDecoder.h" + +#import "PFAssert.h" +#import "PFDecoder.h" +#import "PFFieldOperation.h" + +@interface PFFieldOperationDecoder () { + NSMutableDictionary *_operationDecoders; +} + +@end + +typedef PFFieldOperation * (^PFFieldOperationDecodingBlock_)(NSDictionary *encoded, PFDecoder *decoder); + +@implementation PFFieldOperationDecoder + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _operationDecoders = [NSMutableDictionary dictionary]; + [self _registerDefaultOperationDecoders]; + + return self; +} + ++ (instancetype)defaultDecoder { + static PFFieldOperationDecoder *decoder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + decoder = [[self alloc] init]; + }); + return decoder; +} + +///-------------------------------------- +#pragma mark - Setup +///-------------------------------------- + +- (void)_registerDecoderForOperationWithName:(NSString *)name block:(PFFieldOperationDecodingBlock_)block { + _operationDecoders[name] = [block copy]; +} + +- (void)_registerDefaultOperationDecoders { + @weakify(self); + [self _registerDecoderForOperationWithName:@"Batch" block:^(NSDictionary *encoded, PFDecoder *decoder) { + @strongify(self); + PFFieldOperation *op = nil; + NSArray *ops = encoded[@"ops"]; + for (id maybeEncodedNextOp in ops) { + PFFieldOperation *nextOp = nil; + if ([maybeEncodedNextOp isKindOfClass:[PFFieldOperation class]]) { + nextOp = maybeEncodedNextOp; + } else { + nextOp = [self decode:maybeEncodedNextOp withDecoder:decoder]; + } + op = [nextOp mergeWithPrevious:op]; + } + return op; + }]; + + [self _registerDecoderForOperationWithName:@"Delete" block:^(NSDictionary *encoded, PFDecoder *decoder) { + // Deleting has no state, so it can be a singleton. + static PFDeleteOperation *deleteOperation = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + deleteOperation = [[PFDeleteOperation alloc] init]; + }); + return deleteOperation; + }]; + + [self _registerDecoderForOperationWithName:@"Increment" block:^(NSDictionary *encoded, PFDecoder *decoder) { + return [PFIncrementOperation incrementWithAmount:encoded[@"amount"]]; + }]; + + [self _registerDecoderForOperationWithName:@"Add" block:^(NSDictionary *encoded, PFDecoder *decoder) { + NSArray *objects = [decoder decodeObject:encoded[@"objects"]]; + return [PFAddOperation addWithObjects:objects]; + }]; + + [self _registerDecoderForOperationWithName:@"AddUnique" block:^(NSDictionary *encoded, PFDecoder *decoder) { + NSArray *objects = [decoder decodeObject:encoded[@"objects"]]; + return [PFAddUniqueOperation addUniqueWithObjects:objects]; + }]; + + [self _registerDecoderForOperationWithName:@"Remove" block:^(NSDictionary *encoded, PFDecoder *decoder) { + NSArray *objects = [decoder decodeObject:encoded[@"objects"]]; + return [PFRemoveOperation removeWithObjects:objects]; + }]; + + [self _registerDecoderForOperationWithName:@"AddRelation" block:^(NSDictionary *encoded, PFDecoder *decoder) { + NSArray *objects = [decoder decodeObject:encoded[@"objects"]]; + return [PFRelationOperation addRelationToObjects:objects]; + }]; + + [self _registerDecoderForOperationWithName:@"RemoveRelation" block:^(NSDictionary *encoded, PFDecoder *decoder) { + NSArray *objects = [decoder decodeObject:encoded[@"objects"]]; + return [PFRelationOperation removeRelationToObjects:objects]; + }]; +} + +///-------------------------------------- +#pragma mark - Decoding +///-------------------------------------- + +- (PFFieldOperation *)decode:(NSDictionary *)encoded withDecoder:(PFDecoder *)decoder { + NSString *operationName = encoded[@"__op"]; + PFFieldOperationDecodingBlock_ block = _operationDecoders[operationName]; + PFConsistencyAssert(block, @"Unable to decode operation of type %@.", operationName); + return block(encoded, decoder); +} + +@end diff --git a/Parse/Internal/File/Controller/PFFileController.h b/Parse/Internal/File/Controller/PFFileController.h new file mode 100644 index 000000000..b024e86c2 --- /dev/null +++ b/Parse/Internal/File/Controller/PFFileController.h @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +#import "PFDataProvider.h" + +@class BFCancellationToken; +@class BFTask; +@class PFFileState; + +@interface PFFileController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +@property (nonatomic, copy, readonly) NSString *cacheFilesDirectoryPath; +@property (nonatomic, copy, readonly) NSString *stagedFilesDirectoryPath; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithDataSource:(id)dataSource; + + +///-------------------------------------- +/// @name Download +///-------------------------------------- + +/*! + Downloads a file asynchronously with a given state. + + @param fileState File state to download the file for. + @param cancellationToken Cancellation token. + @param progressBlock Progress block to call (optional). + + @returns `BFTask` with a result set to `nil`. + */ +- (BFTask *)downloadFileAsyncWithState:(PFFileState *)fileState + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock; + +/*! + Downloads a file asynchronously with a given state and yields a stream to the live download of that file. + + @param fileState File state to download the file for. + @param cancellationToken Cancellation token. + @param progressBlock Progress block to call (optional). + + @return `BFTask` with a result set to live `NSInputStream` of the file. + */ +- (BFTask *)downloadFileStreamAsyncWithState:(PFFileState *)fileState + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock; + +///-------------------------------------- +/// @name Upload +///-------------------------------------- + +/*! + Uploads a file asynchronously from file path for a given file state. + + @param fileState File state to upload the file for. + @param sourceFilePath Source file path. + @param sessionToken Session token to use. + @param cancellationToken Cancellation token. + @param progressBlock Progress block to call (optional). + + @returns `BFTask` with a result set to `PFFileState` of uploaded file. + */ +- (BFTask *)uploadFileAsyncWithState:(PFFileState *)fileState + sourceFilePath:(NSString *)sourceFilePath + sessionToken:(NSString *)sessionToken + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock; + +///-------------------------------------- +/// @name Cache +///-------------------------------------- + +- (BFTask *)clearFileCacheAsync; + +- (NSString *)cachedFilePathForFileState:(PFFileState *)fileState; + +@end diff --git a/Parse/Internal/File/Controller/PFFileController.m b/Parse/Internal/File/Controller/PFFileController.m new file mode 100644 index 000000000..9f5a8afb4 --- /dev/null +++ b/Parse/Internal/File/Controller/PFFileController.m @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFileController.h" + +#import +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFBlockRetryer.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFFileManager.h" +#import "PFFileState.h" +#import "PFHash.h" +#import "PFMacros.h" +#import "PFRESTFileCommand.h" + +static NSString *const PFFileControllerCacheDirectoryName_ = @"PFFileCache"; +static NSString *const PFFileControllerStagingDirectoryName_ = @"PFFileStaging"; + +@interface PFFileController () { + NSMutableDictionary *_downloadTasks; // { "urlString" : BFTask } + NSMutableDictionary *_downloadProgressBlocks; // { "urlString" : [ block1, block2 ] } + dispatch_queue_t _downloadDataAccessQueue; +} + +@end + +@implementation PFFileController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + _downloadTasks = [NSMutableDictionary dictionary]; + _downloadProgressBlocks = [NSMutableDictionary dictionary]; + _downloadDataAccessQueue = dispatch_queue_create("com.parse.fileController.download", DISPATCH_QUEUE_SERIAL); + + return self; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Download +///-------------------------------------- + +- (BFTask *)downloadFileAsyncWithState:(PFFileState *)fileState + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock { + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + [self _addFileDownloadProgressBlock:progressBlock forFileWithState:fileState]; + + BFTask *resultTask = [self _fileDownloadResultTaskForFileWithState:fileState]; + if (!resultTask) { + NSURL *url = [NSURL URLWithString:fileState.urlString]; + NSString *temporaryPath = [self _temporaryFileDownloadPathForFileState:fileState]; + + PFProgressBlock unifyingProgressBlock = [self _fileDownloadUnifyingProgressBlockForFileState:fileState]; + resultTask = [self.dataSource.commandRunner runFileDownloadCommandAsyncWithFileURL:url + targetFilePath:temporaryPath + cancellationToken:cancellationToken + progressBlock:unifyingProgressBlock]; + resultTask = [[resultTask continueWithSuccessBlock:^id(BFTask *task) { + // TODO: (nlutsenko) Create `+ moveAsync` in PFFileManager + NSError *fileError = nil; + [[NSFileManager defaultManager] moveItemAtPath:temporaryPath + toPath:[self cachedFilePathForFileState:fileState] + error:&fileError]; + if (fileError && fileError.code != NSFileWriteFileExistsError) { + return fileError; + } + return nil; + }] continueWithBlock:^id(BFTask *task) { + dispatch_barrier_async(_downloadDataAccessQueue, ^{ + [_downloadTasks removeObjectForKey:fileState.urlString]; + [_downloadProgressBlocks removeObjectForKey:fileState.urlString]; + }); + return task; + }]; + dispatch_barrier_async(_downloadDataAccessQueue, ^{ + _downloadTasks[fileState.urlString] = resultTask; + }); + } + return resultTask; + }]; +} + +- (BFTask *)downloadFileStreamAsyncWithState:(PFFileState *)fileState + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + NSString *filePath = [self _temporaryFileDownloadPathForFileState:fileState]; + NSInputStream *stream = [NSInputStream inputStreamWithFileAtPath:filePath]; + [self downloadFileAsyncWithState:fileState + cancellationToken:cancellationToken + progressBlock:^(int percentDone) { + [taskCompletionSource trySetResult:stream]; + + if (progressBlock) { + progressBlock(percentDone); + } + }]; + return taskCompletionSource.task; + }]; +} + +- (BFTask *)_fileDownloadResultTaskForFileWithState:(PFFileState *)state { + __block BFTask *resultTask = nil; + dispatch_sync(_downloadDataAccessQueue, ^{ + resultTask = _downloadTasks[state.urlString]; + }); + return resultTask; +} + +- (PFProgressBlock)_fileDownloadUnifyingProgressBlockForFileState:(PFFileState *)fileState { + return ^(int progress) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __block NSArray *blocks = nil; + dispatch_sync(_downloadDataAccessQueue, ^{ + blocks = [_downloadProgressBlocks[fileState.urlString] copy]; + }); + if (blocks.count != 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + for (PFProgressBlock block in blocks) { + block(progress); + } + }); + } + }); + }; +} + +- (void)_addFileDownloadProgressBlock:(PFProgressBlock)block forFileWithState:(PFFileState *)state { + if (!block) { + return; + } + + dispatch_barrier_async(_downloadDataAccessQueue, ^{ + NSMutableArray *progressBlocks = _downloadProgressBlocks[state.urlString]; + if (!progressBlocks) { + progressBlocks = [NSMutableArray arrayWithObject:block]; + _downloadProgressBlocks[state.urlString] = progressBlocks; + } else { + [progressBlocks addObject:block]; + } + }); +} + +- (NSString *)_temporaryFileDownloadPathForFileState:(PFFileState *)fileState { + return [NSTemporaryDirectory() stringByAppendingPathComponent:PFMD5HashFromString(fileState.urlString)]; +} + +///-------------------------------------- +#pragma mark - Upload +///-------------------------------------- + +- (BFTask *)uploadFileAsyncWithState:(PFFileState *)fileState + sourceFilePath:(NSString *)sourceFilePath + sessionToken:(NSString *)sessionToken + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock { + PFRESTFileCommand *command = [PFRESTFileCommand uploadCommandForFileWithName:fileState.name + sessionToken:sessionToken]; + + @weakify(self); + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + return [[[self.dataSource.commandRunner runFileUploadCommandAsync:command + withContentType:fileState.mimeType + contentSourceFilePath:sourceFilePath + options:PFCommandRunningOptionRetryIfFailed + cancellationToken:cancellationToken + progressBlock:progressBlock] continueWithSuccessBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + PFFileState *fileState = [[PFFileState alloc] initWithName:result.result[@"name"] + urlString:result.result[@"url"] + mimeType:nil]; + return fileState; + }] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + + NSString *finalPath = [self cachedFilePathForFileState:task.result]; + NSError *error = nil; + [[NSFileManager defaultManager] moveItemAtPath:sourceFilePath + toPath:finalPath + error:&error]; + if (error) { + return [BFTask taskWithError:error]; + } + return task; + }]; +} + +///-------------------------------------- +#pragma mark - Cache +///-------------------------------------- + +- (NSString *)cachedFilePathForFileState:(PFFileState *)fileState { + if (!fileState.urlString) { + return nil; + } + + NSString *filename = [fileState.urlString lastPathComponent]; + NSString *path = [self.cacheFilesDirectoryPath stringByAppendingPathComponent:filename]; + return path; +} + +- (NSString *)cacheFilesDirectoryPath { + NSString *path = [self.dataSource.fileManager parseCacheItemPathForPathComponent:PFFileControllerCacheDirectoryName_]; + [[PFFileManager createDirectoryIfNeededAsyncAtPath:path] waitForResult:nil withMainThreadWarning:NO]; + return path; +} + +- (BFTask *)clearFileCacheAsync { + NSString *path = [self cacheFilesDirectoryPath]; + return [PFFileManager removeDirectoryContentsAsyncAtPath:path]; +} + +///-------------------------------------- +#pragma mark - Staging +///-------------------------------------- + +- (NSString *)stagedFilesDirectoryPath { + NSString *folderPath = [self.dataSource.fileManager parseLocalSandboxDataDirectoryPath]; + NSString *path = [folderPath stringByAppendingPathComponent:PFFileControllerStagingDirectoryName_]; + [[PFFileManager createDirectoryIfNeededAsyncAtPath:path] waitForResult:nil withMainThreadWarning:NO]; + return path; +} + +@end diff --git a/Parse/Internal/File/PFFile_Private.h b/Parse/Internal/File/PFFile_Private.h new file mode 100644 index 000000000..f9956eb02 --- /dev/null +++ b/Parse/Internal/File/PFFile_Private.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#else +#import +#import +#endif + +#import "PFFileState.h" + +@class BFTask; + +@interface PFFile (Private) + +@property (nonatomic, strong, readonly) PFFileState *state; + ++ (instancetype)fileWithName:(NSString *)name url:(NSString *)url; + +// +// Download +- (BFTask *)_getDataAsyncWithProgressBlock:(PFProgressBlock)block; +- (NSString *)_cachedFilePath; + +@end diff --git a/Parse/Internal/File/State/PFFileState.h b/Parse/Internal/File/State/PFFileState.h new file mode 100644 index 000000000..7953b8c28 --- /dev/null +++ b/Parse/Internal/File/State/PFFileState.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFBaseState.h" + +@interface PFFileState : PFBaseState + +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, copy, readonly) NSString *urlString; + +@property (nonatomic, copy, readonly) NSString *mimeType; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithState:(PFFileState *)state; +- (instancetype)initWithName:(NSString *)name + urlString:(NSString *)urlString + mimeType:(NSString *)mimeType; + +@end diff --git a/Parse/Internal/File/State/PFFileState.m b/Parse/Internal/File/State/PFFileState.m new file mode 100644 index 000000000..4e4adc0e3 --- /dev/null +++ b/Parse/Internal/File/State/PFFileState.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFileState.h" +#import "PFFileState_Private.h" + +#import "PFMutableFileState.h" +#import "PFPropertyInfo.h" + +@implementation PFFileState + +///-------------------------------------- +#pragma mark - PFBaseStateSubclass +///-------------------------------------- + ++ (NSDictionary *)propertyAttributes { + return @{ + @"name" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"urlString" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"mimeType" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + }; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithState:(PFFileState *)state { + return [super initWithState:state]; +} + +- (instancetype)initWithName:(NSString *)name urlString:(NSString *)urlString mimeType:(NSString *)mimeType { + self = [super init]; + if (!self) return nil; + + _name = (name ? [name copy] : @"file"); + _urlString = [urlString copy]; + _mimeType = [mimeType copy]; + + return self; +} + +///-------------------------------------- +#pragma mark - Mutable Copying +///-------------------------------------- + +- (id)copyWithZone:(NSZone *)zone { + return [[PFFileState allocWithZone:zone] initWithState:self]; +} + +- (instancetype)mutableCopyWithZone:(NSZone *)zone { + return [[PFMutableFileState allocWithZone:zone] initWithState:self]; +} + +@end diff --git a/Parse/Internal/File/State/PFFileState_Private.h b/Parse/Internal/File/State/PFFileState_Private.h new file mode 100644 index 000000000..8d1c4530c --- /dev/null +++ b/Parse/Internal/File/State/PFFileState_Private.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFileState.h" + +@interface PFFileState () + +@property (nonatomic, copy, readwrite) NSString *name; +@property (nonatomic, copy, readwrite) NSString *urlString; +@property (nonatomic, copy, readwrite) NSString *mimeType; + +@end diff --git a/Parse/Internal/File/State/PFMutableFileState.h b/Parse/Internal/File/State/PFMutableFileState.h new file mode 100644 index 000000000..1920abf9e --- /dev/null +++ b/Parse/Internal/File/State/PFMutableFileState.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFileState.h" + +@interface PFMutableFileState : PFFileState + +@property (nonatomic, copy, readwrite) NSString *name; +@property (nonatomic, copy, readwrite) NSString *urlString; +@property (nonatomic, copy, readwrite) NSString *mimeType; + +@end diff --git a/Parse/Internal/File/State/PFMutableFileState.m b/Parse/Internal/File/State/PFMutableFileState.m new file mode 100644 index 000000000..53db52c84 --- /dev/null +++ b/Parse/Internal/File/State/PFMutableFileState.m @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableFileState.h" + +@implementation PFMutableFileState + +@dynamic name; +@dynamic urlString; +@dynamic mimeType; + +@end diff --git a/Parse/Internal/HTTPRequest/PFHTTPRequest.h b/Parse/Internal/HTTPRequest/PFHTTPRequest.h new file mode 100644 index 000000000..e6394e4e8 --- /dev/null +++ b/Parse/Internal/HTTPRequest/PFHTTPRequest.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef Parse_PFHTTPRequest_h +#define Parse_PFHTTPRequest_h + +#import + +static NSString *const PFHTTPRequestMethodGET = @"GET"; +static NSString *const PFHTTPRequestMethodHEAD = @"HEAD"; +static NSString *const PFHTTPRequestMethodDELETE = @"DELETE"; +static NSString *const PFHTTPRequestMethodPOST = @"POST"; +static NSString *const PFHTTPRequestMethodPUT = @"PUT"; + +static NSString *const PFHTTPRequestHeaderNameContentType = @"Content-Type"; +static NSString *const PFHTTPRequestHeaderNameContentLength = @"Content-Length"; + +#endif diff --git a/Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.h b/Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.h new file mode 100644 index 000000000..b3bfb70dd --- /dev/null +++ b/Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFHTTPURLRequestConstructor : NSObject + ++ (NSMutableURLRequest *)urlRequestWithURL:(NSURL *)url + httpMethod:(NSString *)httpMethod + httpHeaders:(NSDictionary *)httpHeaders + parameters:(NSDictionary *)parameters; + +@end diff --git a/Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.m b/Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.m new file mode 100644 index 000000000..e948c3d9f --- /dev/null +++ b/Parse/Internal/HTTPRequest/PFHTTPURLRequestConstructor.m @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPURLRequestConstructor.h" + +#import "PFAssert.h" +#import "PFHTTPRequest.h" +#import "PFURLConstructor.h" + +static NSString *const PFHTTPURLRequestContentTypeJSON = @"application/json; charset=utf8"; + +@implementation PFHTTPURLRequestConstructor + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + ++ (NSMutableURLRequest *)urlRequestWithURL:(NSURL *)url + httpMethod:(NSString *)httpMethod + httpHeaders:(NSDictionary *)httpHeaders + parameters:(NSDictionary *)parameters { + NSParameterAssert(url != nil); + NSParameterAssert(httpMethod != nil); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + + request.HTTPMethod = httpMethod; + request.allHTTPHeaderFields = httpHeaders; + + if (parameters != nil) { + PFConsistencyAssert([httpMethod isEqualToString:PFHTTPRequestMethodPOST] || + [httpMethod isEqualToString:PFHTTPRequestMethodPUT], + @"Can't create %@ request with json body.", httpMethod); + + [request setValue:PFHTTPURLRequestContentTypeJSON forHTTPHeaderField:PFHTTPRequestHeaderNameContentType]; + + NSError *error = nil; + [request setHTTPBody:[NSJSONSerialization dataWithJSONObject:parameters + options:(NSJSONWritingOptions)0 + error:&error]]; + PFConsistencyAssert(error == nil, @"Failed to serialize JSON with error = %@", error); + } + return request; +} + +@end diff --git a/Parse/Internal/HTTPRequest/PFURLConstructor.h b/Parse/Internal/HTTPRequest/PFURLConstructor.h new file mode 100644 index 000000000..3948aa50a --- /dev/null +++ b/Parse/Internal/HTTPRequest/PFURLConstructor.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/*! + This enum is being used to distinguish and encode different types of URL Components. + Things like Path or Query. + + @warning It currently lacks support for scheme, login, password, fragment + Whenever new enum type is added - make sure you add support for it to relevant methods. + */ +typedef NS_ENUM(uint8_t, PFURLComponentType) +{ + PFURLComponentTypePath, + PFURLComponentTypeQuery +}; + +@interface PFURLConstructor : NSObject + ++ (NSURL *)URLFromBaseURL:(NSURL *)baseURL + path:(NSString *)path; ++ (NSURL *)URLFromBaseURL:(NSURL *)baseURL + queryParameters:(NSDictionary *)queryParameters; ++ (NSURL *)URLFromBaseURL:(NSURL *)baseURL + path:(NSString *)path + queryParameters:(NSDictionary *)queryParameters; + ++ (NSString *)stringByAddingPercentEscapesToString:(NSString *)string + forURLComponentType:(PFURLComponentType)type; + +@end diff --git a/Parse/Internal/HTTPRequest/PFURLConstructor.m b/Parse/Internal/HTTPRequest/PFURLConstructor.m new file mode 100644 index 000000000..ac07754b4 --- /dev/null +++ b/Parse/Internal/HTTPRequest/PFURLConstructor.m @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFURLConstructor.h" + +#import "PFAssert.h" + +@implementation PFURLConstructor + +///-------------------------------------- +#pragma mark - Basic +///-------------------------------------- + ++ (NSURL *)URLFromBaseURL:(NSURL *)baseURL + path:(NSString *)path { + return [self URLFromBaseURL:baseURL path:path queryParameters:nil]; +} + ++ (NSURL *)URLFromBaseURL:(NSURL *)baseURL + queryParameters:(NSDictionary *)queryParameters { + return [self URLFromBaseURL:baseURL path:nil queryParameters:queryParameters]; +} + ++ (NSURL *)URLFromBaseURL:(NSURL *)baseURL + path:(NSString *)path + queryParameters:(NSDictionary *)queryParameters { + if (!baseURL) { + return nil; + } + + NSString *escapedPath = [self stringByAddingPercentEscapesToString:path + forURLComponentType:PFURLComponentTypePath]; + NSString *escapedQuery = [self _URLQueryStringFromQueryParameters:queryParameters]; + + NSMutableString *relativeString = (escapedPath ? [escapedPath mutableCopy] : [NSMutableString string]); + if (escapedQuery) { + [relativeString appendFormat:@"?%@", escapedQuery]; + } + + return [NSURL URLWithString:relativeString relativeToURL:baseURL]; +} + +///-------------------------------------- +#pragma mark - Escaping +///-------------------------------------- + ++ (NSString *)stringByAddingPercentEscapesToString:(NSString *)string + forURLComponentType:(PFURLComponentType)type { + PFParameterAssert(type <= PFURLComponentTypeQuery, @"`type` should only be of PFURLComponentType"); + + if (!string) { + return nil; + } + + static NSString *reservedCharacters = @"!*'();:@&=+$,/?%#[]"; + NSString *escapedString = nil; + + switch (type) { + case PFURLComponentTypePath: + { + static NSString *pathSeparator = @"/"; + + NSArray *components = [string componentsSeparatedByString:pathSeparator]; + if ([components count]) { + NSMutableArray *escapedComponents = [NSMutableArray arrayWithCapacity:[components count]]; + for (NSString *component in components) { + NSString *escapedComponent = [self _stringByAddingPercentEscapesToString:component + withReservedCharacters:reservedCharacters]; + [escapedComponents addObject:escapedComponent]; + } + escapedString = [escapedComponents componentsJoinedByString:pathSeparator]; + } else { + escapedString = [self _stringByAddingPercentEscapesToString:string + withReservedCharacters:reservedCharacters]; + } + } + break; + case PFURLComponentTypeQuery: + { + escapedString = [self _stringByAddingPercentEscapesToString:string + withReservedCharacters:reservedCharacters]; + } + break; + default:break; + } + + return escapedString; +} + ++ (NSString *)_stringByAddingPercentEscapesToString:(NSString *)string + withReservedCharacters:(NSString *)reservedCharacters { + CFStringRef escapedString = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, + (__bridge CFStringRef)string, + NULL, // Allowed characters + (__bridge CFStringRef)reservedCharacters, + kCFStringEncodingUTF8); + return CFBridgingRelease(escapedString); +} + +///-------------------------------------- +#pragma mark - URLQuery +///-------------------------------------- + ++ (NSString *)_URLQueryStringFromQueryParameters:(NSDictionary *)parameters { + if ([parameters count] == 0) { + return nil; + } + + NSMutableArray *encodedParameters = [NSMutableArray array]; + [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [encodedParameters addObject:[self _URLQueryParameterWithKey:key value:obj]]; + }]; + return [encodedParameters componentsJoinedByString:@"&"]; + +} + ++ (NSString *)_URLQueryParameterWithKey:(NSString *)key value:(NSString *)value { + NSString *string = [NSString stringWithFormat:@"%@=%@", + [self stringByAddingPercentEscapesToString:key forURLComponentType:PFURLComponentTypeQuery], + [self stringByAddingPercentEscapesToString:value forURLComponentType:PFURLComponentTypeQuery]]; + return string; +} + +@end diff --git a/Parse/Internal/Installation/Constants/PFInstallationConstants.h b/Parse/Internal/Installation/Constants/PFInstallationConstants.h new file mode 100644 index 000000000..c6299df59 --- /dev/null +++ b/Parse/Internal/Installation/Constants/PFInstallationConstants.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +extern NSString *const PFInstallationKeyParseVersion; +extern NSString *const PFInstallationKeyDeviceType; +extern NSString *const PFInstallationKeyInstallationId; +extern NSString *const PFInstallationKeyDeviceToken; +extern NSString *const PFInstallationKeyAppName; +extern NSString *const PFInstallationKeyAppVersion; +extern NSString *const PFInstallationKeyAppIdentifier; +extern NSString *const PFInstallationKeyTimeZone; +extern NSString *const PFInstallationKeyBadge; +extern NSString *const PFInstallationKeyChannels; diff --git a/Parse/Internal/Installation/Constants/PFInstallationConstants.m b/Parse/Internal/Installation/Constants/PFInstallationConstants.m new file mode 100644 index 000000000..b0b53742f --- /dev/null +++ b/Parse/Internal/Installation/Constants/PFInstallationConstants.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInstallationConstants.h" + +NSString *const PFInstallationKeyParseVersion = @"parseVersion"; +NSString *const PFInstallationKeyDeviceType = @"deviceType"; +NSString *const PFInstallationKeyInstallationId = @"installationId"; +NSString *const PFInstallationKeyDeviceToken = @"deviceToken"; +NSString *const PFInstallationKeyAppName = @"appName"; +NSString *const PFInstallationKeyAppVersion = @"appVersion"; +NSString *const PFInstallationKeyAppIdentifier = @"appIdentifier"; +NSString *const PFInstallationKeyTimeZone = @"timeZone"; +NSString *const PFInstallationKeyBadge = @"badge"; +NSString *const PFInstallationKeyChannels = @"channels"; diff --git a/Parse/Internal/Installation/Controller/PFInstallationController.h b/Parse/Internal/Installation/Controller/PFInstallationController.h new file mode 100644 index 000000000..3c70b3ee2 --- /dev/null +++ b/Parse/Internal/Installation/Controller/PFInstallationController.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreDataProvider.h" +#import "PFObjectControlling.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFInstallationController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource; ++ (instancetype)controllerWithDataSource:(id)dataSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Installation/Controller/PFInstallationController.m b/Parse/Internal/Installation/Controller/PFInstallationController.m new file mode 100644 index 000000000..66d09cc63 --- /dev/null +++ b/Parse/Internal/Installation/Controller/PFInstallationController.m @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInstallationController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCurrentInstallationController.h" +#import "PFInstallationPrivate.h" +#import "PFObjectController.h" +#import "PFObjectPrivate.h" + +@implementation PFInstallationController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Fetch +///-------------------------------------- + +- (BFTask *)fetchObjectAsync:(PFInstallation *)object withSessionToken:(nullable NSString *)sessionToken { + @weakify(self); + return [[[self.objectController fetchObjectAsync:object + withSessionToken:sessionToken] continueWithBlock:^id(BFTask *task) { + @strongify(self); + + // Do not attempt to resave an object if LDS is enabled, since changing objectId is not allowed. + if (self.currentInstallationController.storageType == PFCurrentObjectStorageTypeOfflineStore) { + return task; + } + + if (task.faulted && task.error.code == kPFErrorObjectNotFound) { + @synchronized (object.lock) { + // Retry the fetch as a save operation because this Installation was deleted on the server. + // We always want [currentInstallation fetch] to succeed. + object.objectId = nil; + [object _markAllFieldsDirty]; + return [[object saveAsync:nil] continueWithSuccessResult:object]; + } + } + return task; + }] continueWithBlock:^id(BFTask *task) { + @strongify(self); + // Roll-forward the previous task. + return [[self.currentInstallationController saveCurrentObjectAsync:object] continueWithResult:task]; + }]; +} + +- (BFTask *)processFetchResultAsync:(NSDictionary *)result forObject:(PFInstallation *)object { + @weakify(self); + return [[self.objectController processFetchResultAsync:result forObject:object] continueWithBlock:^id(BFTask *task) { + @strongify(self); + // Roll-forward the previous task. + return [[self.currentInstallationController saveCurrentObjectAsync:object] continueWithResult:task]; + }]; +} + +///-------------------------------------- +#pragma mark - Delete +///-------------------------------------- + +- (BFTask *)deleteObjectAsync:(PFObject *)object withSessionToken:(nullable NSString *)sessionToken { + PFConsistencyAssert(NO, @"Installations cannot be deleted."); + return nil; +} + +- (BFTask *)processDeleteResultAsync:(nullable NSDictionary *)result forObject:(PFObject *)object { + PFConsistencyAssert(NO, @"Installations cannot be deleted."); + return nil; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (id)objectController { + return self.dataSource.objectController; +} + +- (PFCurrentInstallationController *)currentInstallationController { + return self.dataSource.currentInstallationController; +} + +@end diff --git a/Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.h b/Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.h new file mode 100644 index 000000000..25fc183d3 --- /dev/null +++ b/Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreDataProvider.h" +#import "PFCurrentObjectControlling.h" +#import "PFDataProvider.h" + +extern NSString *const PFCurrentInstallationFileName; +extern NSString *const PFCurrentInstallationPinName; + +@class BFTask; +@class PFInstallation; + +@interface PFCurrentInstallationController : NSObject + +@property (nonatomic, weak, readonly) id commonDataSource; +@property (nonatomic, weak, readonly) id coreDataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithStorageType:(PFCurrentObjectStorageType)dataStorageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource; + ++ (instancetype)controllerWithStorageType:(PFCurrentObjectStorageType)dataStorageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource; + +///-------------------------------------- +/// @name Installation +///-------------------------------------- + +@property (nonatomic, strong, readonly) PFInstallation *memoryCachedCurrentInstallation; + +- (BFTask *)clearCurrentInstallationAsync; +- (BFTask *)clearMemoryCachedCurrentInstallationAsync; + +@end diff --git a/Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.m b/Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.m new file mode 100644 index 000000000..20f957248 --- /dev/null +++ b/Parse/Internal/Installation/CurrentInstallationController/PFCurrentInstallationController.m @@ -0,0 +1,289 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCurrentInstallationController.h" + +#import "BFTask+Private.h" +#import "PFAsyncTaskQueue.h" +#import "PFFileManager.h" +#import "PFInstallationIdentifierStore.h" +#import "PFInstallationPrivate.h" +#import "PFMacros.h" +#import "PFObjectFilePersistenceController.h" +#import "PFObjectPrivate.h" +#import "PFPushPrivate.h" +#import "PFQuery.h" + +NSString *const PFCurrentInstallationFileName = @"currentInstallation"; +NSString *const PFCurrentInstallationPinName = @"_currentInstallation"; + +@interface PFCurrentInstallationController () { + dispatch_queue_t _dataQueue; + PFAsyncTaskQueue *_dataTaskQueue; +} + +@property (nonatomic, strong, readonly) PFFileManager *fileManager; +@property (nonatomic, strong, readonly) PFInstallationIdentifierStore *installationIdentifierStore; + +@property (nonatomic, strong) PFInstallation *currentInstallation; +@property (nonatomic, assign) BOOL currentInstallationMatchesDisk; + +@end + +@implementation PFCurrentInstallationController + +@synthesize storageType = _storageType; + +@synthesize currentInstallation = _currentInstallation; +@synthesize currentInstallationMatchesDisk = _currentInstallationMatchesDisk; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithStorageType:(PFCurrentObjectStorageType)storageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + self = [super init]; + if (!self) return nil; + + _dataQueue = dispatch_queue_create("com.parse.installation.current", DISPATCH_QUEUE_CONCURRENT); + _dataTaskQueue = [[PFAsyncTaskQueue alloc] init]; + + _storageType = storageType; + _commonDataSource = commonDataSource; + _coreDataSource = coreDataSource; + + return self; +} + ++ (instancetype)controllerWithStorageType:(PFCurrentObjectStorageType)storageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + return [[self alloc] initWithStorageType:storageType + commonDataSource:commonDataSource + coreDataSource:coreDataSource]; +} + +///-------------------------------------- +#pragma mark - PFCurrentObjectControlling +///-------------------------------------- + +- (BFTask *)getCurrentObjectAsync { + @weakify(self); + return [_dataTaskQueue enqueue:^BFTask *(BFTask *unused) { + return [[[BFTask taskFromExecutor:[BFExecutor defaultExecutor] withBlock:^id(BFTask *task) { + @strongify(self); + if (self.currentInstallation) { + return self.currentInstallation; + } + + if (!self.currentInstallationMatchesDisk) { + return [[self _loadCurrentInstallationFromDiskAsync] continueWithBlock:^id(BFTask *task) { + PFInstallation *installation = task.result; + if (installation) { + // If there is no objectId, but there is some data + // it means that the data wasn't yet saved to the server + // so we should mark everything as dirty + if (!installation.objectId && [[installation allKeys] count]) { + [installation _markAllFieldsDirty]; + } + } + return task; + }]; + } + return nil; + }] continueWithBlock:^id(BFTask *task) { + @strongify(self); + if (task.faulted) { + return task; + } + + PFInstallation *installation = task.result; + NSString *installationId = self.installationIdentifierStore.installationIdentifier; + installationId = [installationId lowercaseString]; + if (!installation || ![installationId isEqualToString:installation.installationId]) { + // If there's no installation object, or the object's installation + // ID doesn't match this device's installation ID, create a new + // installation. Try to keep track of the previously stored device + // token: if there was an installation already stored just re-use + // its device token, otherwise try loading from the keychain (where + // old SDKs stored the token). Discard the old installation. + NSString *oldDeviceToken = nil; + if (installation) { + oldDeviceToken = installation.deviceToken; + } else { + oldDeviceToken = [[PFPush pushInternalUtilClass] getDeviceTokenFromKeychain]; + } + + installation = [PFInstallation object]; + installation.deviceType = kPFDeviceType; + installation.installationId = installationId; + if (oldDeviceToken) { + installation.deviceToken = oldDeviceToken; + } + } + + return installation; + }] continueWithBlock:^id(BFTask *task) { + dispatch_barrier_sync(_dataQueue, ^{ + _currentInstallation = task.result; + _currentInstallationMatchesDisk = !task.faulted; + }); + return task; + }]; + }]; +} + +- (BFTask *)saveCurrentObjectAsync:(PFInstallation *)installation { + @weakify(self); + return [_dataTaskQueue enqueue:^BFTask *(BFTask *unused) { + @strongify(self); + + if (installation != self.currentInstallation) { + return nil; + } + return [[self _saveCurrentInstallationToDiskAsync:installation] continueWithBlock:^id(BFTask *task) { + self.currentInstallationMatchesDisk = (!task.faulted && !task.cancelled); + return nil; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Installation +///-------------------------------------- + +- (PFInstallation *)memoryCachedCurrentInstallation { + return self.currentInstallation; +} + +- (BFTask *)clearCurrentInstallationAsync { + @weakify(self); + return [_dataTaskQueue enqueue:^BFTask *(BFTask *unused) { + @strongify(self); + + dispatch_barrier_sync(_dataQueue, ^{ + _currentInstallation = nil; + _currentInstallationMatchesDisk = NO; + }); + + NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:2]; + if (self.storageType == PFCurrentObjectStorageTypeOfflineStore) { + BFTask *unpinTask = [PFObject unpinAllObjectsInBackgroundWithName:PFCurrentInstallationPinName]; + [tasks addObject:unpinTask]; + } + + NSString *path = [self.fileManager parseDataItemPathForPathComponent:PFCurrentInstallationFileName]; + BFTask *fileTask = [PFFileManager removeItemAtPathAsync:path]; + [tasks addObject:fileTask]; + + return [BFTask taskForCompletionOfAllTasks:tasks]; + }]; +} + +- (BFTask *)clearMemoryCachedCurrentInstallationAsync { + return [_dataTaskQueue enqueue:^BFTask *(BFTask *unused) { + self.currentInstallation = nil; + self.currentInstallationMatchesDisk = NO; + + return nil; + }]; +} + +///-------------------------------------- +#pragma mark - Data Storage +///-------------------------------------- + +- (BFTask *)_loadCurrentInstallationFromDiskAsync { + if (self.storageType == PFCurrentObjectStorageTypeOfflineStore) { + // Try loading from OfflineStore + PFQuery *query = [[[PFQuery queryWithClassName:[PFInstallation parseClassName]] + fromPinWithName:PFCurrentInstallationPinName] ignoreACLs]; + + return [[query findObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *results = task.result; + if ([results count] == 1) { + return [BFTask taskWithResult:[results firstObject]]; + } else if ([results count] != 0) { + return [[PFObject unpinAllObjectsInBackgroundWithName:PFCurrentInstallationPinName] + continueWithSuccessResult:nil]; + } + + // Backward compatibility if we previously have non-LDS currentInstallation. + return [PFObject _migrateObjectInBackgroundFromFile:PFCurrentInstallationFileName + toPin:PFCurrentInstallationPinName]; + }]; + } + + PFObjectFilePersistenceController *controller = self.objectFilePersistenceController; + return [controller loadPersistentObjectAsyncForKey:PFCurrentInstallationFileName]; +} + +- (BFTask *)_saveCurrentInstallationToDiskAsync:(PFInstallation *)installation { + if (self.storageType == PFCurrentObjectStorageTypeOfflineStore) { + BFTask *task = [PFObject unpinAllObjectsInBackgroundWithName:PFCurrentInstallationPinName]; + return [task continueWithBlock:^id(BFTask *task) { + // Make sure to not pin children of PFInstallation automatically, as it can create problems + // if any of the children are of Installation class. + return [installation _pinInBackgroundWithName:PFCurrentInstallationPinName includeChildren:NO]; + }]; + } + + PFObjectFilePersistenceController *controller = self.objectFilePersistenceController; + return [controller persistObjectAsync:installation forKey:PFCurrentInstallationFileName]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (PFFileManager *)fileManager { + return self.commonDataSource.fileManager; +} + +- (PFObjectFilePersistenceController *)objectFilePersistenceController { + return self.coreDataSource.objectFilePersistenceController; +} + +- (PFInstallationIdentifierStore *)installationIdentifierStore { + return self.commonDataSource.installationIdentifierStore; +} + +- (PFInstallation *)currentInstallation { + __block PFInstallation *installation = nil; + dispatch_sync(_dataQueue, ^{ + installation = _currentInstallation; + }); + return installation; +} + +- (void)setCurrentInstallation:(PFInstallation *)currentInstallation { + dispatch_barrier_sync(_dataQueue, ^{ + if (_currentInstallation != currentInstallation) { + _currentInstallation = currentInstallation; + } + }); +} + +- (BOOL)currentInstallationMatchesDisk { + __block BOOL matches = NO; + dispatch_sync(_dataQueue, ^{ + matches = _currentInstallationMatchesDisk; + }); + return matches; +} + +- (void)setCurrentInstallationMatchesDisk:(BOOL)currentInstallationMatchesDisk { + dispatch_barrier_sync(_dataQueue, ^{ + _currentInstallationMatchesDisk = currentInstallationMatchesDisk; + }); +} + +@end diff --git a/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.h b/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.h new file mode 100644 index 000000000..3e7becb8c --- /dev/null +++ b/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.h @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFFileManager; + +@interface PFInstallationIdentifierStore : NSObject + +/*! + Returns a cached installationId or creates a new one, saves it to disk and returns it. + + @returns `NSString` representation of current installationId. + */ +@property (nonatomic, copy, readonly) NSString *installationIdentifier; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFileManager:(PFFileManager *)fileManager NS_DESIGNATED_INITIALIZER; + +///-------------------------------------- +/// @name Clear +///-------------------------------------- + +/*! + Clears installation identifier on disk and in-memory. + */ +- (void)clearInstallationIdentifier; + +@end diff --git a/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.m b/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.m new file mode 100644 index 000000000..af7fac536 --- /dev/null +++ b/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore.m @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInstallationIdentifierStore.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFMacros.h" +#import "PFMultiProcessFileLockController.h" +#import "Parse_Private.h" + +static NSString *const PFInstallationIdentifierFileName = @"installationId"; + +@interface PFInstallationIdentifierStore () { + dispatch_queue_t _synchronizationQueue; + PFFileManager *_fileManager; +} + +@property (nonatomic, copy, readwrite) NSString *installationIdentifier; +@property (nonatomic, copy, readonly) NSString *installationIdentifierFilePath; + +@end + +@implementation PFInstallationIdentifierStore + +@synthesize installationIdentifier = _installationIdentifier; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithFileManager:(PFFileManager *)fileManager { + self = [super init]; + if (!self) return nil; + + _synchronizationQueue = dispatch_queue_create("com.parse.installationIdentifier", DISPATCH_QUEUE_SERIAL); + PFMarkDispatchQueue(_synchronizationQueue); + + _fileManager = fileManager; + + return self; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (NSString *)installationIdentifier { + __block NSString *identifier = nil; + dispatch_sync(_synchronizationQueue, ^{ + if (!_installationIdentifier) { + [self _loadInstallationIdentifier]; + } + + identifier = _installationIdentifier; + }); + return identifier; +} + +- (void)setInstallationIdentifier:(NSString *)installationIdentifier { + PFAssertIsOnDispatchQueue(_synchronizationQueue); + if (_installationIdentifier != installationIdentifier) { + _installationIdentifier = [installationIdentifier copy]; + } +} + +- (void)clearInstallationIdentifier { + dispatch_sync(_synchronizationQueue, ^{ + NSString *filePath = self.installationIdentifierFilePath; + [[PFFileManager removeItemAtPathAsync:filePath] waitForResult:nil withMainThreadWarning:NO]; + + self.installationIdentifier = nil; + }); +} + +///-------------------------------------- +#pragma mark - Disk Operations +///-------------------------------------- + +- (void)_loadInstallationIdentifier { + PFAssertIsOnDispatchQueue(_synchronizationQueue); + + NSString *filePath = self.installationIdentifierFilePath; + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:filePath]; + + NSString *identifier = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; + if (!identifier) { + identifier = [[[NSUUID UUID] UUIDString] lowercaseString]; + [[PFFileManager writeStringAsync:identifier toFile:filePath] waitForResult:nil withMainThreadWarning:NO]; + } + self.installationIdentifier = identifier; + + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:filePath]; +} + +- (void)_clearCachedInstallationIdentifier { + dispatch_sync(_synchronizationQueue, ^{ + self.installationIdentifier = nil; + }); +} + +- (NSString *)installationIdentifierFilePath { + return [_fileManager parseDataItemPathForPathComponent:PFInstallationIdentifierFileName]; +} + +@end diff --git a/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore_Private.h b/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore_Private.h new file mode 100644 index 000000000..f84b36bf1 --- /dev/null +++ b/Parse/Internal/Installation/InstallationIdentifierStore/PFInstallationIdentifierStore_Private.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInstallationIdentifierStore.h" + +@interface PFInstallationIdentifierStore (Private) + +/*! + Clears in-memory cached installation identifier, if any. + */ +- (void)_clearCachedInstallationIdentifier; + +@end diff --git a/Parse/Internal/Installation/PFInstallationPrivate.h b/Parse/Internal/Installation/PFInstallationPrivate.h new file mode 100644 index 000000000..de3e5d599 --- /dev/null +++ b/Parse/Internal/Installation/PFInstallationPrivate.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@interface PFInstallation (Private) + +- (void)_clearDeviceToken; +- (void)_markAllFieldsDirty; + +@end + +@interface PFInstallation () + +// Private read-write declarations of publicly-readonly fields. +@property (nonatomic, copy, readwrite) NSString *deviceType; +@property (nonatomic, copy, readwrite) NSString *installationId; +@property (nonatomic, copy, readwrite) NSString *timeZone; + +@end diff --git a/Parse/Internal/KeyValueCache/PFKeyValueCache.h b/Parse/Internal/KeyValueCache/PFKeyValueCache.h new file mode 100644 index 000000000..e2518de41 --- /dev/null +++ b/Parse/Internal/KeyValueCache/PFKeyValueCache.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFKeyValueCache : NSObject + +@property (nonatomic, copy, readonly) NSString *cacheDirectoryPath; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCacheDirectoryPath:(NSString *)path; + +///-------------------------------------- +/// @name Setting +///-------------------------------------- + +- (void)setObject:(NSString *)object forKey:(NSString *)key; +- (void)setObject:(NSString *)object forKeyedSubscript:(NSString *)key; + +///-------------------------------------- +/// @name Getting +///-------------------------------------- + +- (NSString *)objectForKey:(NSString *)key maxAge:(NSTimeInterval)age; + +///-------------------------------------- +/// @name Removing +///-------------------------------------- + +- (void)removeObjectForKey:(NSString *)key; +- (void)removeAllObjects; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/KeyValueCache/PFKeyValueCache.m b/Parse/Internal/KeyValueCache/PFKeyValueCache.m new file mode 100644 index 000000000..b849f68a4 --- /dev/null +++ b/Parse/Internal/KeyValueCache/PFKeyValueCache.m @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFKeyValueCache_Private.h" + +#import + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFConstants.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFLogging.h" + +static const NSUInteger PFKeyValueCacheDefaultDiskCacheSize = 10 << 20; +static const NSUInteger PFKeyValueCacheDefaultDiskCacheRecords = 1000; +static const NSUInteger PFKeyValueCacheDefaultMemoryCacheRecordSize = 1 << 20; + +@interface PFKeyValueCacheEntry () + +// We need to generate a setter that's atomic to safely clear the value. +@property (nullable, atomic, readwrite, copy) NSString *value; + +@end + +@implementation PFKeyValueCacheEntry + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithValue:(NSString *)value { + return [self initWithValue:value creationTime:[NSDate date]]; +} + +- (instancetype)initWithValue:(NSString *)value creationTime:(NSDate *)creationTime { + self = [super init]; + if (!self) return nil; + + _value = [value copy]; + _creationTime = creationTime; + + return self; +} + ++ (instancetype)cacheEntryWithValue:(NSString *)value { + return [[self alloc] initWithValue:value]; +} + ++ (instancetype)cacheEntryWithValue:(NSString *)value creationTime:(NSDate *)creationTime { + return [[self alloc] initWithValue:value creationTime:creationTime]; +} + +@end + +@implementation PFKeyValueCache { + NSURL *_cacheDirectoryURL; + dispatch_queue_t _diskCacheQueue; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCacheDirectoryPath:(NSString *)path { + return [self initWithCacheDirectoryURL:[NSURL fileURLWithPath:path] + fileManager:[NSFileManager defaultManager] + memoryCache:[[NSCache alloc] init]]; +} + +- (instancetype)initWithCacheDirectoryURL:(NSURL *)url + fileManager:(NSFileManager *)fileManager + memoryCache:(NSCache *)cache { + self = [super init]; + if (!self) return nil; + + _cacheDirectoryURL = url; + _fileManager = fileManager; + _memoryCache = cache; + + _diskCacheQueue = dispatch_queue_create("com.parse.keyvaluecache.disk", DISPATCH_QUEUE_SERIAL); + + _maxDiskCacheBytes = PFKeyValueCacheDefaultDiskCacheSize; + _maxDiskCacheRecords = PFKeyValueCacheDefaultDiskCacheRecords; + _maxMemoryCacheBytesPerRecord = PFKeyValueCacheDefaultMemoryCacheRecordSize; + + return self; +} + +///-------------------------------------- +#pragma mark - Property Accessors +///-------------------------------------- + +- (NSString *)cacheDirectoryPath { + [_fileManager createDirectoryAtURL:_cacheDirectoryURL withIntermediateDirectories:YES attributes:nil error:NULL]; + return _cacheDirectoryURL.path; +} + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + +- (void)setObject:(NSString *)object forKeyedSubscript:(NSString *)key { + [self setObject:object forKey:key]; +} + +- (void)setObject:(NSString *)value forKey:(NSString *)key { + NSUInteger keyBytes = [key maximumLengthOfBytesUsingEncoding:[key fastestEncoding]]; + NSUInteger valueBytes = [value maximumLengthOfBytesUsingEncoding:[value fastestEncoding]]; + + if ((keyBytes + valueBytes) < self.maxMemoryCacheBytesPerRecord) { + [self.memoryCache setObject:[PFKeyValueCacheEntry cacheEntryWithValue:value] forKey:key]; + } else { + [self.memoryCache removeObjectForKey:key]; + } + + dispatch_async(_diskCacheQueue, ^{ + [self _createDiskCacheEntry:value atURL:[self _cacheURLForKey:key]]; + [self _compactDiskCache]; + }); +} + +- (NSString *)objectForKey:(NSString *)key maxAge:(NSTimeInterval)maxAge { + NSURL *cacheURL = [self _cacheURLForKey:key]; + PFKeyValueCacheEntry *cacheEntry = [self.memoryCache objectForKey:key]; + + if (cacheEntry) { + if ([[NSDate date] timeIntervalSinceDate:cacheEntry.creationTime] > maxAge) { + // We know the cache to be too old in both copies. + // Save space, remove this key from disk, and it's value from the memory cache. + [self removeObjectForKey:key]; + return nil; + } + + dispatch_async(_diskCacheQueue, ^{ + [self _updateModificationDateAtURL:cacheURL]; + }); + + return cacheEntry.value; + } + + // Wait for all outstanding disk operations before continuing, as another thread could be in the process of + // Writing a value to disk right now. + __block NSString *value = nil; + dispatch_sync(_diskCacheQueue, ^{ + NSDate *creationDate = [self _creationDateOfCacheEntryAtURL:cacheURL]; + if ([[NSDate date] timeIntervalSinceDate:creationDate] > maxAge) { + [self removeObjectForKey:key]; + return; + } + + // Cache misses here (e.g. creationDate and value are both nil) should still be put into the memory cache. + value = [self _diskCacheEntryForURL:cacheURL]; + [self.memoryCache setObject:[PFKeyValueCacheEntry cacheEntryWithValue:value creationTime:creationDate] + forKey:key]; + }); + + return value; +} + +- (void)removeObjectForKey:(NSString *)key { + [self.memoryCache removeObjectForKey:key]; + + dispatch_async(_diskCacheQueue, ^{ + [self.fileManager removeItemAtURL:[self _cacheURLForKey:key] error:NULL]; + }); +} + +- (void)removeAllObjects { + [self.memoryCache removeAllObjects]; + + dispatch_sync(_diskCacheQueue, ^{ + // Directory will be automatically recreated the next time 'cacheDir' is accessed. + [self.fileManager removeItemAtURL:_cacheDirectoryURL error:NULL]; + }); +} + +- (void)waitForOutstandingOperations { + dispatch_sync(_diskCacheQueue, ^{ + // Wait, do nothing + }); +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (NSURL *)_cacheURLForKey:(NSString *)key { + return [_cacheDirectoryURL URLByAppendingPathComponent:key]; +} + +- (NSString *)_diskCacheEntryForURL:(NSURL *)url { + NSData *bytes = [self.fileManager contentsAtPath:[url path]]; + if (!bytes) { + return nil; + } + + [self _updateModificationDateAtURL:url]; + return [[NSString alloc] initWithData:bytes encoding:NSUTF8StringEncoding]; +} + +- (void)_createDiskCacheEntry:(NSString *)value atURL:(NSURL *)url { + NSData *bytes = [value dataUsingEncoding:NSUTF8StringEncoding]; + NSDate *creationDate = [NSDate date]; + + [_fileManager createDirectoryAtURL:_cacheDirectoryURL withIntermediateDirectories:YES attributes:nil error:NULL]; + [self.fileManager createFileAtPath:[url path] + contents:bytes + attributes:@{ NSFileCreationDate: creationDate, NSFileModificationDate: creationDate }]; +} + +- (void)_updateModificationDateAtURL:(NSURL *)url { + [self.fileManager setAttributes:@{ NSFileModificationDate: [NSDate date] } ofItemAtPath:url.path error:NULL]; +} + +- (NSDate *)_creationDateOfCacheEntryAtURL:(NSURL *)url { + return [self.fileManager attributesOfItemAtPath:url.path error:NULL][NSFileModificationDate]; +} + +- (void)_compactDiskCache { + // Check if we should kick out old cache entries + NSDirectoryEnumerator *enumerator = [self.fileManager enumeratorAtPath:[_cacheDirectoryURL path]]; + NSUInteger numBytes = 0; + NSMutableArray *sortedFiles = [[NSMutableArray alloc] init]; + NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init]; + + NSString *path = nil; + while ((path = [enumerator nextObject]) != nil) { + [enumerator skipDescendants]; + + attributes[path] = [enumerator.fileAttributes copy]; + numBytes += [attributes[path][NSFileSize] unsignedIntegerValue]; + + NSUInteger insertionIndex = [sortedFiles indexOfObject:path + inSortedRange:NSMakeRange(0, sortedFiles.count) + options:NSBinarySearchingInsertionIndex + usingComparator:^NSComparisonResult(id obj1, id obj2) { + NSDate *date1 = attributes[obj1][NSFileModificationDate]; + NSDate *date2 = attributes[obj2][NSURLContentModificationDateKey]; + + return [date1 compare:date2]; + }]; + + [sortedFiles insertObject:path atIndex:insertionIndex]; + } + + while (sortedFiles.count > _maxDiskCacheRecords || numBytes > _maxDiskCacheBytes) { + NSString *toRemove = [sortedFiles firstObject]; + NSNumber *fileSize = attributes[toRemove][NSFileSize]; + + [self.fileManager removeItemAtURL:[self _cacheURLForKey:toRemove] error:NULL]; + numBytes -= [fileSize unsignedIntegerValue]; + + [sortedFiles removeObjectAtIndex:0]; + } +} + +@end diff --git a/Parse/Internal/KeyValueCache/PFKeyValueCache_Private.h b/Parse/Internal/KeyValueCache/PFKeyValueCache_Private.h new file mode 100644 index 000000000..6c45b6d33 --- /dev/null +++ b/Parse/Internal/KeyValueCache/PFKeyValueCache_Private.h @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFKeyValueCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFKeyValueCache () + +///-------------------------------------- +/// @name Properties +///-------------------------------------- + +@property (nullable, nonatomic, strong, readwrite) NSFileManager *fileManager; +@property (nullable, nonatomic, strong, readwrite) NSCache *memoryCache; + +@property (nonatomic, assign) NSUInteger maxDiskCacheBytes; +@property (nonatomic, assign) NSUInteger maxDiskCacheRecords; +@property (nonatomic, assign) NSUInteger maxMemoryCacheBytesPerRecord; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithCacheDirectoryURL:(nullable NSURL *)url + fileManager:(nullable NSFileManager *)fileManager + memoryCache:(nullable NSCache *)cache NS_DESIGNATED_INITIALIZER; + +///-------------------------------------- +/// @name Waiting +///-------------------------------------- + +- (void)waitForOutstandingOperations; + +@end + +@interface PFKeyValueCacheEntry : NSObject + +///-------------------------------------- +/// @name Properties +///-------------------------------------- + +@property (atomic, copy, readonly) NSString *value; +@property (atomic, strong, readonly) NSDate *creationTime; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + ++ (instancetype)cacheEntryWithValue:(NSString *)value; ++ (instancetype)cacheEntryWithValue:(NSString *)value creationTime:(NSDate *)creationTime; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithValue:(NSString *)value; +- (instancetype)initWithValue:(NSString *)value + creationTime:(NSDate *)creationTime NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.h b/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.h new file mode 100644 index 000000000..1a61df209 --- /dev/null +++ b/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.h @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFObject; +@class PFOfflineStore; +@class PFQueryState; +@class PFSQLiteDatabase; +@class PFUser; + +typedef BFTask *(^PFConstraintMatcherBlock)(PFObject *object, PFSQLiteDatabase *database); + +typedef NS_OPTIONS(uint8_t, PFOfflineQueryOption) { + PFOfflineQueryOptionOrder = 1 << 0, + PFOfflineQueryOptionLimit = 1 << 1, + PFOfflineQueryOptionSkip = 1 << 2, +}; + +@interface PFOfflineQueryLogic : NSObject + +/*! + Initialize an `PFOfflineQueryLogic` instance with `PFOfflineStore` instance. + `PFOfflineStore` is needed for subQuery, inQuery and fetch. + */ +- (instancetype)initWithOfflineStore:(PFOfflineStore *)offlineStore; + +/*! + @returns YES iff the object is visible based on its read ACL and the given user objectId. + */ ++ (BOOL)userHasReadAccess:(PFUser *)user ofObject:(PFObject *)object; + +/*! + @returns YES iff the object is visible based on its read ACL and the given user objectId. + */ ++ (BOOL)userHasWriteAccess:(PFUser *)user ofObject:(PFObject *)object; + +/*! + Returns a PFConstraintMatcherBlock that returns true iff the object matches the given + query's constraints. This takes in a PFSQLiteDatabase connection because SQLite is finicky + about nesting connections, so we want to reuse them whenever possible. + */ +- (PFConstraintMatcherBlock)createMatcherForQueryState:(PFQueryState *)queryState user:(PFUser *)user; + +/*! + Sort given array with given `PFQuery` constraint. + + @returns sorted result. + */ +- (NSArray *)resultsByApplyingOptions:(PFOfflineQueryOption)options + ofQueryState:(PFQueryState *)queryState + toResults:(NSArray *)results; + +/*! + Make sure all of the objects included by the given query get fetched. + */ +- (BFTask *)fetchIncludesAsyncForResults:(NSArray *)results + ofQueryState:(PFQueryState *)queryState + inDatabase:(PFSQLiteDatabase *)database; + +/*! + Make sure all of the objects included by the given query get fetched. + */ +- (BFTask *)fetchIncludesForObjectAsync:(PFObject *)object + queryState:(PFQueryState *)queryState + database:(PFSQLiteDatabase *)database; + +@end diff --git a/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m b/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m new file mode 100644 index 000000000..5f6379360 --- /dev/null +++ b/Parse/Internal/LocalDataStore/OfflineQueryLogic/PFOfflineQueryLogic.m @@ -0,0 +1,928 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFOfflineQueryLogic.h" + +#import + +#import "PFACL.h" +#import "PFAssert.h" +#import "PFConstants.h" +#import "PFDateFormatter.h" +#import "PFDecoder.h" +#import "PFEncoder.h" +#import "PFErrorUtilities.h" +#import "PFGeoPoint.h" +#import "PFOfflineStore.h" +#import "PFQueryPrivate.h" +#import "PFRelation.h" +#import "PFRelationPrivate.h" + +typedef BOOL (^PFComparatorDeciderBlock)(id value, id constraint); +typedef BOOL (^PFSubQueryMatcherBlock)(id object, NSArray *results); + +/*! + A query to be used in $inQuery, $notInQuery, $select and $dontSelect + */ +@interface PFSubQueryMatcher : NSObject + +@property (nonatomic, strong, readonly) PFQuery *subQuery; +@property (nonatomic, strong) BFTask *subQueryResults; +@property (nonatomic, strong, readonly) PFOfflineStore *offlineStore; + +@end + +@implementation PFSubQueryMatcher + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithSubQuery:(PFQuery *)query offlineStore:(PFOfflineStore *)offlineStore { + if ((self = [super init]) != nil) { + _subQuery = query; + _offlineStore = offlineStore; + } + + return self; +} + +///-------------------------------------- +#pragma mark - SubQuery Matcher Creator +///-------------------------------------- + +- (PFConstraintMatcherBlock)createMatcherWithSubQueryMatcherBlock:(PFSubQueryMatcherBlock)block user:(PFUser *)user { + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + if (self.subQueryResults == nil) { + self.subQueryResults = [self.offlineStore findAsyncForQueryState:self.subQuery.state + user:user + pin:nil + isCount:NO + database:database]; + } + return [self.subQueryResults continueWithSuccessBlock:^id(BFTask *task) { + return @(block(object, task.result)); + }]; + }; +} + +@end + +@interface PFOfflineQueryLogic () + +@property (nonatomic, weak) PFOfflineStore *offlineStore; + +@end + +@implementation PFOfflineQueryLogic + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithOfflineStore:(PFOfflineStore *)offlineStore { + if ((self = [super init]) != nil) { + _offlineStore = offlineStore; + } + return self; +} + +///-------------------------------------- +#pragma mark - Value Getter +///-------------------------------------- + +- (id)valueForContainer:(id)container + key:(NSString *)key { + return [self valueForContainer:container key:key depth:0]; +} + +- (id)valueForContainer:(id)container + key:(NSString *)key + depth:(int)depth { + if ([key rangeOfString:@"."].location != NSNotFound) { + NSArray *parts = [key componentsSeparatedByString:@"."]; + + NSString *firstKey = [parts firstObject]; + NSString *rest = nil; + if ([parts count] > 1) { + NSRange range = NSMakeRange(1, [parts count] - 1); + rest = [[parts subarrayWithRange:range] componentsJoinedByString:@"."]; + } + id value = [self valueForContainer:container key:firstKey depth:depth + 1]; + // Only NSDictionary can be dotted into for getting values, so we should reject + // anything like ParseObjects and arrays. + if (!(value == nil || [value isKindOfClass:[NSDictionary class]])) { + if (depth > 0) { + id restFormat = [[PFPointerObjectEncoder objectEncoder] encodeObject:value]; + if ([restFormat isKindOfClass:[NSDictionary class]]) { + return [self valueForContainer:restFormat key:rest depth:depth + 1]; + } + } + [NSException raise:NSInvalidArgumentException format:@"Key %@ is invalid", key]; + } + return [self valueForContainer:value key:rest depth:depth + 1]; + } + + if ([container isKindOfClass:[PFObject class]]) { + PFObject *object = (PFObject *)container; + + // The object needs to have been fetched already if we are going to sort by one of its field. + if (!object.isDataAvailable) { + [NSException raise:NSInvalidArgumentException format:@"Bad key %@", key]; + } + + // Handle special keys for PFObject. + if ([key isEqualToString:@"objectId"]) { + return object.objectId; + } else if ([key isEqualToString:@"createdAt"] || [key isEqualToString:@"_created_at"]) { + return object.createdAt; + } else if ([key isEqualToString:@"updatedAt"] || [key isEqualToString:@"_updated_at"]) { + return object.updatedAt; + } else { + return object[key]; + } + } else if ([container isKindOfClass:[NSDictionary class]]) { + return ((NSDictionary *)container)[key]; + } else if (container == nil) { + return nil; + } else { + [NSException raise:NSInvalidArgumentException format:@"Bad key %@", key]; + // Shouldn't reach here. + return nil; + } +} + +///-------------------------------------- +#pragma mark - Matcher With Decider +///-------------------------------------- + +/*! + Returns YES if decider returns YES for any value in the given array. + */ ++ (BOOL)matchesArray:(NSArray *)array + constraint:(id)constraint + withDecider:(PFComparatorDeciderBlock)decider { + for (id value in array) { + if (decider(value, constraint)) { + return YES; + } + } + return NO; +} + +/*! + Returns YES if decider returns YES for any value in the given array. + */ ++ (BOOL)matchesValue:(id)value + constraint:(id)constraint + withDecider:(PFComparatorDeciderBlock)decider { + if ([value isKindOfClass:[NSArray class]]) { + return [self matchesArray:value constraint:constraint withDecider:decider]; + } else { + return decider(value, constraint); + } +} + +///-------------------------------------- +#pragma mark - Matcher +///-------------------------------------- + +/*! + Implements simple equality constraints. This emulates Mongo's behavior where "equals" can mean array containment. + */ ++ (BOOL)matchesValue:(id)value + equalTo:(id)constraint { + return [self matchesValue:value constraint:constraint withDecider:^BOOL (id value, id constraint) { + // Do custom matching for dates to make sure we have proper precision. + if ([value isKindOfClass:[NSDate class]] && + [constraint isKindOfClass:[NSDate class]]) { + PFDateFormatter *dateFormatter = [PFDateFormatter sharedFormatter]; + NSString *valueString = [dateFormatter preciseStringFromDate:value]; + NSString *constraintString = [dateFormatter preciseStringFromDate:constraint]; + return [valueString isEqual:constraintString]; + } + + if ([value isKindOfClass:[PFRelation class]]) { + return [value isEqual:constraint] || [value _hasKnownObject:constraint]; + } + + return [value isEqual:constraint]; + }]; +} + +/*! + Matches $ne constraints. + */ ++ (BOOL)matchesValue:(id)value + notEqualTo:(id)constraint { + return ![self matchesValue:value equalTo:constraint]; +} + +/*! + Matches $lt constraints. + */ ++ (BOOL)matchesValue:(id)value + lessThan:(id)constraint { + return [self matchesValue:value constraint:constraint withDecider:^BOOL (id value, id constraint) { + if (value == nil || value == [NSNull null]) { + return NO; + } + NSComparisonResult comparisonResult = [value compare:constraint]; + return comparisonResult == NSOrderedAscending; + }]; +} + +/*! + Matches $lte constraints. + */ ++ (BOOL)matchesValue:(id)value + lessThanOrEqualTo:(id)constraint { + return [self matchesValue:value constraint:constraint withDecider:^BOOL (id value, id constraint) { + if (value == nil || value == [NSNull null]) { + return NO; + } + NSComparisonResult comparisonResult = [value compare:constraint]; + return (comparisonResult == NSOrderedAscending) || (comparisonResult == NSOrderedSame); + }]; +} + +/*! + Matches $gt constraints. + */ ++ (BOOL)matchesValue:(id)value + greaterThan:(id)constraint { + return [self matchesValue:value constraint:constraint withDecider:^BOOL (id value, id constraint) { + if (value == nil || value == [NSNull null]) { + return NO; + } + NSComparisonResult comparisonResult = [value compare:constraint]; + return comparisonResult == NSOrderedDescending; + }]; +} + +/*! + Matches $gte constraints. + */ ++ (BOOL)matchesValue:(id)value +greaterThanOrEqualTo:(id)constraint { + return [self matchesValue:value constraint:constraint withDecider:^BOOL (id value, id constraint) { + if (value == nil || value == [NSNull null]) { + return NO; + } + NSComparisonResult comparisonResult = [value compare:constraint]; + return (comparisonResult == NSOrderedDescending) || (comparisonResult == NSOrderedSame); + }]; +} + +/*! + Matches $in constraints. + $in returns YES if the intersection of value and constraint is not an empty set. + */ ++ (BOOL)matchesValue:(id)value + containedIn:(id)constraint { + if (constraint == nil || constraint == [NSNull null]) { + return NO; + } + + PFParameterAssert([constraint isKindOfClass:[NSArray class]], @"Constraint type not supported for $in queries"); + + for (id requiredItem in (NSArray *)constraint) { + if ([self matchesValue:value equalTo:requiredItem]) { + return YES; + } + } + return NO; +} + +/*! + Matches $nin constraints. + */ ++ (BOOL)matchesValue:(id)value + notContainedIn:(id)constraint { + return ![self matchesValue:value containedIn:constraint]; +} + +/*! + Matches $all constraints. + */ ++ (BOOL)matchesValue:(id)value containsAllObjectsInArray:(id)constraints { + PFParameterAssert([constraints isKindOfClass:[NSArray class]], @"Constraint type not supported for $all queries"); + PFParameterAssert([value isKindOfClass:[NSArray class]], @"Value type not supported for $all queries"); + + for (id requiredItem in (NSArray *)constraints) { + if (![self matchesValue:value equalTo:requiredItem]) { + return NO; + } + } + return YES; +} + +/*! + Matches $regex constraints. + */ ++ (BOOL)matchesValue:(id)value + regex:(id)constraint + withOptions:(NSString *)options { + if (value == nil || value == [NSNull null]) { + return NO; + } + + if (options == nil) { + options = @""; + } + if ([options rangeOfString:@"^[imxs]*$" options:NSRegularExpressionSearch].location == NSNotFound) { + [NSException raise:NSInvalidArgumentException format:@"Invalid regex options: %@", options]; + } + + NSRegularExpressionOptions flags = 0; + if ([options rangeOfString:@"i"].location != NSNotFound) { + flags = flags | NSRegularExpressionCaseInsensitive; + } + if ([options rangeOfString:@"m"].location != NSNotFound) { + flags = flags | NSRegularExpressionAnchorsMatchLines; + } + if ([options rangeOfString:@"x"].location != NSNotFound) { + flags = flags | NSRegularExpressionAllowCommentsAndWhitespace; + } + if ([options rangeOfString:@"s"].location != NSNotFound) { + flags = flags | NSRegularExpressionDotMatchesLineSeparators; + } + + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:constraint + options:flags + error:&error]; + NSArray *matches = [regex matchesInString:value options:0 range:NSMakeRange(0, [value length])]; + return matches.count > 0; +} + +/*! + Matches $exists constraints. + */ ++ (BOOL)matchesValue:(id)value + exists:(id)constraint { + if (constraint != nil && [constraint boolValue]) { + return value != nil && value != [NSNull null]; + } + + return value == nil || value == [NSNull null]; +} + +/*! + Matches $nearSphere constraints. + */ ++ (BOOL)matchesValue:(id)value + nearSphere:(id)constraint + maxDistance:(NSNumber *)maxDistance { + if (value == nil || value == [NSNull null]) { + return NO; + } + if (maxDistance == nil) { + return YES; + } + PFGeoPoint *point1 = constraint; + PFGeoPoint *point2 = value; + return [point1 distanceInRadiansTo:point2] <= [maxDistance doubleValue]; +} + +/*! + Matches $within constraints. + */ ++ (BOOL)matchesValue:(id)value + within:(id)constraint { + NSDictionary *constraintDictionary = (NSDictionary *)constraint; + NSArray *box = constraintDictionary[PFQueryOptionKeyBox]; + PFGeoPoint *southWest = box[0]; + PFGeoPoint *northEast = box[1]; + PFGeoPoint *target = (PFGeoPoint *)value; + + if (northEast.longitude < southWest.longitude) { + [NSException raise:NSInvalidArgumentException + format:@"whereWithinGeoBox queries cannot cross the International Date Line."]; + } + if (northEast.latitude < southWest.latitude) { + [NSException raise:NSInvalidArgumentException + format:@"The southwest corner of a geo box must be south of the northeast corner."]; + } + if (northEast.longitude - southWest.longitude > 180) { + [NSException raise:NSInvalidArgumentException + format:@"Geo box queries larger than 180 degrees in longitude are not supported." + @"Please check point order."]; + } + + return (target.latitude >= southWest.latitude && + target.latitude <= northEast.latitude && + target.longitude >= southWest.longitude && + target.longitude <= northEast.longitude); +} + +/*! + Returns YES iff the given value matches the given operator and constraint. + Raise NSInvalidArgumentException if the operator is not one this function can handle + */ ++ (BOOL)matchesValue:(id)value + constraint:(id)constraint + operator:(NSString *)operator + allKeyConstraints:(NSDictionary *)allKeyConstraints { + if ([operator isEqualToString:PFQueryKeyNotEqualTo]) { + return [self matchesValue:value notEqualTo:constraint]; + } else if ([operator isEqualToString:PFQueryKeyLessThan]) { + return [self matchesValue:value lessThan:constraint]; + } else if ([operator isEqualToString:PFQueryKeyLessThanEqualTo]) { + return [self matchesValue:value lessThanOrEqualTo:constraint]; + } else if ([operator isEqualToString:PFQueryKeyGreaterThan]) { + return [self matchesValue:value greaterThan:constraint]; + } else if ([operator isEqualToString:PFQueryKeyGreaterThanOrEqualTo]) { + return [self matchesValue:value greaterThanOrEqualTo:constraint]; + } else if ([operator isEqualToString:PFQueryKeyContainedIn]) { + return [self matchesValue:value containedIn:constraint]; + } else if ([operator isEqualToString:PFQueryKeyNotContainedIn]) { + return [self matchesValue:value notContainedIn:constraint]; + } else if ([operator isEqualToString:PFQueryKeyContainsAll]) { + return [self matchesValue:value containsAllObjectsInArray:constraint]; + } else if ([operator isEqualToString:PFQueryKeyRegex]) { + return [self matchesValue:value regex:constraint withOptions:allKeyConstraints[PFQueryOptionKeyRegexOptions]]; + } else if ([operator isEqualToString:PFQueryOptionKeyRegexOptions]) { + // No need to do anything. This is handled by $regex. + return YES; + } else if ([operator isEqualToString:PFQueryKeyExists]) { + return [self matchesValue:value exists:constraint]; + } else if ([operator isEqualToString:PFQueryKeyNearSphere]) { + return [self matchesValue:value + nearSphere:constraint + maxDistance:allKeyConstraints[PFQueryOptionKeyMaxDistance]]; + } else if ([operator isEqualToString:PFQueryOptionKeyMaxDistance]) { + // No need to do anything. This is handled by $nearSphere. + return YES; + } else if ([operator isEqualToString:PFQueryKeyWithin]) { + return [self matchesValue:value within:constraint]; + } + + [NSException raise:NSInvalidArgumentException + format:@"The offline store does not yet support %@ operator.", operator]; + // Shouldn't reach here + return YES; +} + +/*! + Creates a matcher that handles $inQuery constraints. + */ +- (PFConstraintMatcherBlock)createMatcherForKey:(NSString *)key + inQuery:(id)constraints + user:(PFUser *)user { + PFQuery *query = (PFQuery *)constraints; + PFSubQueryMatcher *subQueryMatcher = [[PFSubQueryMatcher alloc] initWithSubQuery:query + offlineStore:self.offlineStore]; + return [subQueryMatcher createMatcherWithSubQueryMatcherBlock:^BOOL(id object, NSArray *results) { + id value = [self valueForContainer:object key:key]; + return [[self class] matchesValue:value containedIn:results]; + } user:user]; +} + +/*! + Creates a matcher that handles $notInQuery constraints. + */ +- (PFConstraintMatcherBlock)createMatcherForKey:(NSString *)key + notInQuery:(id)constraints + user:(PFUser *)user { + PFConstraintMatcherBlock inQueryMatcher = [self createMatcherForKey:key inQuery:constraints user:user]; + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + return [inQueryMatcher(object, database) continueWithSuccessBlock:^id(BFTask *task) { + return @(![task.result boolValue]); + }]; + }; +} + +/*! + Creates a matcher that handles $select constraints. + */ +- (PFConstraintMatcherBlock)createMatcherForKey:(NSString *)key + select:(id)constraints + user:(PFUser *)user { + NSDictionary *constraintDictionary = (NSDictionary *)constraints; + PFQuery *query = (PFQuery *)constraintDictionary[PFQueryKeyQuery]; + NSString *resultKey = (NSString *)constraintDictionary[PFQueryKeyKey]; + PFSubQueryMatcher *subQueryMatcher = [[PFSubQueryMatcher alloc] initWithSubQuery:query + offlineStore:self.offlineStore]; + return [subQueryMatcher createMatcherWithSubQueryMatcherBlock:^BOOL(id object, NSArray *results) { + id value = [self valueForContainer:object key:key]; + for (id result in results) { + id resultValue = [self valueForContainer:result key:resultKey]; + if ([[self class] matchesValue:resultValue equalTo:value]) { + return YES; + } + } + return NO; + } user:user]; +} + +/*! + Creates a matcher that handles $dontSelect constraints. + */ +- (PFConstraintMatcherBlock)createMatcherForKey:(NSString *)key + dontSelect:(id)constraints + user:(PFUser *)user { + PFConstraintMatcherBlock selectMatcher = [self createMatcherForKey:key select:constraints user:user]; + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + return [selectMatcher(object, database) continueWithSuccessBlock:^id(BFTask *task) { + return @(![task.result boolValue]); + }]; + }; +} + +/*! + Creates a matcher for a particular constraint operator. + */ +- (PFConstraintMatcherBlock)createMatcherWithOperator:(NSString *)operator + constraints:(id)constraint + key:(NSString *)key + allKeyConstraints:(NSDictionary *)allKeyConstraints + user:(PFUser *)user { + if ([operator isEqualToString:PFQueryKeyInQuery]) { + return [self createMatcherForKey:key inQuery:constraint user:user]; + } else if ([operator isEqualToString:PFQueryKeyNotInQuery]) { + return [self createMatcherForKey:key notInQuery:constraint user:user]; + } else if ([operator isEqualToString:PFQueryKeySelect]) { + return [self createMatcherForKey:key select:constraint user:user]; + } else if ([operator isEqualToString:PFQueryKeyDontSelect]) { + return [self createMatcherForKey:key dontSelect:constraint user:user]; + } else { + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + id value = [self valueForContainer:object key:key]; + BOOL matchesValue = [[self class] matchesValue:value + constraint:constraint + operator:operator + allKeyConstraints:allKeyConstraints]; + return [BFTask taskWithResult:@(matchesValue)]; + }; + } +} + +/*! + Handles $or queries. + */ +- (PFConstraintMatcherBlock)createOrMatcherForQueries:(NSArray *)queries user:(PFUser *)user { + NSMutableArray *matchers = [NSMutableArray array]; + for (PFQuery *query in queries) { + PFConstraintMatcherBlock matcher = [self createMatcherWithQueryConstraints:query.state.conditions user:user]; + [matchers addObject:matcher]; + } + + // Now OR together the constraints for each query. + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + BFTask *task = [BFTask taskWithResult:@NO]; + for (PFConstraintMatcherBlock matcher in matchers) { + task = [task continueWithSuccessBlock:^id(BFTask *task) { + if ([task.result boolValue]) { + return task; + } + return matcher(object, database); + }]; + } + return task; + }; +} + +/*! + Returns a PFConstraintMatcherBlock that return true iff the object matches queryConstraints. This + takes in a SQLiteDatabase connection because SQLite is finicky about nesting connections, so we + want to reuse them whenever possible. + */ +- (PFConstraintMatcherBlock)createMatcherWithQueryConstraints:(NSDictionary *)queryConstraints user:(PFUser *)user { + NSMutableArray *matchers = [[NSMutableArray alloc] init]; + [queryConstraints enumerateKeysAndObjectsUsingBlock:^(id key, id queryConstraintValue, BOOL *stop) { + if ([key isEqualToString:PFQueryKeyOr]) { + // A set of queries to be OR-ed together + PFConstraintMatcherBlock matcher = [self createOrMatcherForQueries:queryConstraintValue user:user]; + [matchers addObject:matcher]; + } else if ([key isEqualToString:PFQueryKeyRelatedTo]) { + PFConstraintMatcherBlock matcher = ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + PFObject *parent = queryConstraintValue[PFQueryKeyObject]; + NSString *relationKey = queryConstraintValue[PFQueryKeyKey]; + PFRelation *relation = parent[relationKey]; + + return [BFTask taskWithResult:@([relation _hasKnownObject:object])]; + }; + [matchers addObject:matcher]; + } else if ([queryConstraintValue isKindOfClass:[NSDictionary class]]) { + // If it's a set of constraints that should be AND-ed together + NSDictionary *keyConstraints = (NSDictionary *)queryConstraintValue; + [keyConstraints enumerateKeysAndObjectsUsingBlock:^(id operator, id keyConstraintValue, BOOL *stop) { + PFConstraintMatcherBlock matcher = [self createMatcherWithOperator:operator + constraints:keyConstraintValue + key:key + allKeyConstraints:keyConstraints + user:user]; + [matchers addObject:matcher]; + }]; + } else { + // It's not a set of constraints, so it's just a value to compare against. + PFConstraintMatcherBlock matcher = ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + id objectValue = [self valueForContainer:object key:key]; + BOOL matches = [[self class] matchesValue:objectValue equalTo:queryConstraintValue]; + return [BFTask taskWithResult:@(matches)]; + }; + [matchers addObject:matcher]; + } + }]; + + // Now AND together the constraints for each key + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + BFTask *task = [BFTask taskWithResult:@YES]; + for (PFConstraintMatcherBlock matcher in matchers) { + task = [task continueWithSuccessBlock:^id(BFTask *task) { + if (![task.result boolValue]) { + return task; + } + @try { + return matcher(object, database); + } @catch (NSException *exception) { + // Promote to error to keep the same behavior as online. + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorInvalidQuery + message:exception.reason + shouldLog:NO]; + return [BFTask taskWithError:error]; + } + }]; + } + return task; + }; +} + +///-------------------------------------- +#pragma mark - Fetch +///-------------------------------------- + +- (BFTask *)fetchIncludeAsync:(NSString *)include + container:(id)container + database:(PFSQLiteDatabase *)database { + if (container == nil) { + return [BFTask taskWithResult:nil]; + } + + if ([container isKindOfClass:[NSArray class]]) { + NSArray *array = (NSArray *)container; + // We do the fetches in series because it makes it easier to fail on the first error. + BFTask *task = [BFTask taskWithResult:nil]; + for (id item in array) { + task = [task continueWithSuccessBlock:^id(BFTask *task) { + return [self fetchIncludeAsync:include container:item database:database]; + }]; + } + return task; + } + + // If we've reached the end of include, then actually do the fetch. + if (include == nil) { + if ([container isKindOfClass:[PFObject class]]) { + PFObject *object = (PFObject *)container; + return [self.offlineStore fetchObjectLocallyAsync:object database:database]; + } else if (container == [NSNull null]) { + // Accept NSNull value in included field. We swallow it silently instead of + // throwing an exception. + return nil; + } + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorInvalidNestedKey + message:@"include is invalid for non-ParseObjects"]; + return [BFTask taskWithError:error]; + } + + // Descend into the container and try again + NSArray *parts = [include componentsSeparatedByString:@"."]; + + NSString *key = [parts firstObject]; + NSString *rest = nil; + if ([parts count] > 1) { + NSRange range = NSMakeRange(1, [parts count] - 1); + rest = [[parts subarrayWithRange:range] componentsJoinedByString:@"."]; + } + + return [[[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) { + if ([container isKindOfClass:[PFObject class]]) { + BFTask *fetchTask = [self fetchIncludeAsync:nil container:container database:database]; + return [fetchTask continueWithSuccessBlock:^id(BFTask *task) { + return ((PFObject *)container)[key]; + }]; + } else if ([container isKindOfClass:[NSDictionary class]]) { + return ((NSDictionary *)container)[key]; + } else if (container == [NSNull null]) { + // Accept NSNull value in included field. We swallow it silently instead of + // throwing an exception. + return nil; + } + NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"include is invalid" + userInfo:nil]; + return [BFTask taskWithException:exception]; + }] continueWithSuccessBlock:^id(BFTask *task) { + return [self fetchIncludeAsync:rest container:task.result database:database]; + }]; +} + +///-------------------------------------- +#pragma mark - User Access +///-------------------------------------- + ++ (BOOL)userHasReadAccess:(PFUser *)user ofObject:(PFObject *)object { + if (user == object) { + return YES; + } + + PFACL *acl = [object ACL]; + if (acl == nil) { + return YES; + } + if ([acl getPublicReadAccess]) { + return YES; + } + if (user != nil && [acl getReadAccessForUser:user]) { + return YES; + } + + // TODO (hallucinogen): Implement roles + return NO; +} + ++ (BOOL)userHasWriteAccess:(PFUser *)user ofObject:(PFObject *)object { + if (user == object) { + return YES; + } + + PFACL *acl = [object ACL]; + if (acl == nil) { + return YES; + } + if ([acl getPublicWriteAccess]) { + return YES; + } + if (user != nil && [acl getWriteAccessForUser:user]) { + return YES; + } + + // TODO (hallucinogen): Implement roles + return NO; +} + +///-------------------------------------- +#pragma mark - Internal Public Methods +///-------------------------------------- + +- (PFConstraintMatcherBlock)createMatcherForQueryState:(PFQueryState *)queryState user:(PFUser *)user { + PFConstraintMatcherBlock constraintMatcher = [self createMatcherWithQueryConstraints:queryState.conditions + user:user]; + // Capture ignoreACLs before the block since it might be modified between matchings. + BOOL shouldIgnoreACLs = queryState.shouldIgnoreACLs; + + return ^BFTask *(PFObject *object, PFSQLiteDatabase *database) { + // TODO (hallucinogen): revisit this whether we should check query and object parseClassname equality + if (!shouldIgnoreACLs && ![[self class] userHasReadAccess:user ofObject:object]) { + return [BFTask taskWithResult:@NO]; + } + return constraintMatcher(object, database); + }; +} + +///-------------------------------------- +#pragma mark - Query Options +///-------------------------------------- + +- (NSArray *)resultsByApplyingOptions:(PFOfflineQueryOption)options + ofQueryState:(PFQueryState *)queryState + toResults:(NSArray *)results { + // No results or empty options. + if (results.count == 0 || options == 0) { + return results; + } + + NSMutableArray *mutableResults = [results mutableCopy]; + if (options & PFOfflineQueryOptionOrder) { + [self _sortResults:mutableResults ofQueryState:queryState]; + } + if (options & PFOfflineQueryOptionSkip) { + NSInteger skip = queryState.skip; + if (skip > 0) { + skip = MIN(skip, results.count); + [mutableResults removeObjectsInRange:NSMakeRange(0, skip)]; + } + } + if (options & PFOfflineQueryOptionLimit) { + NSInteger limit = queryState.limit; + if (limit >= 0 && mutableResults.count > limit) { + [mutableResults removeObjectsInRange:NSMakeRange(limit, mutableResults.count - limit)]; + } + } + + return [mutableResults copy]; +} + +- (void)_sortResults:(NSMutableArray *)results ofQueryState:(PFQueryState *)queryState { + NSArray *keys = queryState.sortKeys; + [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + NSString *key = (NSString *)obj; + if ([key rangeOfString:@"^-?[A-Za-z][A-Za-z0-9_]*$" options:NSRegularExpressionSearch].location == NSNotFound) { + if (![@"_created_at" isEqualToString:key] && ![@"_updated_at" isEqualToString:key]) { + [NSException raise:NSInternalInconsistencyException + format:@"Invalid key name: %@", key]; + } + } + }]; + + __block NSString *nearSphereKey = nil; + __block PFGeoPoint *nearSphereValue = nil; + [queryState.conditions enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([obj isKindOfClass:[NSDictionary class]]) { + NSDictionary *keyConstraints = (NSDictionary *)obj; + if (keyConstraints[PFQueryKeyNearSphere]) { + nearSphereKey = [key copy]; + nearSphereValue = keyConstraints[PFQueryKeyNearSphere]; + } + } + }]; + + // If there's nothing to sort based on, then don't do anything. + if (keys.count == 0 && nearSphereKey == nil) { + return; + } + + [results sortUsingComparator:^NSComparisonResult(id lhs, id rhs) { + if (nearSphereKey != nil) { + PFGeoPoint *lhsPoint = [self valueForContainer:lhs key:nearSphereKey]; + PFGeoPoint *rhsPoint = [self valueForContainer:rhs key:nearSphereKey]; + + double lhsDistance = [lhsPoint distanceInRadiansTo:nearSphereValue]; + double rhsDistance = [rhsPoint distanceInRadiansTo:nearSphereValue]; + if (lhsDistance != rhsDistance) { + return (lhsDistance - rhsDistance < 0) ? NSOrderedAscending : NSOrderedDescending; + } + } + + for (int i = 0; i < keys.count; ++i) { + NSString *key = keys[i]; + BOOL descending = NO; + if ([key hasPrefix:@"-"]) { + descending = YES; + key = [key substringFromIndex:1]; + } + + id lhsValue = [self valueForContainer:lhs key:key]; + id rhsValue = [self valueForContainer:rhs key:key]; + + NSComparisonResult result = NSOrderedSame; + if (lhsValue != nil && rhsValue == nil) { + result = NSOrderedAscending; + } else if (lhsValue == nil && rhsValue != nil) { + result = NSOrderedDescending; + } else if (lhsValue == nil && rhsValue == nil) { + result = NSOrderedSame; + } else { + result = [lhsValue compare:rhsValue]; + } + + if (result != 0) { + return descending ? -result : result; + } + + } + + return NSOrderedSame; + }]; +} + +- (BFTask *)fetchIncludesAsyncForResults:(NSArray *)results + ofQueryState:(PFQueryState *)queryState + inDatabase:(PFSQLiteDatabase *)database { + BFTask *fetchTask = [BFTask taskWithResult:nil]; + for (PFObject *object in results) { + @weakify(self); + fetchTask = [fetchTask continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + return [self fetchIncludesForObjectAsync:object + queryState:queryState + database:database]; + }]; + } + return fetchTask; +} + +- (BFTask *)fetchIncludesForObjectAsync:(PFObject *)object + queryState:(PFQueryState *)queryState + database:(PFSQLiteDatabase *)database { + NSSet *includes = queryState.includedKeys; + // We do the fetches in series because it makes it easier to fail on first error. + BFTask *task = [BFTask taskWithResult:nil]; + for (NSString *include in includes) { + // We do the fetches in series because it makes it easier to fail on the first error. + task = [task continueWithSuccessBlock:^id(BFTask *task) { + return [self fetchIncludeAsync:include container:object database:database]; + }]; + } + return task; +} + +@end diff --git a/Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.h b/Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.h new file mode 100644 index 000000000..c51e9e4a7 --- /dev/null +++ b/Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.h @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFFileManager; +@class PFObject; +@class PFPin; +@class PFQueryState; +@class PFSQLiteDatabase; +@class PFUser; + +typedef NS_OPTIONS(uint8_t, PFOfflineStoreOptions) +{ + PFOfflineStoreOptionAlwaysFetchFromSQLite = 1 << 0, +}; + +//TODO: (nlutsenko) Bring this header up to standard with @name, method comments, etc... +@interface PFOfflineStore : NSObject + +@property (nonatomic, assign, readonly) PFOfflineStoreOptions options; +@property (nonatomic, strong, readonly) PFFileManager *fileManager; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFileManager:(PFFileManager *)fileManager + options:(PFOfflineStoreOptions)options NS_DESIGNATED_INITIALIZER; + +///-------------------------------------- +/// @name Fetch +///-------------------------------------- + +- (BFTask *)fetchObjectLocallyAsync:(PFObject *)object; + +/*! + Gets the data for the given object from the offline database. Returns a task that will be + completed if data for the object was available. If the object is not in the cache, the task + will be faulted, with a CACHE_MISS error. + + @param object The object to fetch. + @param database A database connection to use. + */ +- (BFTask *)fetchObjectLocallyAsync:(PFObject *)object database:(PFSQLiteDatabase *)database; + +///-------------------------------------- +/// @name Save +///-------------------------------------- + +//TODO: (nlutsenko) Remove `includChildren` method, replace with PFLocalStore that wraps OfflineStore + Pin. +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object includeChildren:(BOOL)includeChildren; +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object withChildren:(NSArray *)children; + +/*! + Stores an object (and optionally, every object it points to recursively) in the local database. + If any of the objects have not been fetched from Parse, they will not be stored. However, if + they have changed data, the data will be retained. To get the objects back later, you can use a + ParseQuery with a cache policy that uses the local cache, or you can create an unfetched + pointer with ParseObject.createWithoutData() and then call fetchFromLocalDatastore() on it. + If you modify the object after saving it locally, such as by fetching it or saving it, + those changes will automatically be applied to the cache. + + @param object The root of the objects to save. + @param children If non-empty - these children will be saved to LDS as well. + @param database A database connection to use. + */ +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object + withChildren:(NSArray *)children + database:(PFSQLiteDatabase *)database; + +///-------------------------------------- +/// @name Find +///-------------------------------------- + +/*! + Runs a PFQueryState against the store's contents. + + @returns The objects that match the query's constraint. + */ +- (BFTask *)findAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin; + +/*! + Runs a PFQueryState against the store's contents. + + @returns The count of objects that match the query's constraint. + */ +- (BFTask *)countAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin; + +/*! + Runs a PFQueryState against the store's contents. + + @returns The objects that match the query's constraint. + */ +- (BFTask *)findAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin + isCount:(BOOL)isCount; + +/*! + Runs a PFQueryState against the store's contents. May cause any instances of the object to get fetched from + offline database. (TODO (hallucinogen): should we consider objects in memory but not in Offline Store?) + + @param queryState The query. + @param user The user making the query. + @param pin (Optional) The pin we're querying across. If null, all pins. + @param isCount YES if we're doing count. + @param database The PFSQLiteDatabase + + @returns The objects that match the query's constraint. + */ +- (BFTask *)findAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin + isCount:(BOOL)isCount + database:(PFSQLiteDatabase *)database; + +///-------------------------------------- +/// @name Update Internal State +///-------------------------------------- + +/*! + Takes an object that has been fetched from the database before and updates it with whatever + data is in memory. This will only be used when data comes back from the server after a fetch + or a save. + */ +- (BFTask *)updateDataForObjectAsync:(PFObject *)object; + +///-------------------------------------- +/// @name Delete +///-------------------------------------- + +/*! + Deletes the given object from Offline Store's pins + */ +- (BFTask *)deleteDataForObjectAsync:(PFObject *)object; + +///-------------------------------------- +/// @name Unpin +///-------------------------------------- + +- (BFTask *)unpinObjectAsync:(PFObject *)object; + +///-------------------------------------- +/// @name Internal Helper Methods +///-------------------------------------- + +/*! + Gets the UUID for the given object, if it has one. Otherwise, creates a new UUID for the object + and adds a new row to the database for the object with no data. + */ +- (BFTask *)getOrCreateUUIDAsyncForObject:(PFObject *)object + database:(PFSQLiteDatabase *)database; + +/*! + This should only be called from `PFObject.objectWithoutDataWithClassName`. + + @returns an object from OfflineStore cache. If nil is returned the object is not found in the cache. + */ +- (PFObject *)getOrCreateObjectWithoutDataWithClassName:(NSString *)className + objectId:(NSString *)objectId; + +/*! + When an object is finished saving, it gets an objectId. Then it should call this method to + clean up the bookeeping around ids. + */ +- (void)updateObjectIdForObject:(PFObject *)object + oldObjectId:(NSString *)oldObjectId + newObjectId:(NSString *)newObjectId; + +///-------------------------------------- +/// @name Unit Test Helper Methods +///-------------------------------------- + +/*! + Used in unit testing only. Clears all in-memory caches so that data must be retrieved from disk. + */ +- (void)simulateReboot; + +/*! + Used in unit testing only. Clears the database on disk. + */ +- (void)clearDatabase; + +@end diff --git a/Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.m b/Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.m new file mode 100644 index 000000000..899045c8c --- /dev/null +++ b/Parse/Internal/LocalDataStore/OfflineStore/PFOfflineStore.m @@ -0,0 +1,1069 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFOfflineStore.h" + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFDecoder.h" +#import "PFEncoder.h" +#import "PFErrorUtilities.h" +#import "PFFileManager.h" +#import "PFJSONSerialization.h" +#import "PFObjectPrivate.h" +#import "PFOfflineQueryLogic.h" +#import "PFPin.h" +#import "PFQueryPrivate.h" +#import "PFSQLiteDatabase.h" +#import "PFSQLiteDatabaseController.h" +#import "PFSQLiteDatabaseResult.h" +#import "PFUser.h" +#import "PFWeakValue.h" +#import "Parse_Private.h" + +typedef BFTask *(^PFOfflineStoreDatabaseExecutionBlock)(PFSQLiteDatabase *database); + +NSString *const PFOfflineStoreDatabaseName = @"ParseOfflineStore"; + +NSString *const PFOfflineStoreTableOfObjects = @"ParseObjects"; +NSString *const PFOfflineStoreKeyOfClassName = @"className"; +NSString *const PFOfflineStoreKeyOfIsDeletingEventually = @"isDeletingEventually"; +NSString *const PFOfflineStoreKeyOfJSON = @"json"; +NSString *const PFOfflineStoreKeyOfObjectId = @"objectId"; +NSString *const PFOfflineStoreKeyOfUUID = @"uuid"; + +NSString *const PFOfflineStoreTableOfDependencies = @"Dependencies"; +NSString *const PFOfflineStoreKeyOfKey = @"key"; + +int const PFOfflineStoreMaximumSQLVariablesCount = 999; + +@interface PFOfflineStore () + +@property (nonatomic, assign, readwrite) PFOfflineStoreOptions options; + +@property (nonatomic, strong, readonly) NSObject *lock; + +/*! + In-memory map of (className, objectId) to ParseObject. This is used so that we can + always return the same instance for a given object. Objects in this map may or may + not be in the database. + */ +@property (nonatomic, strong, readonly) NSMapTable *classNameAndObjectIdToObjectMap; + +/*! + In-memory set of ParseObjects that have been fetched from local database already. + If the object is in the map, a fetch of it has been started. If the value is a + finished task, then the fetch was completed. + */ +@property (nonatomic, strong, readonly) NSMapTable *fetchedObjects; + +/*! + In-memory map of ParseObject to UUID. This is used so that we can always return + the same instance for a given object. Objects in this map may or may not be in the + database. + */ +@property (nonatomic, strong, readonly) NSMapTable *objectToUUIDMap; + +/*! + In-memory map of UUID to ParseObject. This is used so we can always return + the same instance for a given object. The only objects in this map are ones that + are in database. + */ +@property (nonatomic, strong, readonly) NSMapTable *UUIDToObjectMap; + +@property (nonatomic, strong, readonly) PFOfflineQueryLogic *offlineQueryLogic; + +@property (nonatomic, strong, readonly) PFSQLiteDatabaseController *databaseController; + +@end + +@implementation PFOfflineStore + +@synthesize offlineQueryLogic = _offlineQueryLogic; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithFileManager:(PFFileManager *)fileManager options:(PFOfflineStoreOptions)options { + self = [super init]; + if (!self) return nil; + + _options = options; + _fileManager = fileManager; + _databaseController = [PFSQLiteDatabaseController controllerWithFileManager:_fileManager]; + _lock = [[NSObject alloc] init]; + _classNameAndObjectIdToObjectMap = [NSMapTable strongToWeakObjectsMapTable]; + _fetchedObjects = [NSMapTable weakToStrongObjectsMapTable]; + // This is a bit different from what we have in Android. The reason is because the object is quickly + // retained by the OS and we depend on this MapTable to fetch the `uuidTask` of the object. + _objectToUUIDMap = [NSMapTable weakToStrongObjectsMapTable]; + _UUIDToObjectMap = [NSMapTable strongToWeakObjectsMapTable]; + + [[self class] _initializeTablesInBackgroundWithDatabaseController:_databaseController]; + + return self; +} + +///-------------------------------------- +#pragma mark - Fetch +///-------------------------------------- + +- (BFTask *)fetchObjectLocallyAsync:(PFObject *)object { + __block BFTask *fetchTask = nil; + return [[self _performDatabaseOperationAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + // We need this to return the result of `fetchObjectLocallyAsync` instead of returning the + // result of `[database closeAsync]` + fetchTask = [self fetchObjectLocallyAsync:object database:database]; + return fetchTask; + }] continueWithBlock:^id(BFTask *task) { + return fetchTask; + }]; +} + +- (BFTask *)fetchObjectLocallyAsync:(PFObject *)object database:(PFSQLiteDatabase *)database { + BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; + BFTask *uuidTask = nil; + + @synchronized (self.lock) { + BFTask *fetchTask = [self.fetchedObjects objectForKey:object]; + if (fetchTask && !(self.options & PFOfflineStoreOptionAlwaysFetchFromSQLite)) { + // The object has been fetched from offline store, so any data that's in there + // is already reflected in the in-memory version. There's nothing more to do. + return [fetchTask continueWithBlock:^id(BFTask *task) { + return [BFTask taskWithResult:[task.result weakObject]]; + }]; + } + + // Put a placeholder so that anyone else who attempts to fetch this object will just + // wait for this call to finish doing it. + [self.fetchedObjects setObject:[tcs.task continueWithBlock:^id(BFTask *task) { + return [BFTask taskWithResult:[PFWeakValue valueWithWeakObject:task.result]]; + }] forKey:object]; + uuidTask = [self.objectToUUIDMap objectForKey:object]; + } + NSString *className = [object parseClassName]; + NSString *objectId = [object objectId]; + + // If this gets set, then it will contain data from offline store that need to be merged + // into existing object in memory + BFTask *jsonStringTask = [BFTask taskWithResult:nil]; + __block NSString *uuid = nil; + + if (objectId == nil) { + // This object has never been saved to Parse + if (uuidTask == nil) { + // This object was not pulled from the datastore or previously saved to it. + // There's nothing that can be fetched from it. This isn't an error, because it's + // really convenient to try to fetch objects from offline store just to make sure + // they're up-to-date, and we shouldn't force developers to specially handle this case. + } else { + // This object is a new ParseObject that is known to the datastore, but hasn't been + // fetched. The only way this could happen is if the object had previously been stored + // in the offline store, then the object was removed from memory (maybe by rebooting), + // and then an object with a pointer to it was fetched, so we only created the pointer. + // We need to pull the data out of the database using UUID. + + jsonStringTask = [[uuidTask continueWithSuccessBlock:^id(BFTask *task) { + uuid = task.result; + NSString *query = [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@ = ?;", + PFOfflineStoreKeyOfJSON, PFOfflineStoreTableOfObjects, PFOfflineStoreKeyOfUUID]; + return [database executeQueryAsync:query + withArgumentsInArray:[NSArray arrayWithObjects:uuid, nil]]; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + if (![result next]) { + [result close]; + [NSException raise:NSInternalInconsistencyException + format:@"Attempted to find non-existent uuid %@.", uuid]; + } + NSString *jsonString = [result stringForColumnIndex:0]; + [result close]; + + return jsonString; + }]; + } + } else { + if (uuidTask && !(self.options & PFOfflineStoreOptionAlwaysFetchFromSQLite)) { + // This object is an existing ParseObject, and we must've already pulled its data + // out of the offline store, or else we wouldn't know its UUID. This should never happen. + NSString *message = @"Object must have already been fetched but isn't marked as fetched."; + [tcs setException:[NSException exceptionWithName:NSInternalInconsistencyException + reason:message + userInfo:nil]]; + + @synchronized (self.lock) { + [self.fetchedObjects removeObjectForKey:object]; + } + return tcs.task; + } + + // We've got a pointer to an existing ParseObject, but we've never pulled its data out of + // the offline store. Since fetching from the server forces a fetch from the offline + // store, that means this is a pointer. We need to try to find any existing entry for this + // object in the database. + NSString *query = [NSString stringWithFormat:@"SELECT %@, %@ FROM %@ WHERE %@ = ? AND %@ = ?;", + PFOfflineStoreKeyOfJSON, PFOfflineStoreKeyOfUUID, + PFOfflineStoreTableOfObjects, PFOfflineStoreKeyOfClassName, + PFOfflineStoreKeyOfObjectId]; + NSArray *args = @[ className, objectId ]; + jsonStringTask = [[database executeQueryAsync:query + withArgumentsInArray:args] continueWithSuccessBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + if (![result next]) { + NSString *errorMessage = @"This object is not available in the offline cache."; + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorCacheMiss + message:errorMessage + shouldLog:NO]; + [result close]; + return [BFTask taskWithError:error]; + } + + NSString *jsonString = [result stringForColumnIndex:0]; + NSString *newUUID = [result stringForColumnIndex:1]; + [result close]; + + @synchronized (self.lock) { + // It's okay to put this object into the uuid map. No one will try to fetch it, + // because it's already in the fetchedObjects map. And no one will try to save it + // without fetching it first, so everything should be fine. + [self.objectToUUIDMap setObject:[BFTask taskWithResult:newUUID] forKey:object]; + [self.UUIDToObjectMap setObject:object forKey:newUUID]; + } + return jsonString; + }]; + } + + return [[jsonStringTask continueWithSuccessBlock:^id(BFTask *task) { + NSString *jsonString = task.result; + if (jsonString == nil) { + // This means we tried to fetch from the database that was never actually saved + // locally. This probably means that its parent object was saved locally and we + // just created a pointer to this object. This should be considered cache miss. + + NSString *errorMessage = @"Attempted to fetch and object offline which was never " + @"saved to the offline cache"; + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorCacheMiss + message:errorMessage + shouldLog:NO]; + return [BFTask taskWithError:error]; + } + id parsedJson = [PFJSONSerialization JSONObjectFromString:jsonString]; + NSMutableDictionary *offlineObjects = [[NSMutableDictionary alloc] init]; + [PFInternalUtils traverseObject:parsedJson usingBlock:^id(id object) { + // Omit root and PFObject + if ([object isKindOfClass:[NSDictionary class]] && + [((NSDictionary *)object)[@"__type"] isEqualToString:@"OfflineObject"] && + object != parsedJson) { + NSString *uuid = ((NSDictionary *)object)[@"uuid"]; + offlineObjects[uuid] = [self _getPointerAsyncWithUUID:uuid database:database]; + } + return object; + }]; + + NSArray *objectValues = [offlineObjects allValues]; + return [[BFTask taskForCompletionOfAllTasks:objectValues] continueWithSuccessBlock:^id(BFTask *task) { + PFDecoder *decoder = [PFOfflineDecoder decoderWithOfflineObjects:offlineObjects]; + [object mergeFromRESTDictionary:parsedJson withDecoder:decoder]; + return [BFTask taskWithResult:nil]; + }]; + }] continueWithBlock:^id(BFTask *task) { + if (task.isCancelled) { + [tcs cancel]; + } else if (task.error != nil) { + [tcs setError:task.error]; + } else if (task.exception != nil) { + [tcs setException:task.exception]; + } else { + [tcs setResult:object]; + } + return tcs.task; + }]; +} + +///-------------------------------------- +#pragma mark - Save +///-------------------------------------- + +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object includeChildren:(BOOL)includeChildren { + //TODO: (nlutsenko) Remove this method, replace with LocalStore implementation that wraps OfflineStore + Pin. + return [self _performDatabaseTransactionAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + return [self saveObjectLocallyAsync:object includeChildren:includeChildren database:database]; + }]; +} + +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object withChildren:(NSArray *)children { + return [self _performDatabaseTransactionAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + return [self saveObjectLocallyAsync:object withChildren:children database:database]; + }]; +} + +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object + includeChildren:(BOOL)includeChildren + database:(PFSQLiteDatabase *)database { + //TODO: (nlutsenko) Remove this method, replace with LocalStore implementation that wraps OfflineStore + Pin. + NSMutableArray *children = nil; + if (includeChildren) { + children = [NSMutableArray array]; + [PFInternalUtils traverseObject:object usingBlock:^id(id traversedObject) { + if ([traversedObject isKindOfClass:[PFObject class]]) { + [children addObject:traversedObject]; + } + return traversedObject; + }]; + } + return [self saveObjectLocallyAsync:object withChildren:children database:database]; +} + +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object + withChildren:(NSArray *)children + database:(PFSQLiteDatabase *)database { + //TODO (nlutsenko): Add assert that checks whether all children are actually children of an object. + NSMutableArray *objectsInTree = nil; + if (children == nil) { + objectsInTree = [NSMutableArray arrayWithObject:object]; + } else { + objectsInTree = [children mutableCopy]; + if (![objectsInTree containsObject:object]) { + [objectsInTree addObject:object]; + } + } + + // Call saveObjectLocallyAsync for each of them individually + NSMutableArray *tasks = [[NSMutableArray alloc] init]; + for (PFObject *objInTree in objectsInTree) { + [tasks addObject:[self fetchObjectLocallyAsync:objInTree database:database]]; + } + + return [[[[[BFTask taskForCompletionOfAllTasks:tasks] continueWithBlock:^id(BFTask *task) { + return [self.objectToUUIDMap objectForKey:object]; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSString *uuid = task.result; + if (uuid == nil) { + // The root object was never stored in offline store, so nothing unpin. + return [BFTask taskWithResult:nil]; + } + + // Delete all objects locally corresponding to the key we're trying to use in case it was + // used before (overwrite) + return [self _unpinKeyAsync:uuid database:database]; + }] continueWithSuccessBlock:^id(BFTask *task) { + return [self getOrCreateUUIDAsyncForObject:object database:database]; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSString *uuid = task.result; + + NSMutableArray *tasks = [[NSMutableArray alloc] init]; + for (PFObject *object in objectsInTree) { + [tasks addObject:[self saveObjectLocallyAsync:object key:uuid database:database]]; + } + + return [BFTask taskForCompletionOfAllTasks:tasks]; + }]; +} + +- (BFTask *)saveObjectLocallyAsync:(PFObject *)object + key:(NSString *)key + database:(PFSQLiteDatabase *)database { + if ([object objectId] != nil && ![object isDataAvailable] && + ![object _hasChanges] && ![object _hasOutstandingOperations]) { + return [BFTask taskWithResult:nil]; + } + + __block NSString *uuid = nil; + __block id encoded = nil; + return [[[[BFTask taskFromExecutor:[BFExecutor defaultExecutor] withBlock:^id{ + // Make sure we have UUID for the object to be saved. + return [self getOrCreateUUIDAsyncForObject:object database:database]; + }] continueWithSuccessBlock:^id(BFTask *task) { + uuid = task.result; + + // Encode the object, and wait for the UUIDs in its pointers to get encoded. + PFOfflineObjectEncoder *encoder = [PFOfflineObjectEncoder objectEncoderWithOfflineStore:self database:database]; + // We don't care about operationSetUUIDs here + NSArray *operationSetUUIDs = nil; + encoded = [object RESTDictionaryWithObjectEncoder:encoder operationSetUUIDs:&operationSetUUIDs]; + return [encoder encodeFinished]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // Time to actually save the object + NSString *className = [object parseClassName]; + NSString *objectId = [object objectId]; + NSString *encodedString = [PFJSONSerialization stringFromJSONObject:encoded]; + NSString *updateFields = nil; + NSArray *queryParams = nil; + + if (objectId != nil) { + updateFields = [NSString stringWithFormat:@"%@ = ?, %@ = ?, %@ = ?", + PFOfflineStoreKeyOfClassName, PFOfflineStoreKeyOfJSON, + PFOfflineStoreKeyOfObjectId]; + queryParams = @[className, encodedString, objectId, uuid]; + } else { + updateFields = [NSString stringWithFormat:@"%@ = ?, %@ = ?", + PFOfflineStoreKeyOfClassName, PFOfflineStoreKeyOfJSON]; + queryParams = @[className, encodedString, uuid]; + } + + NSString *sql = [NSString stringWithFormat:@"UPDATE %@ SET %@ WHERE %@ = ?", + PFOfflineStoreTableOfObjects, updateFields, + PFOfflineStoreKeyOfUUID]; + return [database executeSQLAsync:sql withArgumentsInArray:queryParams]; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSString *sql = [NSString stringWithFormat:@"INSERT OR IGNORE INTO %@(%@, %@) VALUES (?, ?)", + PFOfflineStoreTableOfDependencies, PFOfflineStoreKeyOfKey, + PFOfflineStoreKeyOfUUID]; + return [database executeSQLAsync:sql withArgumentsInArray:@[key, uuid]]; + }]; +} + +///-------------------------------------- +#pragma mark - Find +///-------------------------------------- + +- (BFTask *)findAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin { + return [self findAsyncForQueryState:queryState user:user pin:pin isCount:NO]; +} + +- (BFTask *)countAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin { + return [[self findAsyncForQueryState:queryState + user:user + pin:pin + isCount:YES] continueWithSuccessBlock:^id(BFTask *task) { + if (!task.cancelled && !task.error && !task.exception) { + NSArray *result = task.result; + return @(result.count); + } + return task; + }]; +} + +- (BFTask *)findAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin + isCount:(BOOL)isCount { + __block BFTask *resultTask = nil; + return [[self _performDatabaseOperationAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + resultTask = [self findAsyncForQueryState:queryState user:user pin:pin isCount:isCount database:database]; + return resultTask; + }] continueWithBlock:^id(BFTask *ignored) { + // We need this to return the result of `findQuery` instead of returning the + // result of `[database closeAsync]` + return resultTask; + }]; +} + +- (BFTask *)findAsyncForQueryState:(PFQueryState *)queryState + user:(PFUser *)user + pin:(PFPin *)pin + isCount:(BOOL)isCount + database:(PFSQLiteDatabase *)database { + __block NSMutableArray *mutableResults = [NSMutableArray array]; + BFTask *queryTask = nil; + BOOL includeIsDeletingEventually = queryState.shouldIncludeDeletingEventually; + + if (pin == nil) { + NSString *isDeletingEventuallyQuery = @""; + if (!includeIsDeletingEventually) { + isDeletingEventuallyQuery = [NSString stringWithFormat:@"AND %@ = 0", + PFOfflineStoreKeyOfIsDeletingEventually]; + } + NSString *queryString = [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@ = ? %@;", + PFOfflineStoreKeyOfUUID, PFOfflineStoreTableOfObjects, + PFOfflineStoreKeyOfClassName, isDeletingEventuallyQuery]; + queryTask = [database executeQueryAsync:queryString withArgumentsInArray:@[ queryState.parseClassName ]]; + } else { + BFTask *uuidTask = [self.objectToUUIDMap objectForKey:pin]; + if (uuidTask == nil) { + // Pin was never saved locally, therefore there won't be any results. + return [BFTask taskWithResult:mutableResults]; + } + + queryTask = [uuidTask continueWithSuccessBlock:^id(BFTask *task) { + NSString *uuid = task.result; + NSString *isDeletingEventuallyQuery = @""; + if (!includeIsDeletingEventually) { + isDeletingEventuallyQuery = [NSString stringWithFormat:@"AND %@ = 0", + PFOfflineStoreKeyOfIsDeletingEventually]; + } + NSString *queryString = [NSString stringWithFormat:@"SELECT A.%@ FROM %@ A " + @"INNER JOIN %@ B ON A.%@ = B.%@ WHERE %@ = ? AND %@ = ? %@;", + PFOfflineStoreKeyOfUUID, PFOfflineStoreTableOfObjects, + PFOfflineStoreTableOfDependencies, PFOfflineStoreKeyOfUUID, + PFOfflineStoreKeyOfUUID, PFOfflineStoreKeyOfClassName, + PFOfflineStoreKeyOfKey, isDeletingEventuallyQuery]; + + return [database executeQueryAsync:queryString + withArgumentsInArray:@[ queryState.parseClassName, uuid ]]; + }]; + } + + @weakify(self); + return [[queryTask continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFSQLiteDatabaseResult *result = task.result; + + PFConstraintMatcherBlock matcherBlock = [self.offlineQueryLogic createMatcherForQueryState:queryState + user:user]; + + BFTask *checkAllObjectsTask = [BFTask taskWithResult:nil]; + while ([result next]) { + NSString *uuid = [result stringForColumnIndex:0]; + __block PFObject *object = nil; + + checkAllObjectsTask = [[[[checkAllObjectsTask continueWithSuccessBlock:^id(BFTask *task) { + return [self _getPointerAsyncWithUUID:uuid database:database]; + }] continueWithSuccessBlock:^id(BFTask *task) { + object = task.result; + return [self fetchObjectLocallyAsync:object database:database]; + }] continueWithSuccessBlock:^id(BFTask *task) { + if (!object.isDataAvailable) { + return [BFTask taskWithResult:@NO]; + } + return matcherBlock(object, database); + }] continueWithSuccessBlock:^id(BFTask *task) { + if ([task.result boolValue]) { + [mutableResults addObject:object]; + } + return [BFTask taskWithResult:nil]; + }]; + } + [result close]; + + return checkAllObjectsTask; + }] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + + // Sort, Apply Skip and Limit + + PFOfflineQueryOption queryOptions = 0; + if (!isCount) { + queryOptions = PFOfflineQueryOptionOrder | PFOfflineQueryOptionSkip | PFOfflineQueryOptionLimit; + } + NSArray *results = [self.offlineQueryLogic resultsByApplyingOptions:queryOptions + ofQueryState:queryState + toResults:mutableResults]; + + // Fetch includes + BFTask *fetchIncludesTask = [self.offlineQueryLogic fetchIncludesAsyncForResults:results + ofQueryState:queryState + inDatabase:database]; + + return [fetchIncludesTask continueWithSuccessBlock:^id(BFTask *task) { + return results; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Update +///-------------------------------------- + +- (BFTask *)updateDataForObjectAsync:(PFObject *)object { + BFTask *fetchTask = nil; + + @synchronized (self.lock) { + fetchTask = [self.fetchedObjects objectForKey:object]; + if (!fetchTask) { + NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"An object cannot be updated if it wasn't fetched" + userInfo:nil]; + return [BFTask taskWithException:exception]; + } + } + return [fetchTask continueWithBlock:^id(BFTask *task) { + if (task.error != nil) { + // Catch CACHE_MISS errors and ignore them. + if (task.error.code == kPFErrorCacheMiss) { + return [BFTask taskWithResult:nil]; + } + return [BFTask taskWithResult:[task.result weakObject]]; + } + + return [self _performDatabaseTransactionAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + return [self _updateDataForObjectAsync:object inDatabase:database]; + }]; + }]; +} + +- (BFTask *)_updateDataForObjectAsync:(PFObject *)object inDatabase:(PFSQLiteDatabase *)database { + BFTask *uuidTask = nil; + @synchronized (self.lock) { + uuidTask = [self.objectToUUIDMap objectForKey:object]; + if (!uuidTask) { + // It was fetched, but it has no UUID. That must mean it isn't actually in the database. + return [BFTask taskWithResult:nil]; + } + } + + __block NSString *uuid = nil; + __block NSDictionary *dataDictionary = nil; + return [[uuidTask continueWithSuccessBlock:^id(BFTask *task) { + uuid = task.result; + + PFOfflineObjectEncoder *encoder = [PFOfflineObjectEncoder objectEncoderWithOfflineStore:self + database:database]; + NSArray *operationSetUUIDs = nil; + dataDictionary = [object RESTDictionaryWithObjectEncoder:encoder operationSetUUIDs:&operationSetUUIDs]; + return [encoder encodeFinished]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // Put it in database + NSString *className = object.parseClassName; + NSString *objectId = object.objectId; + NSString *encodedDataDictionary = [PFJSONSerialization stringFromJSONObject:dataDictionary]; + NSNumber *deletingEventually = dataDictionary[PFOfflineStoreKeyOfIsDeletingEventually]; + + NSString *updateParams = nil; + NSArray *updateArguments = nil; + if (objectId != nil) { + updateParams = [NSString stringWithFormat:@"%@ = ?, %@ = ?, %@ = ?, %@ = ?", + PFOfflineStoreKeyOfClassName, PFOfflineStoreKeyOfJSON, + PFOfflineStoreKeyOfIsDeletingEventually, PFOfflineStoreKeyOfObjectId]; + updateArguments = @[ className, encodedDataDictionary, deletingEventually, objectId, uuid ]; + } else { + updateParams = [NSString stringWithFormat:@"%@ = ?, %@ = ?, %@ = ?", + PFOfflineStoreKeyOfClassName, PFOfflineStoreKeyOfJSON, + PFOfflineStoreKeyOfIsDeletingEventually]; + updateArguments = @[ className, encodedDataDictionary, deletingEventually, uuid ]; + } + + NSString *sql = [NSString stringWithFormat:@"UPDATE %@ SET %@ WHERE %@ = ?", + PFOfflineStoreTableOfObjects, updateParams, PFOfflineStoreKeyOfUUID]; + + return [database executeSQLAsync:sql withArgumentsInArray:updateArguments]; + }]; +} + +///-------------------------------------- +#pragma mark - Delete +///-------------------------------------- + +- (BFTask *)deleteDataForObjectAsync:(PFObject *)object { + return [self _performDatabaseTransactionAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + return [self deleteDataForObjectAsync:object database:database]; + }]; +} + +- (BFTask *)deleteDataForObjectAsync:(PFObject *)object database:(PFSQLiteDatabase *)database { + __block NSString *uuid = nil; + + // Make sure the object has a UUID. + BFTask *uuidTask = nil; + @synchronized (self.lock) { + uuidTask = [self.objectToUUIDMap objectForKey:object]; + if (!uuidTask) { + // It was fetched, but it has no UUID. That must mean it isn't actually in the database. + return [BFTask taskWithResult:nil]; + } + } + + uuidTask = [uuidTask continueWithSuccessBlock:^id(BFTask *task) { + uuid = task.result; + return task; + }]; + + // If the object was the root of a pin, unpin it. + BFTask *unpinTask = [[uuidTask continueWithSuccessBlock:^id(BFTask *task) { + // Find all the roots for this object. + NSString *sql = [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@ = ?", + PFOfflineStoreKeyOfKey, PFOfflineStoreTableOfDependencies, + PFOfflineStoreKeyOfUUID]; + return [database executeQueryAsync:sql withArgumentsInArray:@[ uuid ]]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // Try to unpin this object from the pin label if it's a root of the PFPin. + PFSQLiteDatabaseResult *result = task.result; + NSMutableArray *tasks = [NSMutableArray array]; + + while (result.next) { + NSString *objectUUID = [result stringForColumnIndex:0]; + + BFTask *getPointerTask = [self _getPointerAsyncWithUUID:objectUUID database:database]; + BFTask *objectUnpinTask = [[getPointerTask continueWithSuccessBlock:^id(BFTask *task) { + PFPin *pin = task.result; + return [self fetchObjectLocallyAsync:pin database:database]; + }] continueWithBlock:^id(BFTask *task) { + PFPin *pin = task.result; + + NSMutableArray *modified = pin.objects; + if (modified == nil || ![modified containsObject:object]) { + return task; + } + + [modified removeObject:object]; + if (modified.count == 0) { + return [self _unpinKeyAsync:objectUUID database:database]; + } + pin.objects = modified; + + return [self saveObjectLocallyAsync:pin includeChildren:YES database:database]; + }]; + [tasks addObject:objectUnpinTask]; + } + [result close]; + + return [BFTask taskForCompletionOfAllTasks:tasks]; + }]; + + return [[[unpinTask continueWithSuccessBlock:^id(BFTask *task) { + NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ = ?", + PFOfflineStoreTableOfDependencies, PFOfflineStoreKeyOfUUID]; + return [database executeSQLAsync:sql withArgumentsInArray:@[ uuid ]]; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ = ?", + PFOfflineStoreTableOfObjects, PFOfflineStoreKeyOfUUID]; + return [database executeSQLAsync:sql withArgumentsInArray:@[ uuid ]]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // Delete the object from memory cache. + // (or else `PFObject.objectWithoutDataWithClassName` will return a valid object) + @synchronized (self.lock) { + // TODO (hallucinogen): we should probably clean up UUIDToObjectMap and objectToUUIDMap + // but getting the uuid requires a task and things might get a little funky... + if (object.objectId != nil) { + NSString *key = [self _generateKeyForClassName:object.parseClassName objectId:object.objectId]; + [self.classNameAndObjectIdToObjectMap removeObjectForKey:key]; + } + [self.fetchedObjects removeObjectForKey:object]; + } + return task; + }]; +} + +///-------------------------------------- +#pragma mark - Unpin +///-------------------------------------- + +- (BFTask *)unpinObjectAsync:(PFObject *)object { + BFTask *uuidTask = [self.objectToUUIDMap objectForKey:object]; + return [uuidTask continueWithBlock:^id(BFTask *task) { + NSString *uuid = task.result; + if (!uuid) { + // The root object was never stored in the offline store, so nothing to unpin. + return [BFTask taskWithResult:nil]; + } + return [self _unpinKeyAsync:uuid]; + }]; +} + +- (BFTask *)_unpinKeyAsync:(NSString *)key { + return [self _performDatabaseTransactionAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + return [self _unpinKeyAsync:key database:database]; + }]; +} + +- (BFTask *)_unpinKeyAsync:(NSString *)key database:(PFSQLiteDatabase *)database { + NSMutableArray *uuidsToDelete = [NSMutableArray array]; + // Fetch all uuids from Dependencies for key=? grouped by uuid having a count of 1 + NSString *query = [NSString stringWithFormat:@"SELECT %@ FROM %@ WHERE %@ = ? AND %@ IN " + @"(SELECT %@ FROM %@ GROUP BY %@ HAVING COUNT(%@) = 1);", + PFOfflineStoreKeyOfUUID, PFOfflineStoreTableOfDependencies, + PFOfflineStoreKeyOfKey, PFOfflineStoreKeyOfUUID, PFOfflineStoreKeyOfUUID, + PFOfflineStoreTableOfDependencies, PFOfflineStoreKeyOfUUID, + PFOfflineStoreKeyOfUUID]; + return [[[[database executeQueryAsync:query + withArgumentsInArray:@[ key ]] continueWithSuccessBlock:^id(BFTask *task) { + // DELETE FROM Objects + PFSQLiteDatabaseResult *result = task.result; + while (result.next) { + [uuidsToDelete addObject:[result stringForColumnIndex:0]]; + } + [result close]; + + return [self _deleteObjectsWithUUIDs:uuidsToDelete database:database]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // DELETE FROM Dependencies + NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ = ?", + PFOfflineStoreTableOfDependencies, PFOfflineStoreKeyOfKey]; + return [database executeSQLAsync:sql withArgumentsInArray:@[ key ]]; + }] continueWithSuccessBlock:^id(BFTask *task) { + @synchronized (self.lock) { + // Remove uuids from memory + for (NSString *uuid in uuidsToDelete) { + PFObject *object = [self.UUIDToObjectMap objectForKey:uuid]; + if (object != nil) { + [self.objectToUUIDMap removeObjectForKey:object]; + [self.UUIDToObjectMap removeObjectForKey:uuid]; + } + } + } + return [BFTask taskWithResult:nil]; + }]; +} + +- (BFTask *)_deleteObjectsWithUUIDs:(NSArray *)uuids database:(PFSQLiteDatabase *)database { + if (uuids.count <= 0) { + return [BFTask taskWithResult:nil]; + } + + if (uuids.count > PFOfflineStoreMaximumSQLVariablesCount) { + NSRange range = NSMakeRange(0, PFOfflineStoreMaximumSQLVariablesCount); + return [[self _deleteObjectsWithUUIDs:[uuids subarrayWithRange:range] + database:database] continueWithSuccessBlock:^id(BFTask *task) { + unsigned long includedCount = uuids.count - PFOfflineStoreMaximumSQLVariablesCount; + NSRange range = NSMakeRange(PFOfflineStoreMaximumSQLVariablesCount, includedCount); + return [self _deleteObjectsWithUUIDs:[uuids subarrayWithRange:range] database:database]; + }]; + } + + NSMutableArray *placeholders = [NSMutableArray array]; + for (int i = 0; i < uuids.count; ++i) { + [placeholders addObject:@"?"]; + } + NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@ IN (%@);", + PFOfflineStoreTableOfObjects, PFOfflineStoreKeyOfUUID, + [placeholders componentsJoinedByString:@","]]; + return [database executeSQLAsync:sql withArgumentsInArray:uuids]; +} + +///-------------------------------------- +#pragma mark - Internal Helper Methods +///-------------------------------------- + +- (BFTask *)getOrCreateUUIDAsyncForObject:(PFObject *)object + database:(PFSQLiteDatabase *)database { + NSString *newUUID = [[NSUUID UUID] UUIDString]; + BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource]; + + @synchronized (self.lock) { + BFTask *uuidTask = [self.objectToUUIDMap objectForKey:object]; + if (uuidTask != nil) { + // Return existing task. + return uuidTask; + } + + // The object doesn't have UUID yet, so we're gonna have to make one + [self.objectToUUIDMap setObject:tcs.task forKey:object]; + [self.UUIDToObjectMap setObject:object forKey:newUUID]; + + __weak id weakObject = object; + [self.fetchedObjects setObject:[tcs.task continueWithSuccessBlock:^id(BFTask *task) { + return [PFWeakValue valueWithWeakObject:weakObject]; + }] forKey:object]; + } + + // We need to put a placeholder row in the database so that later on the save can be just + // an update. This could be a pointer to an object that itself never gets saved offline, + // in which case the consumer will just have to deal with that. + NSString *query = [NSString stringWithFormat:@"INSERT INTO %@(%@, %@) VALUES(?, ?);", + PFOfflineStoreTableOfObjects, PFOfflineStoreKeyOfUUID, PFOfflineStoreKeyOfClassName]; + [[database executeSQLAsync:query + withArgumentsInArray:@[ newUUID, [object parseClassName]]] continueWithSuccessBlock:^id(BFTask *task) { + [tcs setResult:newUUID]; + return [BFTask taskWithResult:nil]; + }]; + + return tcs.task; +} + +/*! + Gets an unfetched pointer to an object in the database, based on its uuid. The object may or may + not be in memory, but it must be in database. If it is already in memory, the instance will be + returned. Since this is only for creating pointers to objects that are referenced by other objects + in the datastore, it's a fair assumption. + + @param uuid The UUID of the object to retrieve. + @param database The database instance to retrieve from. + @returns The object with that UUID. + */ +- (BFTask *)_getPointerAsyncWithUUID:(NSString *)uuid + database:(PFSQLiteDatabase *)database { + @synchronized (self.lock) { + PFObject *existing = [self.UUIDToObjectMap objectForKey:uuid]; + if (existing != nil) { + return [BFTask taskWithResult:existing]; + } + } + + // We only want the pointer, but we have to look in the database to know if there's something + // with this classname and object id already. + NSString *query = [NSString stringWithFormat:@"SELECT %@, %@ FROM %@ WHERE %@ = ?;", + PFOfflineStoreKeyOfClassName, PFOfflineStoreKeyOfObjectId, + PFOfflineStoreTableOfObjects, PFOfflineStoreKeyOfUUID]; + return [[database executeQueryAsync:query + withArgumentsInArray:@[ uuid ]] continueWithSuccessBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + if (![result next]) { + [result close]; + [NSException raise:NSInternalInconsistencyException + format:@"Attempted to find non-existent uuid %@", uuid]; + } + + @synchronized (self.lock) { + PFObject *existing = [self.UUIDToObjectMap objectForKey:uuid]; + if (existing != nil) { + [result close]; + return existing; + } + + NSString *className = [result stringForColumnIndex:0]; + NSString *objectId = [result stringForColumnIndex:1]; + [result close]; + + PFObject *pointer = [PFObject objectWithoutDataWithClassName:className objectId:objectId]; + + // If it doesn't have objectId, we don't really need the UUID, and this simplifies some + // other logic elsewhere if we only update the map for new objects. + if (objectId == nil) { + [self.UUIDToObjectMap setObject:pointer forKey:uuid]; + [self.objectToUUIDMap setObject:[BFTask taskWithResult:uuid] forKey:pointer]; + } + return pointer; + } + }]; +} + +- (PFObject *)getOrCreateObjectWithoutDataWithClassName:(NSString *)className + objectId:(NSString *)objectId { + PFParameterAssert(objectId, @"objectId cannot be nil."); + + PFObject *object = nil; + @synchronized (self.lock) { + NSString *key = [self _generateKeyForClassName:className objectId:objectId]; + object = [self.classNameAndObjectIdToObjectMap objectForKey:key]; + if (!object) { + object = [PFObject objectWithClassName:className objectId:objectId completeData:NO]; + [self updateObjectIdForObject:object oldObjectId:nil newObjectId:objectId]; + } + } + return object; +} + +- (void)updateObjectIdForObject:(PFObject *)object + oldObjectId:(NSString *)oldObjectId + newObjectId:(NSString *)newObjectId { + if (oldObjectId != nil) { + if ([oldObjectId isEqualToString:newObjectId]) { + return; + } + [NSException raise:NSInternalInconsistencyException format:@"objectIds cannot be changed in offline mode."]; + } + + NSString *className = object.parseClassName; + NSString *key = [self _generateKeyForClassName:className objectId:newObjectId]; + + @synchronized (self.lock) { + // See if there's already an entry for new objectId. + PFObject *existing = [self.classNameAndObjectIdToObjectMap objectForKey:key]; + PFConsistencyAssert(existing == nil || existing == object, + @"Attempted to change an objectId to one that's already known to the OfflineStore."); + + // Okay, all clear to add the new reference. + [self.classNameAndObjectIdToObjectMap setObject:object forKey:key]; + } +} + +- (NSString *)_generateKeyForClassName:(NSString *)className + objectId:(NSString *)objectId { + return [NSString stringWithFormat:@"%@:%@", className, objectId]; +} + +// TODO (hallucinogen): is this the right way to store the schema? ++ (NSString *)PFOfflineStoreParseObjectsTableSchema { + return [NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ (" + @"%@ TEXT PRIMARY KEY, " + @"%@ TEXT NOT NULL, " + @"%@ TEXT, " + @"%@ TEXT, " + @"%@ INTEGER DEFAULT 0, " + @"UNIQUE(%@, %@));", PFOfflineStoreTableOfObjects, PFOfflineStoreKeyOfUUID, + PFOfflineStoreKeyOfClassName, PFOfflineStoreKeyOfObjectId, PFOfflineStoreKeyOfJSON, + PFOfflineStoreKeyOfIsDeletingEventually, PFOfflineStoreKeyOfClassName, + PFOfflineStoreKeyOfObjectId]; +} + ++ (NSString *)PFOfflineStoreDependenciesTableSchema { + return [NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ (" + @"%@ TEXT NOT NULL, " + @"%@ TEXT NOT NULL, " + @"PRIMARY KEY(%@, %@));", PFOfflineStoreTableOfDependencies, PFOfflineStoreKeyOfKey, + PFOfflineStoreKeyOfUUID, PFOfflineStoreKeyOfKey, PFOfflineStoreKeyOfUUID]; +} + ++ (BFTask *)_initializeTablesInBackgroundWithDatabaseController:(PFSQLiteDatabaseController *)databaseController { + return [[databaseController openDatabaseWithNameAsync:PFOfflineStoreDatabaseName] continueWithBlock:^id(BFTask *task) { + PFSQLiteDatabase *database = task.result; + return [[[[[database beginTransactionAsync] continueWithSuccessBlock:^id(BFTask *task) { + return [database executeSQLAsync:[self PFOfflineStoreParseObjectsTableSchema] withArgumentsInArray:nil]; + }] continueWithSuccessBlock:^id(BFTask *task) { + return [database executeSQLAsync:[self PFOfflineStoreDependenciesTableSchema] withArgumentsInArray:nil]; + }] continueWithSuccessBlock:^id(BFTask *task) { + return [database commitAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database closeAsync]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Database Helpers +///-------------------------------------- + +- (BFTask *)_performDatabaseTransactionAsyncWithBlock:(PFOfflineStoreDatabaseExecutionBlock)block { + return [self _performDatabaseOperationAsyncWithBlock:^BFTask *(PFSQLiteDatabase *database) { + BFTask *task = [database beginTransactionAsync]; + task = [task continueWithSuccessBlock:^id(BFTask *task) { + return block(database); + }]; + return [task continueWithSuccessBlock:^id(BFTask *task) { + return [database commitAsync]; + }]; + }]; +} + +- (BFTask *)_performDatabaseOperationAsyncWithBlock:(PFOfflineStoreDatabaseExecutionBlock)block { + return [[self.databaseController openDatabaseWithNameAsync:PFOfflineStoreDatabaseName] continueWithBlock:^id(BFTask *task) { + PFSQLiteDatabase *database = task.result; + return [block(database) continueWithBlock:^id(BFTask *task) { + return [database closeAsync]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (PFOfflineQueryLogic *)offlineQueryLogic { + @synchronized (self.lock) { + if (!_offlineQueryLogic) { + _offlineQueryLogic = [[PFOfflineQueryLogic alloc] initWithOfflineStore:self]; + } + return _offlineQueryLogic; + } +} + +///-------------------------------------- +#pragma mark - Unit Test helper +///-------------------------------------- + +- (void)simulateReboot { + @synchronized (self.lock) { + [self.UUIDToObjectMap removeAllObjects]; + [self.objectToUUIDMap removeAllObjects]; + [self.classNameAndObjectIdToObjectMap removeAllObjects]; + [self.fetchedObjects removeAllObjects]; + } +} + +- (void)clearDatabase { + // Delete DB file + NSString *filePath = [self.fileManager parseDataItemPathForPathComponent:PFOfflineStoreDatabaseName]; + [[PFFileManager removeItemAtPathAsync:filePath] waitForResult:nil withMainThreadWarning:NO]; + + // Reinitialize tables + [PFOfflineStore _initializeTablesInBackgroundWithDatabaseController:self.databaseController]; +} + +@end diff --git a/Parse/Internal/LocalDataStore/Pin/PFPin.h b/Parse/Internal/LocalDataStore/Pin/PFPin.h new file mode 100644 index 000000000..df70eeadb --- /dev/null +++ b/Parse/Internal/LocalDataStore/Pin/PFPin.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#else +#import +#import +#endif + +extern NSString *const PFPinKeyName; +extern NSString *const PFPinKeyObjects; + +/*! + PFPin represent internal pin implementation of PFObject's `pin`. + */ +@interface PFPin : PFObject + +@property (nonatomic, copy) NSString *name; +@property (nonatomic, strong) NSMutableArray *objects; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithName:(NSString *)name; ++ (instancetype)pinWithName:(NSString *)name; + +@end diff --git a/Parse/Internal/LocalDataStore/Pin/PFPin.m b/Parse/Internal/LocalDataStore/Pin/PFPin.m new file mode 100644 index 000000000..3519b474f --- /dev/null +++ b/Parse/Internal/LocalDataStore/Pin/PFPin.m @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPin.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFObject+Subclass.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFQueryPrivate.h" +#import "Parse_Private.h" + +NSString *const PFPinKeyName = @"_name"; +NSString *const PFPinKeyObjects = @"_objects"; + +@implementation PFPin + +///-------------------------------------- +#pragma mark - PFSubclassing +///-------------------------------------- + ++ (NSString *)parseClassName { + return @"_Pin"; +} + +// Validates a class name. We override this to only allow the pin class name. ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert([className isEqualToString:[self parseClassName]], + @"Cannot initialize a PFPin with a custom class name."); +} + +- (BOOL)needsDefaultACL { + return NO; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithName:(NSString *)name { + self = [super init]; + if (!self) return nil; + + // Use property accessor, as there is no ivar here for `name`. + self.name = name; + + return self; +} + ++ (instancetype)pinWithName:(NSString *)name { + return [[self alloc] initWithName:name]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (NSString *)name { + return self[PFPinKeyName]; +} + +- (void)setName:(NSString *)name { + self[PFPinKeyName] = [name copy]; +} + +- (NSMutableArray *)objects { + return self[PFPinKeyObjects]; +} + +- (void)setObjects:(NSMutableArray *)objects { + self[PFPinKeyObjects] = [objects mutableCopy]; +} + +@end diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.h b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.h new file mode 100644 index 000000000..67499ad7e --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.h @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFFileManager; + +/*! + Argument count given in executeSQLAsync or executeQueryAsync is invalid. + */ +extern int const PFSQLiteDatabaseInvalidArgumenCountErrorCode; + +/*! + Method `executeSQL` cannot execute SELECT. Use `executeQuery` instead. + */ +extern int const PFSQLiteDatabaseInvalidSQL; + +/*! + Database is opened already. + */ +extern int const PFSQLiteDatabaseDatabaseAlreadyOpened; + +/*! + Database is closed already. + */ +extern int const PFSQLiteDatabaseDatabaseAlreadyClosed; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFSQLiteDatabase : NSObject + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithPath:(NSString *)path; + +///-------------------------------------- +/// @name Database Creation +///-------------------------------------- + ++ (instancetype)databaseWithPath:(NSString *)path; + +///-------------------------------------- +/// @name Connection +///-------------------------------------- + +/*! + @returns A `BFTask` that resolves to `YES` if the database is open. + */ +- (BFTask *)isOpenAsync; + +/*! + Opens database. Database is one time use. Open > Close > Open is forbidden. + */ +- (BFTask *)openAsync; + +/*! + Closes the database connection. + */ +- (BFTask *)closeAsync; + +///-------------------------------------- +/// @name Transaction +///-------------------------------------- + +/*! + Begins a database transaction in EXCLUSIVE mode. + */ +- (BFTask *)beginTransactionAsync; + +/*! + Commits running transaction. + */ +- (BFTask *)commitAsync; + +/*! + Rollbacks running transaction. + */ +- (BFTask *)rollbackAsync; + +///-------------------------------------- +/// @name Query Methods +///-------------------------------------- + +/*! + Runs a single SQL statement which return result (SELECT). + */ +- (BFTask *)executeQueryAsync:(NSString *)sql withArgumentsInArray:(nullable NSArray *)args; + +/*! + Runs a single SQL statement, while caching the resulting statement for future use. + */ +- (BFTask *)executeCachedQueryAsync:(NSString *)sql withArgumentsInArray:(nullable NSArray *)args; + +/*! + Runs a single SQL statement which doesn't return result (UPDATE/INSERT/DELETE). + */ +- (BFTask *)executeSQLAsync:(NSString *)sql withArgumentsInArray:(nullable NSArray *)args; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.m b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.m new file mode 100644 index 000000000..98f17016b --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase.m @@ -0,0 +1,341 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSQLiteDatabase.h" +#import "PFSQLiteDatabase_Private.h" + +#import + +#import +#import + +#import "BFTask+Private.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFMacros.h" +#import "PFMultiProcessFileLockController.h" +#import "PFSQLiteDatabaseResult.h" +#import "PFSQLiteStatement.h" +#import "Parse_Private.h" + +NSString *const PFSQLiteDatabaseBeginExclusiveOperationCommand = @"BEGIN EXCLUSIVE"; +NSString *const PFSQLiteDatabaseCommitOperationCommand = @"COMMIT"; +NSString *const PFSQLiteDatabaseRollbackOperationCommand = @"ROLLBACK"; + +NSString *const PFSQLiteDatabaseErrorSQLiteDomain = @"SQLite"; +NSString *const PFSQLiteDatabaseErrorPFSQLiteDatabaseDomain = @"PFSQLiteDatabase"; + +char *const PFSQLiteDatabaseDispatchQueue = "com.parse.PFSQLiteDatabase"; + +int const PFSQLiteDatabaseInvalidArgumenCountErrorCode = 1; +int const PFSQLiteDatabaseInvalidSQL = 2; +int const PFSQLiteDatabaseDatabaseAlreadyOpened = 3; +int const PFSQLiteDatabaseDatabaseAlreadyClosed = 4; + +@interface PFSQLiteDatabase () { + BFTaskCompletionSource *_databaseClosedTaskCompletionSource; + dispatch_queue_t _databaseQueue; + BFExecutor *_databaseExecutor; + NSMutableDictionary *_cachedStatements; +} + +/*! + Database instance + */ +@property (nonatomic, assign) sqlite3 *database; + +/*! + Database path + */ +@property (nonatomic, copy) NSString *databasePath; + +@end + +@implementation PFSQLiteDatabase + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithPath:(NSString *)path { + self = [super init]; + if (!self) return nil; + + _databaseClosedTaskCompletionSource = [[BFTaskCompletionSource alloc] init]; + _databasePath = [path copy]; + _databaseQueue = dispatch_queue_create("com.parse.sqlite.db.queue", DISPATCH_QUEUE_SERIAL); + _databaseExecutor = [BFExecutor executorWithDispatchQueue:_databaseQueue]; + _cachedStatements = [[NSMutableDictionary alloc] init]; + + return self; +} + ++ (instancetype)databaseWithPath:(NSString *)path { + return [[self alloc] initWithPath:path]; +} + +///-------------------------------------- +#pragma mark - Connection +///-------------------------------------- + +- (BFTask *)isOpenAsync { + return [BFTask taskFromExecutor:_databaseExecutor withBlock:^id { + return @(self.database != nil); + }]; +} + +- (BFTask *)openAsync { + return [BFTask taskFromExecutor:_databaseExecutor withBlock:^id { + if (self.database) { + NSError *error = [self _errorWithErrorCode:PFSQLiteDatabaseDatabaseAlreadyOpened + errorMessage:@"Database is opened already." + domain:PFSQLiteDatabaseErrorPFSQLiteDatabaseDomain]; + return [BFTask taskWithError:error]; + } + + // Check if this database have already been opened before. + if (_databaseClosedTaskCompletionSource.task.completed) { + NSError *error = [self _errorWithErrorCode:PFSQLiteDatabaseDatabaseAlreadyClosed + errorMessage:@"Closed database cannot be reopen." + domain:PFSQLiteDatabaseErrorPFSQLiteDatabaseDomain]; + return [BFTask taskWithError:error]; + } + + // Lock the file to avoid multi-process access. + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:self.databasePath]; + + sqlite3 *db; + int resultCode = sqlite3_open([self.databasePath UTF8String], &db); + if (resultCode != SQLITE_OK) { + return [BFTask taskWithError:[self _errorWithErrorCode:resultCode]]; + } + + self.database = db; + return [BFTask taskWithResult:nil]; + }]; +} + +- (BFTask *)closeAsync { + return [BFTask taskFromExecutor:_databaseExecutor withBlock:^id { + if (!self.database) { + NSError *error = [self _errorWithErrorCode:PFSQLiteDatabaseDatabaseAlreadyClosed + errorMessage:@"Database is closed already." + domain:PFSQLiteDatabaseErrorPFSQLiteDatabaseDomain]; + return [BFTask taskWithError:error]; + } + + [self _clearCachedStatements]; + int resultCode = sqlite3_close(self.database); + + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:self.databasePath]; + + if (resultCode == SQLITE_OK) { + + self.database = nil; + [_databaseClosedTaskCompletionSource setResult:nil]; + } else { + // Returns error + [_databaseClosedTaskCompletionSource setError:[self _errorWithErrorCode:resultCode]]; + } + return _databaseClosedTaskCompletionSource.task; + }]; +} + +///-------------------------------------- +#pragma mark - Transaction +///-------------------------------------- + +- (BFTask *)beginTransactionAsync { + return [self executeSQLAsync:PFSQLiteDatabaseBeginExclusiveOperationCommand + withArgumentsInArray:nil]; +} + +- (BFTask *)commitAsync { + return [self executeSQLAsync:PFSQLiteDatabaseCommitOperationCommand + withArgumentsInArray:nil]; +} + +- (BFTask *)rollbackAsync { + return [self executeSQLAsync:PFSQLiteDatabaseRollbackOperationCommand + withArgumentsInArray:nil]; +} + +///-------------------------------------- +#pragma mark - Query Methods +///-------------------------------------- + +- (BFTask *)_executeQueryAsync:(NSString *)sql withArgumentsInArray:(NSArray *)args cachingEnabled:(BOOL)enableCaching { + int resultCode = 0; + PFSQLiteStatement *statement = enableCaching ? [self _cachedStatementForQuery:sql] : nil; + if (!statement) { + sqlite3_stmt *sqliteStatement = nil; + resultCode = sqlite3_prepare_v2(self.database, [sql UTF8String], -1, &sqliteStatement, 0); + if (resultCode != SQLITE_OK) { + sqlite3_finalize(sqliteStatement); + return [BFTask taskWithError:[self _errorWithErrorCode:resultCode]]; + } + statement = [[PFSQLiteStatement alloc] initWithStatement:sqliteStatement]; + + if (enableCaching) { + [self _cacheStatement:statement forQuery:sql]; + } + } else { + [statement reset]; + } + + // Make parameter + int queryCount = sqlite3_bind_parameter_count([statement sqliteStatement]); + int argumentCount = (int)[args count]; + if (queryCount != argumentCount) { + if (!enableCaching) { + [statement close]; + } + + NSError *error = [self _errorWithErrorCode:PFSQLiteDatabaseInvalidArgumenCountErrorCode + errorMessage:@"Statement arguments count doesn't match " + @"given arguments count." + domain:NSStringFromClass([self class])]; + return [BFTask taskWithError:error]; + } + + for (int idx = 0; idx < queryCount; ++idx) { + [self _bindObject:args[idx] toColumn:(idx + 1) inStatement:statement]; + } + + PFSQLiteDatabaseResult *result = [[PFSQLiteDatabaseResult alloc] initWithStatement:statement]; + return [BFTask taskWithResult:result]; +} + +- (BFTask *)executeCachedQueryAsync:(NSString *)sql withArgumentsInArray:(NSArray *)args { + return [BFTask taskFromExecutor:_databaseExecutor withBlock:^id { + return [self _executeQueryAsync:sql withArgumentsInArray:args cachingEnabled:YES]; + }]; +} + +- (BFTask *)executeQueryAsync:(NSString *)sql withArgumentsInArray:(NSArray *)args { + return [BFTask taskFromExecutor:_databaseExecutor withBlock:^id { + return [self _executeQueryAsync:sql withArgumentsInArray:args cachingEnabled:NO]; + }]; +} + +- (BFTask *)executeSQLAsync:(NSString *)sql withArgumentsInArray:(NSArray *)args { + return [BFTask taskFromExecutor:_databaseExecutor withBlock:^id { + return [[self _executeQueryAsync:sql + withArgumentsInArray:args + cachingEnabled:NO] continueWithExecutor:[BFExecutor immediateExecutor] withSuccessBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *databaseResult = task.result; + int sqliteResultCode = [databaseResult step]; + [databaseResult close]; + + switch (sqliteResultCode) { + case SQLITE_DONE: { + return [BFTask taskWithResult:nil]; + } + case SQLITE_ROW: { + NSError *error = [self _errorWithErrorCode:PFSQLiteDatabaseInvalidSQL + errorMessage:@"Cannot SELECT on executeSQLAsync." + @"Please use executeQueryAsync." + domain:NSStringFromClass([self class])]; + return [BFTask taskWithError:error]; + } + default: { + return [BFTask taskWithError:[self _errorWithErrorCode:sqliteResultCode]]; + } + } + }]; + }]; +} + +/*! + bindObject will bind any object supported by PFSQLiteDatabase to query statement. + Note: sqlite3 query index binding is one-based, while querying result is zero-based. + */ +- (void)_bindObject:(id)obj toColumn:(int)idx inStatement:(PFSQLiteStatement *)statement { + if ((!obj) || ((NSNull *)obj == [NSNull null])) { + sqlite3_bind_null([statement sqliteStatement], idx); + } else if ([obj isKindOfClass:[NSData class]]) { + const void *bytes = [obj bytes]; + if (!bytes) { + // It's an empty NSData object, aka [NSData data]. + // Don't pass a NULL pointer, or sqlite will bind a SQL null instead of a blob. + bytes = ""; + } + sqlite3_bind_blob([statement sqliteStatement], idx, bytes, (int)[obj length], SQLITE_TRANSIENT); + } else if ([obj isKindOfClass:[NSDate class]]) { + sqlite3_bind_double([statement sqliteStatement], idx, [obj timeIntervalSince1970]); + } else if ([obj isKindOfClass:[NSNumber class]]) { + if (CFNumberIsFloatType((__bridge CFNumberRef)obj)) { + sqlite3_bind_double([statement sqliteStatement], idx, [obj doubleValue]); + } else { + sqlite3_bind_int64([statement sqliteStatement], idx, [obj longLongValue]); + } + } else { + sqlite3_bind_text([statement sqliteStatement], idx, [[obj description] UTF8String], -1, SQLITE_TRANSIENT); + } +} + +///-------------------------------------- +#pragma mark - Cached Statements +///-------------------------------------- + +- (void)_clearCachedStatements { + for (PFSQLiteStatement *statement in [_cachedStatements allValues]) { + [statement close]; + } + + [_cachedStatements removeAllObjects]; +} + +- (PFSQLiteStatement *)_cachedStatementForQuery:(NSString *)query { + return _cachedStatements[query]; +} + +- (void)_cacheStatement:(PFSQLiteStatement *)statement forQuery:(NSString *)query { + _cachedStatements[query] = statement; +} + +///-------------------------------------- +#pragma mark - Errors +///-------------------------------------- + +/*! + Generates SQLite error. The details of the error code can be seen in: www.sqlite.org/c3ref/errcode.html + */ +- (NSError *)_errorWithErrorCode:(int)errorCode { + return [self _errorWithErrorCode:errorCode + errorMessage:[NSString stringWithUTF8String:sqlite3_errmsg(self.database)]]; +} + +- (NSError *)_errorWithErrorCode:(int)errorCode errorMessage:(NSString *)errorMessage { + return [self _errorWithErrorCode:errorCode + errorMessage:errorMessage + domain:PFSQLiteDatabaseErrorSQLiteDomain]; +} + +/*! + Generates SQLite/PFSQLiteDatabase error. + */ +- (NSError *)_errorWithErrorCode:(int)errorCode + errorMessage:(NSString *)errorMessage + domain:(NSString *)domain { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + result[@"code"] = @(errorCode); + result[@"error"] = errorMessage; + return [[NSError alloc] initWithDomain:domain code:errorCode userInfo:result]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (BFTask *)databaseClosedTask { + return _databaseClosedTaskCompletionSource.task; +} + +@end diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.h b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.h new file mode 100644 index 000000000..7779ce4d1 --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFFileManager; +@class PFSQLiteDatabase; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFSQLiteDatabaseController : NSObject + +@property (nonatomic, strong, readonly) PFFileManager *fileManager; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithFileManager:(PFFileManager *)fileManager NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithFileManager:(PFFileManager *)fileManager; + +///-------------------------------------- +/// @name Opening +///-------------------------------------- + +/*! + @abstract Asynchronously opens a database connection to the database with the name specified. + @note Only one database can be actively open at a time. + + @param name The name of the database to open. + + @return A task, which yields a `PFSQLiteDatabase`, with the open database. + When the database is closed, a new database connection can be opened. + */ +- (BFTask *)openDatabaseWithNameAsync:(NSString *)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.m b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.m new file mode 100644 index 000000000..d971ea40a --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseController.m @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSQLiteDatabaseController.h" + +#import +#import + +#import "PFAssert.h" +#import "PFAsyncTaskQueue.h" +#import "PFFileManager.h" +#import "PFSQLiteDatabase_Private.h" + +@implementation PFSQLiteDatabaseController { + PFAsyncTaskQueue *_openDatabaseQueue; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithFileManager:(PFFileManager *)fileManager { + self = [super init]; + if (!self) return nil; + + _fileManager = fileManager; + _openDatabaseQueue = [[PFAsyncTaskQueue alloc] init]; + + return self; +} + ++ (instancetype)controllerWithFileManager:(PFFileManager *)fileManager { + return [[self alloc] initWithFileManager:fileManager]; +} + +///-------------------------------------- +#pragma mark - Opening +///-------------------------------------- + +// TODO: (richardross) Implement connection pooling using NSCache or similar mechanism. +- (BFTask *)openDatabaseWithNameAsync:(NSString *)name { + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + [_openDatabaseQueue enqueue:^id(BFTask *task) { + NSString *databasePath = [self.fileManager parseDataItemPathForPathComponent:name]; + PFSQLiteDatabase *sqliteDatabase = [PFSQLiteDatabase databaseWithPath:databasePath]; + [[sqliteDatabase openAsync] continueWithBlock:^id(BFTask *task) { + if (task.faulted) { + NSError *error = task.error; + if (error) { + [taskCompletionSource trySetError:error]; + } else { + [taskCompletionSource trySetException:task.exception]; + } + } else if (task.cancelled) { + [taskCompletionSource trySetCancelled]; + } else { + [taskCompletionSource trySetResult:sqliteDatabase]; + } + + return nil; + }]; + + return sqliteDatabase.databaseClosedTask; + }]; + + return taskCompletionSource.task; +} + +@end diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.h b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.h new file mode 100644 index 000000000..d63d36f62 --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.h @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFSQLiteStatement; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFSQLiteDatabaseResult : NSObject + +- (instancetype)initWithStatement:(PFSQLiteStatement *)statement; + +/*! + Move current result to next row. Returns true if next result exists. False if current result + is the end of result set. + */ +- (BOOL)next; + +/*! + Move the current result to next row, and returns the raw SQLite return code for the cursor. + Useful for detecting end of cursor vs. error. + */ +- (int)step; + +/*! + Closes the database result. + */ +- (BOOL)close; + +///-------------------------------------- +/// @name Get Column Value +///-------------------------------------- + +- (int)intForColumn:(NSString *)columnName; +- (int)intForColumnIndex:(int)columnIndex; + +- (long)longForColumn:(NSString *)columnName; +- (long)longForColumnIndex:(int)columnIndex; + +- (BOOL)boolForColumn:(NSString *)columnName; +- (BOOL)boolForColumnIndex:(int)columnIndex; + +- (double)doubleForColumn:(NSString *)columnName; +- (double)doubleForColumnIndex:(int)columnIndex; + +- (nullable NSString *)stringForColumn:(NSString *)columnName; +- (nullable NSString *)stringForColumnIndex:(int)columnIndex; + +- (nullable NSDate *)dateForColumn:(NSString *)columnName; +- (nullable NSDate *)dateForColumnIndex:(int)columnIndex; + +- (nullable NSData *)dataForColumn:(NSString *)columnName; +- (nullable NSData *)dataForColumnIndex:(int)columnIndex; + +- (nullable id)objectForColumn:(NSString *)columnName; +- (nullable id)objectForColumnIndex:(int)columnIndex; + +- (BOOL)columnIsNull:(NSString *)columnName; +- (BOOL)columnIndexIsNull:(int)columnIndex; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.m b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.m new file mode 100644 index 000000000..cdbeb0e89 --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabaseResult.m @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSQLiteDatabaseResult.h" + +#import + +#import "PFSQLiteStatement.h" + +@interface PFSQLiteDatabaseResult () + +@property (nonatomic, copy, readonly) NSDictionary *columnNameToIndexMap; +@property (nonatomic, strong, readonly) PFSQLiteStatement *statement; + +@end + +@implementation PFSQLiteDatabaseResult + +@synthesize columnNameToIndexMap = _columnNameToIndexMap; + +- (instancetype)initWithStatement:(PFSQLiteStatement *)stmt { + if ((self = [super init])) { + _statement = stmt; + } + return self; +} + +- (BOOL)next { + return [self step] == SQLITE_ROW; +} + +- (int)step { + return sqlite3_step([self.statement sqliteStatement]); +} + +- (BOOL)close { + return [self.statement close]; +} + +- (int)intForColumn:(NSString *)columnName { + return [self intForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (int)intForColumnIndex:(int)columnIndex { + return sqlite3_column_int([self.statement sqliteStatement], columnIndex); +} + +- (long)longForColumn:(NSString *)columnName { + return [self longForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (long)longForColumnIndex:(int)columnIndex { + return (long)sqlite3_column_int64([self.statement sqliteStatement], columnIndex); +} + +- (BOOL)boolForColumn:(NSString *)columnName { + return [self boolForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (BOOL)boolForColumnIndex:(int)columnIndex { + return ([self intForColumnIndex:columnIndex] != 0); +} + +- (double)doubleForColumn:(NSString *)columnName { + return [self doubleForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (double)doubleForColumnIndex:(int)columnIndex { + return sqlite3_column_double([self.statement sqliteStatement], columnIndex); +} + +- (NSString *)stringForColumn:(NSString *)columnName { + return [self stringForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSString *)stringForColumnIndex:(int)columnIndex { + if ([self columnIndexIsNull:columnIndex]) { + return nil; + } + + const char *str = (const char *)sqlite3_column_text([self.statement sqliteStatement], columnIndex); + if (!str) { + return nil; + } + return [NSString stringWithUTF8String:str]; +} + +- (NSDate *)dateForColumn:(NSString *)columnName { + return [self dateForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSDate *)dateForColumnIndex:(int)columnIndex { + // TODO: (nlutsenko) probably use formatter + return [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIndex]]; +} + +- (NSData *)dataForColumn:(NSString *)columnName { + return [self dataForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSData *)dataForColumnIndex:(int)columnIndex { + if ([self columnIndexIsNull:columnIndex]) { + return nil; + } + + int size = sqlite3_column_bytes([self.statement sqliteStatement], columnIndex); + const char *buffer = sqlite3_column_blob([self.statement sqliteStatement], columnIndex); + if (buffer == nil) { + return nil; + } + return [NSData dataWithBytes:buffer length:size]; +} + +- (id)objectForColumn:(NSString *)columnName { + return [self objectForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (id)objectForColumnIndex:(int)columnIndex { + int columnType = sqlite3_column_type([self.statement sqliteStatement], columnIndex); + switch (columnType) { + case SQLITE_INTEGER: + return @([self longForColumnIndex:columnIndex]); + case SQLITE_FLOAT: + return @([self doubleForColumnIndex:columnIndex]); + case SQLITE_BLOB: + return [self dataForColumnIndex:columnIndex]; + default: + return [self stringForColumnIndex:columnIndex]; + } +} + +- (BOOL)columnIsNull:(NSString *)columnName { + return [self columnIndexIsNull:[self columnIndexForName:columnName]]; +} + +- (BOOL)columnIndexIsNull:(int)columnIndex { + return (sqlite3_column_type([self.statement sqliteStatement], columnIndex) == SQLITE_NULL); +} + +- (int)columnIndexForName:(NSString *)columnName { + NSNumber *index = self.columnNameToIndexMap[[columnName lowercaseString]]; + if (index) { + return [index intValue]; + } + // not found + return -1; +} + +- (NSDictionary *)columnNameToIndexMap { + if (!_columnNameToIndexMap) { + int columnCount = sqlite3_column_count([self.statement sqliteStatement]); + NSMutableDictionary *mutableColumnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:columnCount]; + for (int i = 0; i < columnCount; ++i) { + NSString *key = [NSString stringWithUTF8String:sqlite3_column_name([self.statement sqliteStatement], i)]; + mutableColumnNameToIndexMap[[key lowercaseString]] = @(i); + } + _columnNameToIndexMap = mutableColumnNameToIndexMap; + } + return _columnNameToIndexMap; +} + +@end diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase_Private.h b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase_Private.h new file mode 100644 index 000000000..5db03668f --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteDatabase_Private.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSQLiteDatabase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFSQLiteDatabase () + +@property (nonatomic, strong, readonly) BFTask *databaseClosedTask; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.h b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.h new file mode 100644 index 000000000..04e181c43 --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/*! + PFSQLiteStatement is sqlite3_stmt wrapper class. + */ +typedef struct sqlite3_stmt sqlite3_stmt; + +@interface PFSQLiteStatement : NSObject + +@property (atomic, assign, readonly) sqlite3_stmt *sqliteStatement; + +- (instancetype)initWithStatement:(sqlite3_stmt *)stmt; + +- (BOOL)close; +- (BOOL)reset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.m b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.m new file mode 100644 index 000000000..03f31b6ff --- /dev/null +++ b/Parse/Internal/LocalDataStore/SQLite/PFSQLiteStatement.m @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSQLiteStatement.h" + +#import + +@implementation PFSQLiteStatement + +- (instancetype)initWithStatement:(sqlite3_stmt *)stmt { + self = [super init]; + if (!stmt || !self) return nil; + + _sqliteStatement = stmt; + + return self; +} + +- (void)dealloc { + [self close]; +} + +- (BOOL)close { + if (!_sqliteStatement) { + return YES; + } + + int resultCode = sqlite3_finalize(_sqliteStatement); + _sqliteStatement = nil; + + return (resultCode == SQLITE_OK || resultCode == SQLITE_DONE); +} + +- (BOOL)reset { + if (!_sqliteStatement) { + return YES; + } + + int resultCode = sqlite3_reset(_sqliteStatement); + return (resultCode == SQLITE_OK || resultCode == SQLITE_DONE); +} + +@end diff --git a/Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.h b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.h new file mode 100644 index 000000000..da1ad894c --- /dev/null +++ b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +//TODO: (nlutsenko) Add unit tests for this class. +@interface PFMultiProcessFileLock : NSObject + +@property (nonatomic, copy, readonly) NSString *filePath; +@property (nonatomic, copy, readonly) NSString *lockFilePath; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initForFileWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER; ++ (instancetype)lockForFileWithPath:(NSString *)path; + +@end diff --git a/Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.m b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.m new file mode 100644 index 000000000..2c02ceaf3 --- /dev/null +++ b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLock.m @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMultiProcessFileLock.h" + +#import "PFAssert.h" +#import "PFMacros.h" + +static const NSTimeInterval PFMultiProcessLockAttemptsDelay = 0.001; + +@interface PFMultiProcessFileLock () { + dispatch_queue_t _synchronizationQueue; + int _fileDescriptor; +} + +@property (nonatomic, copy, readwrite) NSString *filePath; +@property (nonatomic, copy, readwrite) NSString *lockFilePath; + +@end + +@implementation PFMultiProcessFileLock + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initForFileWithPath:(NSString *)path { + self = [super init]; + if (!self) return nil; + + _filePath = [path copy]; + _lockFilePath = [path stringByAppendingPathExtension:@"pflock"]; + + NSString *queueName = [NSString stringWithFormat:@"com.parse.multiprocess.%@", + [[path lastPathComponent] stringByDeletingPathExtension]]; + _synchronizationQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL); + + return self; +} + ++ (instancetype)lockForFileWithPath:(NSString *)path { + return [[self alloc] initForFileWithPath:path]; +} + +- (void)dealloc { + [self unlock]; +} + +///-------------------------------------- +#pragma mark - NSLocking +///-------------------------------------- + +- (void)lock { + dispatch_sync(_synchronizationQueue, ^{ + // Greater than zero means that the lock was already succesfully acquired. + if (_fileDescriptor > 0) { + return; + } + + BOOL locked = NO; + while (!locked) @autoreleasepool { + locked = [self _tryLock]; + if (!locked) { + [NSThread sleepForTimeInterval:PFMultiProcessLockAttemptsDelay]; + } + } + }); +} + +- (void)unlock { + dispatch_sync(_synchronizationQueue, ^{ + // Only descriptor that is greater than zero is going to be open. + if (_fileDescriptor <= 0) { + return; + } + + close(_fileDescriptor); + _fileDescriptor = 0; + }); +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (BOOL)_tryLock { + const char *filePath = [self.lockFilePath fileSystemRepresentation]; + + // Atomically create a lock file if it doesn't exist and acquire the lock. + _fileDescriptor = open(filePath, (O_RDWR | O_CREAT | O_EXLOCK), + ((S_IRUSR | S_IWUSR | S_IXUSR) | (S_IRGRP | S_IWGRP | S_IXGRP) | (S_IROTH | S_IWOTH | S_IXOTH))); + return (_fileDescriptor > 0); +} + +@end diff --git a/Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.h b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.h new file mode 100644 index 000000000..098b01a24 --- /dev/null +++ b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +//TODO: (nlutsenko) Add unit tests for this class. +@interface PFMultiProcessFileLockController : NSObject + +//TODO: (nlutsenko) Re-consider using singleton here. ++ (instancetype)sharedController; + +/*! + Increments the content access counter by 1. + If the count was 0 - this will try to acquire the file lock first. + + @param filePath Path to a file to lock access to. + */ +- (void)beginLockedContentAccessForFileAtPath:(NSString *)filePath; + +/*! + Decrements the content access counter by 1. + If the count reaches 0 - the lock is going to be released. + + @param filePath Path to a file to lock access to. + */ +- (void)endLockedContentAccessForFileAtPath:(NSString *)filePath; + +- (NSUInteger)lockedContentAccessCountForFileAtPath:(NSString *)filePath; + +@end diff --git a/Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.m b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.m new file mode 100644 index 000000000..be10da982 --- /dev/null +++ b/Parse/Internal/MultiProcessLock/PFMultiProcessFileLockController.m @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMultiProcessFileLockController.h" + +#import "PFMultiProcessFileLock.h" + +@interface PFMultiProcessFileLockController () { + dispatch_queue_t _synchronizationQueue; + NSMutableDictionary *_locksDictionary; + NSMutableDictionary *_contentAccessDictionary; +} + +@end + +@implementation PFMultiProcessFileLockController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _synchronizationQueue = dispatch_queue_create("com.parse.multiprocesslock.controller", DISPATCH_QUEUE_CONCURRENT); + + _locksDictionary = [NSMutableDictionary dictionary]; + _contentAccessDictionary = [NSMutableDictionary dictionary]; + + return self; +} + ++ (instancetype)sharedController { + static PFMultiProcessFileLockController *controller; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + controller = [[self alloc] init]; + }); + return controller; +} + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + +- (void)beginLockedContentAccessForFileAtPath:(NSString *)filePath { + dispatch_barrier_sync(_synchronizationQueue, ^{ + PFMultiProcessFileLock *fileLock = _locksDictionary[filePath]; + if (!fileLock) { + fileLock = [PFMultiProcessFileLock lockForFileWithPath:filePath]; + _locksDictionary[filePath] = fileLock; + } + + [fileLock lock]; + + NSUInteger contentAccess = [_contentAccessDictionary[filePath] unsignedIntegerValue]; + _contentAccessDictionary[filePath] = @(contentAccess + 1); + }); +} + +- (void)endLockedContentAccessForFileAtPath:(NSString *)filePath { + dispatch_barrier_sync(_synchronizationQueue, ^{ + PFMultiProcessFileLock *fileLock = _locksDictionary[filePath]; + [fileLock unlock]; + + if (fileLock && [_contentAccessDictionary[filePath] unsignedIntegerValue] == 0) { + [_locksDictionary removeObjectForKey:filePath]; + [_contentAccessDictionary removeObjectForKey:filePath]; + } + }); +} + +- (NSUInteger)lockedContentAccessCountForFileAtPath:(NSString *)filePath { + __block NSUInteger value = 0; + dispatch_sync(_synchronizationQueue, ^{ + value = [_contentAccessDictionary[filePath] unsignedIntegerValue]; + }); + return value; +} + +@end diff --git a/Parse/Internal/Object/BatchController/PFObjectBatchController.h b/Parse/Internal/Object/BatchController/PFObjectBatchController.h new file mode 100644 index 000000000..c17bf7aa9 --- /dev/null +++ b/Parse/Internal/Object/BatchController/PFObjectBatchController.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" + +@class BFTask; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFObjectBatchController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Fetch +///-------------------------------------- + +- (BFTask *)fetchObjectsAsync:(nullable NSArray *)objects withSessionToken:(nullable NSString *)sessionToken; + +///-------------------------------------- +/// @name Utilities +///-------------------------------------- + ++ (nullable NSArray *)uniqueObjectsArrayFromArray:(nullable NSArray *)objects omitObjectsWithData:(BOOL)omitFetched; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/BatchController/PFObjectBatchController.m b/Parse/Internal/Object/BatchController/PFObjectBatchController.m new file mode 100644 index 000000000..c2b46dc89 --- /dev/null +++ b/Parse/Internal/Object/BatchController/PFObjectBatchController.m @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectBatchController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFErrorUtilities.h" +#import "PFMacros.h" +#import "PFObjectController.h" +#import "PFObjectPrivate.h" +#import "PFQueryPrivate.h" +#import "PFRESTQueryCommand.h" + +@implementation PFObjectBatchController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Fetch +///-------------------------------------- + +- (BFTask *)fetchObjectsAsync:(NSArray *)objects withSessionToken:(NSString *)sessionToken { + if (objects.count == 0) { + return [BFTask taskWithResult:objects]; + } + + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [self _fetchCommandForObjects:objects withSessionToken:sessionToken]; + return [self.dataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFCommandResult *result = task.result; + return [self _processFetchResultAsync:result.result forObjects:objects]; + }]; +} + +- (PFRESTCommand *)_fetchCommandForObjects:(NSArray *)objects withSessionToken:(NSString *)sessionToken { + NSArray *objectIds = [objects valueForKey:@keypath(PFObject, objectId)]; + PFQuery *query = [PFQuery queryWithClassName:[objects.firstObject parseClassName]]; + [query whereKey:@keypath(PFObject, objectId) containedIn:objectIds]; + query.limit = objectIds.count; + return [PFRESTQueryCommand findCommandForQueryState:query.state withSessionToken:sessionToken]; +} + +- (BFTask *)_processFetchResultAsync:(NSDictionary *)result forObjects:(NSArray *)objects { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSArray *results = result[@"results"]; // TODO: (nlutsenko) Move this logic into command itself? + NSArray *objectIds = [results valueForKey:@keypath(PFObject, objectId)]; + NSDictionary *objectResults = [NSDictionary dictionaryWithObjects:results forKeys:objectIds]; + + NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:objects.count]; + for (PFObject *object in objects) { + PFObjectController *controller = [[object class] objectController]; + NSDictionary *objectResult = objectResults[object.objectId]; + + BFTask *task = nil; + if (objectResult) { + task = [controller processFetchResultAsync:objectResult forObject:object]; + } else { + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorObjectNotFound + message:@"Object not found on the server."]; + task = [BFTask taskWithError:error]; + } + [tasks addObject:task]; + } + return [BFTask taskForCompletionOfAllTasks:tasks]; + }]; +} + +///-------------------------------------- +#pragma mark - Utilities +///-------------------------------------- + ++ (NSArray *)uniqueObjectsArrayFromArray:(NSArray *)objects omitObjectsWithData:(BOOL)omitFetched { + if (objects.count == 0) { + return objects; + } + + NSMutableSet *set = [NSMutableSet setWithCapacity:[objects count]]; + NSString *className = [objects.firstObject parseClassName]; + for (PFObject *object in objects) { + @synchronized (object.lock) { + if (omitFetched && [object isDataAvailable]) { + continue; + } + + PFParameterAssert([className isEqualToString:object.parseClassName], + @"All object should be in the same class."); + PFParameterAssert(object.objectId != nil, + @"All objects must exist on the server."); + + [set addObject:object]; + } + } + return [set allObjects]; +} + +@end diff --git a/Parse/Internal/Object/Coder/File/PFObjectFileCoder.h b/Parse/Internal/Object/Coder/File/PFObjectFileCoder.h new file mode 100644 index 000000000..371acc131 --- /dev/null +++ b/Parse/Internal/Object/Coder/File/PFObjectFileCoder.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFDecoder; +@class PFEncoder; +@class PFObject; + +NS_ASSUME_NONNULL_BEGIN + +/*! + Handles encoding/decoding of `PFObject`s into a /2 JSON format. + /2 format is only used for persisting `currentUser`, `currentInstallation` to disk when LDS is not enabled. + */ +@interface PFObjectFileCoder : NSObject + +///-------------------------------------- +/// @name Encode +///-------------------------------------- + ++ (NSData *)dataFromObject:(PFObject *)object usingEncoder:(PFEncoder *)encoder; + +///-------------------------------------- +/// @name Decode +///-------------------------------------- + ++ (PFObject *)objectFromData:(NSData *)data usingDecoder:(PFDecoder *)decoder; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/Coder/File/PFObjectFileCoder.m b/Parse/Internal/Object/Coder/File/PFObjectFileCoder.m new file mode 100644 index 000000000..75bc1975e --- /dev/null +++ b/Parse/Internal/Object/Coder/File/PFObjectFileCoder.m @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectFileCoder.h" + +#import "PFJSONSerialization.h" +#import "PFObjectFileCodingLogic.h" +#import "PFObjectPrivate.h" +#import "PFObjectState.h" + +@implementation PFObjectFileCoder + +///-------------------------------------- +#pragma mark - Encode +///-------------------------------------- + ++ (NSData *)dataFromObject:(PFObject *)object usingEncoder:(PFEncoder *)encoder { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + result[@"classname"] = object._state.parseClassName; + result[@"data"] = [object._state dictionaryRepresentationWithObjectEncoder:encoder]; + return [PFJSONSerialization dataFromJSONObject:result]; +} + +///-------------------------------------- +#pragma mark - Decode +///-------------------------------------- + ++ (PFObject *)objectFromData:(NSData *)data usingDecoder:(PFDecoder *)decoder { + NSDictionary *dictionary = [PFJSONSerialization JSONObjectFromData:data]; + NSString *className = dictionary[@"classname"] ?: dictionary[@"className"]; + NSString *objectId = dictionary[@"data"][@"objectId"] ?: dictionary[@"id"]; + + PFObject *object = [PFObject objectWithoutDataWithClassName:className objectId:objectId]; + [[[object class] objectFileCodingLogic] updateObject:object fromDictionary:dictionary usingDecoder:decoder]; + return object; +} + +@end diff --git a/Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.h b/Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.h new file mode 100644 index 000000000..c9ce66ce9 --- /dev/null +++ b/Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFDecoder; +@class PFObject; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFObjectFileCodingLogic : NSObject + +///-------------------------------------- +/// @name Init +///-------------------------------------- + ++ (instancetype)codingLogic; + +///-------------------------------------- +/// @name Logic +///-------------------------------------- + +- (void)updateObject:(PFObject *)object fromDictionary:(NSDictionary *)dictionary usingDecoder:(PFDecoder *)decoder; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.m b/Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.m new file mode 100644 index 000000000..f5955f159 --- /dev/null +++ b/Parse/Internal/Object/Coder/File/PFObjectFileCodingLogic.m @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectFileCodingLogic.h" + +#import "PFMutableObjectState.h" +#import "PFObjectPrivate.h" + +@implementation PFObjectFileCodingLogic + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)codingLogic { + return [[self alloc] init]; +} + +///-------------------------------------- +#pragma mark - Logic +///-------------------------------------- + +- (void)updateObject:(PFObject *)object fromDictionary:(NSDictionary *)dictionary usingDecoder:(PFDecoder *)decoder { + PFMutableObjectState *state = [object._state mutableCopy]; + NSString *newObjectId = dictionary[@"id"]; + if (newObjectId) { + state.objectId = newObjectId; + } + NSString *createdAtString = dictionary[@"created_at"]; + if (createdAtString) { + [state setCreatedAtFromString:createdAtString]; + } + NSString *updatedAtString = dictionary[@"updated_at"]; + if (updatedAtString) { + [state setUpdatedAtFromString:updatedAtString]; + } + object._state = state; + + NSDictionary *newPointers = dictionary[@"pointers"]; + NSMutableDictionary *pointersDictionary = [NSMutableDictionary dictionaryWithCapacity:newPointers.count]; + [newPointers enumerateKeysAndObjectsUsingBlock:^(id key, NSArray *pointerArray, BOOL *stop) { + PFObject *pointer = [PFObject objectWithoutDataWithClassName:[pointerArray firstObject] + objectId:[pointerArray lastObject]]; + pointersDictionary[key] = pointer; + }]; + + NSMutableDictionary *dataDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionary[@"data"]]; + [dataDictionary addEntriesFromDictionary:pointersDictionary]; + [object _mergeAfterFetchWithResult:dataDictionary decoder:decoder completeData:YES]; +} + +@end diff --git a/Parse/Internal/Object/Constants/PFObjectConstants.h b/Parse/Internal/Object/Constants/PFObjectConstants.h new file mode 100644 index 000000000..524078c65 --- /dev/null +++ b/Parse/Internal/Object/Constants/PFObjectConstants.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +// REST Key magic strings +extern NSString *const PFObjectCompleteRESTKey; +extern NSString *const PFObjectOperationsRESTKey; +extern NSString *const PFObjectTypeRESTKey; +extern NSString *const PFObjectObjectIdRESTKey; +extern NSString *const PFObjectUpdatedAtRESTKey; +extern NSString *const PFObjectCreatedAtRESTKey; +extern NSString *const PFObjectIsDeletingEventuallyRESTKey; +extern NSString *const PFObjectClassNameRESTKey; +extern NSString *const PFObjectACLRESTKey; + +extern NSString *const PFObjectDefaultPin; diff --git a/Parse/Internal/Object/Constants/PFObjectConstants.m b/Parse/Internal/Object/Constants/PFObjectConstants.m new file mode 100644 index 000000000..76047ecf3 --- /dev/null +++ b/Parse/Internal/Object/Constants/PFObjectConstants.m @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectConstants.h" + +NSString *const PFObjectCompleteRESTKey = @"__complete"; +NSString *const PFObjectOperationsRESTKey = @"__operations"; +NSString *const PFObjectTypeRESTKey = @"__type"; +NSString *const PFObjectObjectIdRESTKey = @"objectId"; +NSString *const PFObjectUpdatedAtRESTKey = @"updatedAt"; +NSString *const PFObjectCreatedAtRESTKey = @"createdAt"; +NSString *const PFObjectIsDeletingEventuallyRESTKey = @"isDeletingEventually"; +NSString *const PFObjectClassNameRESTKey = @"className"; +NSString *const PFObjectACLRESTKey = @"ACL"; + +NSString *const PFObjectDefaultPin = @"_default"; diff --git a/Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.h b/Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.h new file mode 100644 index 000000000..27dd6dd9f --- /dev/null +++ b/Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFOfflineObjectController : PFObjectController + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithDataSource:(id)dataSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.m b/Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.m new file mode 100644 index 000000000..e9cefc33d --- /dev/null +++ b/Parse/Internal/Object/Controller/OfflineController/PFOfflineObjectController.m @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFOfflineObjectController.h" + +#import "BFTask+Private.h" +#import "PFMacros.h" +#import "PFObjectController_Private.h" +#import "PFObjectPrivate.h" +#import "PFObjectState.h" +#import "PFOfflineStore.h" + +@interface PFOfflineObjectController () + +@property (nonatomic, strong, readonly) PFOfflineStore *offlineStore; + +@end + +@implementation PFOfflineObjectController + +@dynamic dataSource; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithDataSource:(id)dataSource { + return [super initWithDataSource:dataSource]; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [super controllerWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - PFObjectController +///-------------------------------------- + +- (BFTask *)processFetchResultAsync:(NSDictionary *)result forObject:(PFObject *)object { + return [[[[self.offlineStore fetchObjectLocallyAsync:object] continueWithBlock:^id(BFTask *task) { + // Catch CacheMiss error and ignore it. + if ([task.error.domain isEqualToString:PFParseErrorDomain] && + task.error.code == kPFErrorCacheMiss) { + return nil; + } + return task; + }] continueWithBlock:^id(BFTask *task) { + return [super processFetchResultAsync:result forObject:object]; + }] continueWithBlock:^id(BFTask *task) { + return [[self.offlineStore updateDataForObjectAsync:object] continueWithBlock:^id(BFTask *task) { + // Catch CACHE_MISS and ignore it. + if ([task.error.domain isEqualToString:PFParseErrorDomain] && + task.error.code == kPFErrorCacheMiss) { + return [BFTask taskWithResult:nil]; + } + return task; + }]; + }]; +} + +- (BFTask *)processDeleteResultAsync:(nullable NSDictionary *)result forObject:(PFObject *)object { + @weakify(self); + return [[super processDeleteResultAsync:result forObject:object] continueWithBlock:^id(BFTask *task) { + @strongify(self); + if (object._state.deleted) { + return [self.offlineStore deleteDataForObjectAsync:object]; + } + return [self.offlineStore updateDataForObjectAsync:object]; + }]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (PFOfflineStore *)offlineStore { + return self.dataSource.offlineStore; +} + +@end diff --git a/Parse/Internal/Object/Controller/PFObjectController.h b/Parse/Internal/Object/Controller/PFObjectController.h new file mode 100644 index 000000000..dc269a2ae --- /dev/null +++ b/Parse/Internal/Object/Controller/PFObjectController.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" +#import "PFObjectControlling.h" + +@class BFTask; +@class PFObject; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFObjectController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithDataSource:(id)dataSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/Controller/PFObjectController.m b/Parse/Internal/Object/Controller/PFObjectController.m new file mode 100644 index 000000000..0fd14fba8 --- /dev/null +++ b/Parse/Internal/Object/Controller/PFObjectController.m @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectController.h" +#import "PFObjectController_Private.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFErrorUtilities.h" +#import "PFMacros.h" +#import "PFObjectPrivate.h" +#import "PFObjectState.h" +#import "PFRESTObjectCommand.h" +#import "PFTaskQueue.h" + +@implementation PFObjectController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - PFObjectControlling +///-------------------------------------- + +#pragma mark Fetch + +- (BFTask *)fetchObjectAsync:(PFObject *)object withSessionToken:(NSString *)sessionToken { + @weakify(self); + return [[[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFObjectState *state = [object._state copy]; + if (!state.objectId) { + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorMissingObjectId + message:@"Can't fetch an object that hasn't been saved to the server."]; + return [BFTask taskWithError:error]; + } + PFRESTCommand *command = [PFRESTObjectCommand fetchObjectCommandForObjectState:state + withSessionToken:sessionToken]; + return [self _runFetchCommand:command forObject:object]; + }] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFCommandResult *result = task.result; + return [self processFetchResultAsync:result.result forObject:object]; + }] continueWithSuccessResult:object]; +} + +- (BFTask *)_runFetchCommand:(PFRESTCommand *)command forObject:(PFObject *)object { + return [self.dataSource.commandRunner runCommandAsync:command withOptions:PFCommandRunningOptionRetryIfFailed]; +} + +- (BFTask *)processFetchResultAsync:(NSDictionary *)result forObject:(PFObject *)object { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSDictionary *fetchedObjects = [object _collectFetchedObjects]; + @synchronized (object.lock) { + PFKnownParseObjectDecoder *decoder = [PFKnownParseObjectDecoder decoderWithFetchedObjects:fetchedObjects]; + [object _mergeAfterFetchWithResult:result decoder:decoder completeData:YES]; + } + return nil; + }]; +} + +#pragma mark Delete + +- (BFTask *)deleteObjectAsync:(PFObject *)object withSessionToken:(nullable NSString *)sessionToken { + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFObjectState *state = [object._state copy]; + if (!state.objectId) { + return nil; + } + + PFRESTCommand *command = [PFRESTObjectCommand deleteObjectCommandForObjectState:state + withSessionToken:sessionToken]; + return [[self _runDeleteCommand:command forObject:object] continueWithBlock:^id(BFTask *fetchTask) { + @strongify(self); + PFCommandResult *result = fetchTask.result; + return [[self processDeleteResultAsync:result.result forObject:object] continueWithBlock:^id(BFTask *task) { + // Propagate the result of network task if it's faulted, cancelled. + if (fetchTask.faulted || fetchTask.cancelled) { + return fetchTask; + } + // Propagate the result of processDeleteResult otherwise. + return task; + }]; + }]; + }]; +} + +- (BFTask *)_runDeleteCommand:(PFRESTCommand *)command forObject:(PFObject *)object { + return [self.dataSource.commandRunner runCommandAsync:command withOptions:PFCommandRunningOptionRetryIfFailed]; +} + +- (BFTask *)processDeleteResultAsync:(NSDictionary *)result forObject:(PFObject *)object { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + BOOL deleted = (result != nil); + [object _setDeleted:deleted]; + return nil; + }]; +} + +@end diff --git a/Parse/Internal/Object/Controller/PFObjectController_Private.h b/Parse/Internal/Object/Controller/PFObjectController_Private.h new file mode 100644 index 000000000..7ca5abc67 --- /dev/null +++ b/Parse/Internal/Object/Controller/PFObjectController_Private.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectController.h" + +@class PFRESTCommand; + +@interface PFObjectController () + +///-------------------------------------- +/// @name Fetch +///-------------------------------------- + +- (BFTask *)_runFetchCommand:(PFRESTCommand *)command forObject:(PFObject *)object; + +@end diff --git a/Parse/Internal/Object/Controller/PFObjectControlling.h b/Parse/Internal/Object/Controller/PFObjectControlling.h new file mode 100644 index 000000000..f4f553501 --- /dev/null +++ b/Parse/Internal/Object/Controller/PFObjectControlling.h @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFObject; + +NS_ASSUME_NONNULL_BEGIN + +@protocol PFObjectControlling + +///-------------------------------------- +/// @name Fetch +///-------------------------------------- + +/*! + Fetches an object asynchronously. + + @param object Object to fetch. + @param sessionToken Session token to use. + + @returns `BFTask` with result set to `PFObject`. + */ +- (BFTask *)fetchObjectAsync:(PFObject *)object withSessionToken:(nullable NSString *)sessionToken; + +- (BFTask *)processFetchResultAsync:(NSDictionary *)result forObject:(PFObject *)object; + +///-------------------------------------- +/// @name Delete +///-------------------------------------- + +/*! + Deletes an object asynchronously. + + @param object Object to fetch. + @param sessionToken Session token to use. + + @returns `BFTask` with result set to `nil`. + */ +- (BFTask *)deleteObjectAsync:(PFObject *)object withSessionToken:(nullable NSString *)sessionToken; + +//TODO: (nlutsenko) This needs removal, figure out how to kill it. +- (BFTask *)processDeleteResultAsync:(nullable NSDictionary *)result forObject:(PFObject *)object; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/CurrentController/PFCurrentObjectControlling.h b/Parse/Internal/Object/CurrentController/PFCurrentObjectControlling.h new file mode 100644 index 000000000..3ff5e616c --- /dev/null +++ b/Parse/Internal/Object/CurrentController/PFCurrentObjectControlling.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class BFTask; +@class PFObject; + +typedef NS_ENUM(NSUInteger, PFCurrentObjectStorageType) { + PFCurrentObjectStorageTypeFile = 1, + PFCurrentObjectStorageTypeOfflineStore, +}; + +@protocol PFCurrentObjectControlling + +@property (nonatomic, assign, readonly) PFCurrentObjectStorageType storageType; + +///-------------------------------------- +/// @name Current +///-------------------------------------- + +- (BFTask *)getCurrentObjectAsync; +- (BFTask *)saveCurrentObjectAsync:(PFObject *)object; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.h b/Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.h new file mode 100644 index 000000000..b507e8a32 --- /dev/null +++ b/Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFFieldOperation; +@class PFOperationSet; + +@interface PFObjectEstimatedData : NSObject + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithServerData:(NSDictionary *)serverData + operationSetQueue:(NSArray *)operationSetQueue; ++ (instancetype)estimatedDataFromServerData:(NSDictionary *)serverData + operationSetQueue:(NSArray *)operationSetQueue; + +///-------------------------------------- +/// @name Read +///-------------------------------------- + +- (id)objectForKey:(NSString *)key; +- (id)objectForKeyedSubscript:(NSString *)keyedSubscript; + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(NSString *key, id obj, BOOL *stop))block; + +@property (nonatomic, copy, readonly) NSArray *allKeys; +@property (nonatomic, copy, readonly) NSDictionary *dictionaryRepresentation; + +///-------------------------------------- +/// @name Write +///-------------------------------------- + +- (id)applyFieldOperation:(PFFieldOperation *)operation forKey:(NSString *)key; + +@end diff --git a/Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.m b/Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.m new file mode 100644 index 000000000..7fb5df597 --- /dev/null +++ b/Parse/Internal/Object/EstimatedData/PFObjectEstimatedData.m @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectEstimatedData.h" + +#import "PFObjectUtilities.h" + +@interface PFObjectEstimatedData () { + NSMutableDictionary *_dataDictionary; +} + +@end + +@implementation PFObjectEstimatedData + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _dataDictionary = [NSMutableDictionary dictionary]; + + return self; +} + +- (instancetype)initWithServerData:(NSDictionary *)serverData + operationSetQueue:(NSArray *)operationSetQueue { + self = [super init]; + if (!self) return nil; + + // Don't use mutableCopy to make sure we never initialize _dataDictionary to `nil`. + _dataDictionary = [NSMutableDictionary dictionaryWithDictionary:serverData]; + for (PFOperationSet *operationSet in operationSetQueue) { + [PFObjectUtilities applyOperationSet:operationSet toDictionary:_dataDictionary]; + } + + return self; +} + ++ (instancetype)estimatedDataFromServerData:(NSDictionary *)serverData + operationSetQueue:(NSArray *)operationSetQueue { + return [[self alloc] initWithServerData:serverData operationSetQueue:operationSetQueue]; +} + +///-------------------------------------- +#pragma mark - Read +///-------------------------------------- + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(NSString *key, id obj, BOOL *stop))block { + [_dataDictionary enumerateKeysAndObjectsUsingBlock:block]; +} + +- (id)objectForKey:(NSString *)key { + return [_dataDictionary objectForKey:key]; +} + +- (id)objectForKeyedSubscript:(NSString *)keyedSubscript { + return [_dataDictionary objectForKeyedSubscript:keyedSubscript]; +} + +- (NSArray *)allKeys { + return [_dataDictionary allKeys]; +} + +- (NSDictionary *)dictionaryRepresentation { + return [_dataDictionary copy]; +} + +///-------------------------------------- +#pragma mark - Write +///-------------------------------------- + +- (id)applyFieldOperation:(PFFieldOperation *)operation forKey:(NSString *)key { + return [PFObjectUtilities newValueByApplyingFieldOperation:operation toDictionary:_dataDictionary forKey:key]; +} + +@end diff --git a/Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.h b/Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.h new file mode 100644 index 000000000..15c9ab242 --- /dev/null +++ b/Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" + +@class BFTask; +@class PFObject; + +@interface PFObjectFilePersistenceController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Objects +///-------------------------------------- + +/*! + Loads and creates a PFObject from file. + + @param key File name to use. + + @returns `BFTask` with `PFObject` or `nil` result. + */ +- (BFTask *)loadPersistentObjectAsyncForKey:(NSString *)key; + +/*! + Saves a given object to a file with name. + + @param object Object to save. + @param key File name to use. + + @returns `BFTask` with `nil` result. + */ +- (BFTask *)persistObjectAsync:(PFObject *)object forKey:(NSString *)key; + +@end diff --git a/Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.m b/Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.m new file mode 100644 index 000000000..38fdcd12f --- /dev/null +++ b/Parse/Internal/Object/FilePersistence/PFObjectFilePersistenceController.m @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectFilePersistenceController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFFileManager.h" +#import "PFJSONSerialization.h" +#import "PFMacros.h" +#import "PFMultiProcessFileLockController.h" +#import "PFObjectFileCoder.h" +#import "PFObjectPrivate.h" + +@implementation PFObjectFilePersistenceController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Objects +///-------------------------------------- + +- (BFTask *)loadPersistentObjectAsyncForKey:(NSString *)key { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + + NSString *path = [self.dataSource.fileManager parseDataItemPathForPathComponent:key]; + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:path]; + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:path]; + return nil; + } + + NSError *error = nil; + NSData *jsonData = [NSData dataWithContentsOfFile:path + options:NSDataReadingMappedIfSafe + error:&error]; + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:path]; + + if (error) { + return [BFTask taskWithError:error]; + } + return jsonData; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSData *jsonData = task.result; + if (jsonData) { + PFObject *object = [PFObjectFileCoder objectFromData:jsonData usingDecoder:[PFDecoder objectDecoder]]; + return object; + } + + return nil; + }]; +} + +- (BFTask *)persistObjectAsync:(PFObject *)object forKey:(NSString *)key { + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + + NSData *data = [PFObjectFileCoder dataFromObject:object usingEncoder:[PFPointerObjectEncoder objectEncoder]]; + + NSString *filePath = [self.dataSource.fileManager parseDataItemPathForPathComponent:key]; + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:filePath]; + + return [[PFFileManager writeDataAsync:data toFile:filePath] continueWithBlock:^id(BFTask *task) { + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:filePath]; + return nil; + }]; + }]; +} + +@end diff --git a/Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.h b/Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.h new file mode 100644 index 000000000..541056ba5 --- /dev/null +++ b/Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" + +/*! + A disk-based map of local ids to global Parse objectIds. Every entry in this + map has a retain count, and the entry will be removed from the map if the + retain count reaches 0. Every time a localId is written out to disk, its retain + count should be incremented. When the reference on disk is deleted, it should + be decremented. Some entries in this map may not have an object id yet. + This class is thread-safe. + */ +@interface PFObjectLocalIdStore : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)storeWithDataSource:(id)dataSource; + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (NSString *)createLocalId; +- (void)retainLocalIdOnDisk:(NSString *)localId; +- (void)releaseLocalIdOnDisk:(NSString *)localId; + +- (void)setObjectId:(NSString *)objectId forLocalId:(NSString *)localId; +- (NSString *)objectIdForLocalId:(NSString *)localId; + +// For testing only. +- (BOOL)clear; +- (void)clearInMemoryCache; + +@end diff --git a/Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.m b/Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.m new file mode 100644 index 000000000..e615e425e --- /dev/null +++ b/Parse/Internal/Object/LocalIdStore/PFObjectLocalIdStore.m @@ -0,0 +1,303 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectLocalIdStore.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFJSONSerialization.h" +#import "PFLogging.h" +#import "Parse_Private.h" + +static NSString *const _PFObjectLocalIdStoreDiskFolderPath = @"LocalId"; + +///-------------------------------------- +#pragma mark - PFObjectLocalIdStoreMapEntry +///-------------------------------------- + +/*! + * Internal class representing all the information we know about a local id. + */ +@interface PFObjectLocalIdStoreMapEntry : NSObject + +@property (nonatomic, strong) NSString *objectId; +@property (atomic, assign) int referenceCount; + +- (instancetype)init NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithFile:(NSString *)filePath; + +@end + +@implementation PFObjectLocalIdStoreMapEntry + +- (instancetype)init { + return [super init]; +} + +- (instancetype)initWithFile:(NSString *)filePath { + self = [self init]; + if (!self) return nil; + + NSData *jsonData = [NSData dataWithContentsOfFile:filePath]; + NSDictionary *dictionary = [PFJSONSerialization JSONObjectFromData:jsonData]; + + _objectId = [dictionary[@"objectId"] copy]; + _referenceCount = [dictionary[@"referenceCount"] intValue]; + + return self; +} + +- (void)writeToFile:(NSString *)filePath { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + dictionary[@"referenceCount"] = @(self.referenceCount); + if (self.objectId) { + dictionary[@"objectId"] = self.objectId; + } + + NSData *jsonData = [PFJSONSerialization dataFromJSONObject:dictionary]; + [[PFFileManager writeDataAsync:jsonData toFile:filePath] waitForResult:nil withMainThreadWarning:NO]; +} + +@end + +///-------------------------------------- +#pragma mark - PFObjectLocalIdStore +///-------------------------------------- + +@interface PFObjectLocalIdStore () { + NSString *_diskPath; + NSObject *_lock; + NSMutableDictionary *_inMemoryCache; +} + +@end + +@implementation PFObjectLocalIdStore + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +/*! + * Creates a new LocalIdManager with default options. + */ +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + _lock = [[NSObject alloc] init]; + _inMemoryCache = [NSMutableDictionary dictionary]; + + // Construct the path to the disk storage directory. + _diskPath = [[Parse _currentManager].fileManager parseDataItemPathForPathComponent:_PFObjectLocalIdStoreDiskFolderPath]; + + NSError *error = nil; + [[PFFileManager createDirectoryIfNeededAsyncAtPath:_diskPath] waitForResult:&error withMainThreadWarning:NO]; + if (error) { + PFLogError(PFLoggingTagCommon, @"Unable to create directories for local id storage with error: %@", error); + } + + return self; +} + ++ (instancetype)storeWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +/*! + * Returns Yes if localId has the right basic format for a local id. + */ ++ (BOOL)isLocalId:(NSString *)localId { + if ([localId length] != 22U) { + return NO; + } + if (![localId hasPrefix:@"local_"]) { + return NO; + } + for (int i = 6; i < [localId length]; ++i) { + if (!ishexnumber([localId characterAtIndex:i])) { + return NO; + } + } + return YES; +} + +/*! + * Grabs one entry in the local id map off the disk. + */ +- (PFObjectLocalIdStoreMapEntry *)getMapEntry:(NSString *)localId { + if (![[self class] isLocalId:localId]) { + [NSException raise:NSInternalInconsistencyException + format:@"Tried to get invalid local id: \"%@\".", localId]; + } + + PFObjectLocalIdStoreMapEntry *entry = nil; + + NSString *file = [_diskPath stringByAppendingPathComponent:localId]; + if (![[NSFileManager defaultManager] isReadableFileAtPath:file]) { + entry = [[PFObjectLocalIdStoreMapEntry alloc] init]; + } else { + entry = [[PFObjectLocalIdStoreMapEntry alloc] initWithFile:file]; + } + + // If there's an objectId in memory, make sure it matches the one in the + // file. This is in case the id was retained on disk *after* it was resolved. + if (!entry.objectId) { + NSString *objectId = [_inMemoryCache objectForKey:localId]; + if (objectId) { + entry.objectId = objectId; + if (entry.referenceCount > 0) { + [self putMapEntry:entry forLocalId:localId]; + } + } + } + + return entry; +} + +/*! + * Writes one entry to the local id map on disk. + */ +- (void)putMapEntry:(PFObjectLocalIdStoreMapEntry *)entry forLocalId:(NSString *)localId { + if (![[self class] isLocalId:localId]) { + [NSException raise:NSInternalInconsistencyException + format:@"Tried to get invalid local id: \"%@\".", localId]; + } + + NSString *file = [_diskPath stringByAppendingPathComponent:localId]; + [entry writeToFile:file]; +} + +/*! + * Removes an entry from the local id map on disk. + */ +- (void)removeMapEntry:(NSString *)localId { + if (![[self class] isLocalId:localId]) { + [NSException raise:NSInternalInconsistencyException + format:@"Tried to get invalid local id: \"%@\".", localId]; + } + + NSString *file = [_diskPath stringByAppendingPathComponent:localId]; + [[NSFileManager defaultManager] removeItemAtPath:file error:nil]; +} + +/*! + * Creates a new local id in the map. + */ +- (NSString *)createLocalId { + @synchronized (_lock) { + // Generate a new random string of upper and lower case letters. + + // Start by generating a number. It will be the localId as a base-52 number. + // It has to be a uint64_t because log256(52^10) ~= 7.13 bytes. + uint64_t localIdNumber = (((uint64_t)arc4random()) << 32) | ((uint64_t)arc4random()); + NSString *localId = [NSString stringWithFormat:@"local_%016llx", localIdNumber]; + + if (![[self class] isLocalId:localId]) { + [NSException raise:NSInternalInconsistencyException + format:@"Generated an invalid local id: \"%@\".", localId]; + } + + return localId; + } +} + +/*! + * Increments the retain count of a local id on disk. + */ +- (void)retainLocalIdOnDisk:(NSString *)localId { + @synchronized (_lock) { + PFObjectLocalIdStoreMapEntry *entry = [self getMapEntry:localId]; + entry.referenceCount++; + [self putMapEntry:entry forLocalId:localId]; + } +} + +/*! + * Decrements the retain count of a local id on disk. + * If the retain count hits zero, the id is forgotten forever. + */ +- (void)releaseLocalIdOnDisk:(NSString *)localId { + @synchronized (_lock) { + PFObjectLocalIdStoreMapEntry *entry = [self getMapEntry:localId]; + if (--entry.referenceCount > 0) { + [self putMapEntry:entry forLocalId:localId]; + } else { + [self removeMapEntry:localId]; + } + } +} + +/*! + * Sets the objectId associated with a given local id. + */ +- (void)setObjectId:(NSString *)objectId forLocalId:(NSString *)localId { + @synchronized (_lock) { + PFObjectLocalIdStoreMapEntry *entry = [self getMapEntry:localId]; + if (entry.referenceCount > 0) { + entry.objectId = objectId; + [self putMapEntry:entry forLocalId:localId]; + } + [_inMemoryCache setObject:objectId forKey:localId]; + } +} + +/*! + * Returns the objectId associated with a given local id. + * Returns nil if no objectId is yet known for the lcoal id. + */ +- (NSString *)objectIdForLocalId:(NSString *)localId { + @synchronized (_lock) { + NSString *objectId = [_inMemoryCache objectForKey:localId]; + if (objectId) { + return objectId; + } + + PFObjectLocalIdStoreMapEntry *entry = [self getMapEntry:localId]; + return entry.objectId; + } +} + +/*! + * Removes all local ids from the disk and memory caches. + */ +- (BOOL)clear { + @synchronized (_lock) { + [self clearInMemoryCache]; + + BOOL empty = ([[[[NSFileManager defaultManager] enumeratorAtPath:_diskPath] allObjects] count] == 0); + + [[NSFileManager defaultManager] removeItemAtPath:_diskPath error:nil]; + + [[NSFileManager defaultManager] createDirectoryAtPath:_diskPath + withIntermediateDirectories:YES + attributes:nil + error:nil]; + return !empty; + } +} + +/*! + * Removes all local ids from the memory cache. + */ +- (void)clearInMemoryCache { + @synchronized (_lock) { + [_inMemoryCache removeAllObjects]; + } +} + +@end diff --git a/Parse/Internal/Object/OperationSet/PFOperationSet.h b/Parse/Internal/Object/OperationSet/PFOperationSet.h new file mode 100644 index 000000000..fef191982 --- /dev/null +++ b/Parse/Internal/Object/OperationSet/PFOperationSet.h @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFDecoder; +@class PFEncoder; +@class PFFieldOperation; + +/*! + A set of field-level operations that can be performed on an object, corresponding to one + command. For example, all the data for a single call to save() will be packaged here. It is + assumed that the PFObject that owns the operations handles thread-safety. + */ +@interface PFOperationSet : NSObject + +/*! + Returns true if this set corresponds to a call to saveEventually. + */ +@property (nonatomic, assign, getter=isSaveEventually) BOOL saveEventually; + +/*! + A unique id for this operation set. + */ +@property (nonatomic, copy, readonly) NSString *uuid; + +@property (nonatomic, copy) NSDate *updatedAt; + +/*! + Merges the changes from the given operation set into this one. Most typically, this is what + happens when a save fails and changes need to be rolled into the next save. + */ +- (void)mergeOperationSet:(PFOperationSet *)other; + +/*! + Converts this operation set into its REST format for serializing to the pinning store + */ +- (NSDictionary *)RESTDictionaryUsingObjectEncoder:(PFEncoder *)objectEncoder + operationSetUUIDs:(NSArray **)operationSetUUIDs; + +/*! + The inverse of RESTDictionaryUsingObjectEncoder. + Creates a new OperationSet from the given NSDictionary + */ ++ (PFOperationSet *)operationSetFromRESTDictionary:(NSDictionary *)data + usingDecoder:(PFDecoder *)decoder; + +///-------------------------------------- +/// @name Accessors +///-------------------------------------- + +- (id)objectForKey:(id)aKey; +- (id)objectForKeyedSubscript:(id)aKey; +- (NSUInteger)count; +- (NSEnumerator *)keyEnumerator; + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(NSString *key, PFFieldOperation *operation, BOOL *stop))block; + +- (void)setObject:(id)anObject forKey:(id)aKey; +- (void)setObject:(id)anObject forKeyedSubscript:(id)aKey; +- (void)removeObjectForKey:(id)aKey; + +@end diff --git a/Parse/Internal/Object/OperationSet/PFOperationSet.m b/Parse/Internal/Object/OperationSet/PFOperationSet.m new file mode 100644 index 000000000..cf5ea0aee --- /dev/null +++ b/Parse/Internal/Object/OperationSet/PFOperationSet.m @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFOperationSet.h" + +#import "PFACL.h" +#import "PFACLPrivate.h" +#import "PFDecoder.h" +#import "PFEncoder.h" +#import "PFFieldOperation.h" +#import "PFInternalUtils.h" + +NSString *const PFOperationSetKeyUUID = @"__uuid"; +NSString *const PFOperationSetKeyIsSaveEventually = @"__isSaveEventually"; +NSString *const PFOperationSetKeyUpdatedAt = @"__updatedAt"; +NSString *const PFOperationSetKeyACL = @"ACL"; + +@interface PFOperationSet() + +@property (nonatomic, strong) NSMutableDictionary *dictionary; + +@end + +@implementation PFOperationSet + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + return [self initWithUUID:[[NSUUID UUID] UUIDString]]; +} + +- (instancetype)initWithUUID:(NSString *)uuid { + self = [super init]; + if (!self) return nil; + + _dictionary = [NSMutableDictionary dictionary]; + _uuid = [uuid copy]; + + _updatedAt = [NSDate date]; + + return self; +} + +///-------------------------------------- +#pragma mark - Merge +///-------------------------------------- + +- (void)mergeOperationSet:(PFOperationSet *)other { + [other.dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + PFFieldOperation *localOperation = self.dictionary[key]; + PFFieldOperation *remoteOperation = other.dictionary[key]; + if (localOperation != nil) { + localOperation = [localOperation mergeWithPrevious:remoteOperation]; + self.dictionary[key] = localOperation; + } else { + self.dictionary[key] = remoteOperation; + } + }]; + self.updatedAt = [NSDate date]; +} + +///-------------------------------------- +#pragma mark - Encoding +///-------------------------------------- + +- (NSDictionary *)RESTDictionaryUsingObjectEncoder:(PFEncoder *)objectEncoder + operationSetUUIDs:(NSArray **)operationSetUUIDs { + NSMutableDictionary *operationSetResult = [[NSMutableDictionary alloc] init]; + [self.dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + operationSetResult[key] = [obj encodeWithObjectEncoder:objectEncoder]; + }]; + + operationSetResult[PFOperationSetKeyUUID] = self.uuid; + operationSetResult[PFOperationSetKeyUpdatedAt] = [objectEncoder encodeObject:self.updatedAt]; + + if (self.saveEventually) { + operationSetResult[PFOperationSetKeyIsSaveEventually] = @YES; + } + *operationSetUUIDs = @[ self.uuid ]; + return operationSetResult; +} + ++ (PFOperationSet *)operationSetFromRESTDictionary:(NSDictionary *)data + usingDecoder:(PFDecoder *)decoder { + NSMutableDictionary *mutableData = [data mutableCopy]; + NSString *inputUUID = mutableData[PFOperationSetKeyUUID]; + [mutableData removeObjectForKey:PFOperationSetKeyUUID]; + PFOperationSet *operationSet = nil; + if (inputUUID == nil) { + operationSet = [[PFOperationSet alloc] init]; + } else { + operationSet = [[PFOperationSet alloc] initWithUUID:inputUUID]; + } + + NSNumber *saveEventuallyFlag = mutableData[PFOperationSetKeyIsSaveEventually]; + if (saveEventuallyFlag) { + operationSet.saveEventually = [saveEventuallyFlag boolValue]; + [mutableData removeObjectForKey:PFOperationSetKeyIsSaveEventually]; + } + + NSDate *updatedAt = mutableData[PFOperationSetKeyUpdatedAt]; + [mutableData removeObjectForKey:PFOperationSetKeyUpdatedAt]; + + [mutableData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + id value = [decoder decodeObject:obj]; + PFFieldOperation *fieldOperation = nil; + if ([key isEqualToString:PFOperationSetKeyACL]) { + // TODO (hallucinogen): where to use the decoder? + value = [PFACL ACLWithDictionary:obj]; + } + if ([value isKindOfClass:[PFFieldOperation class]]) { + fieldOperation = value; + } else { + fieldOperation = [PFSetOperation setWithValue:value]; + } + operationSet[key] = fieldOperation; + }]; + operationSet.updatedAt = updatedAt ? [decoder decodeObject:updatedAt] : nil; + + return operationSet; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (id)objectForKey:(id)aKey { + return self.dictionary[aKey]; +} + +- (id)objectForKeyedSubscript:(id)aKey { + return [self objectForKey:aKey]; +} + +- (NSUInteger)count { + return [self.dictionary count]; +} + +- (NSEnumerator *)keyEnumerator { + return [self.dictionary keyEnumerator]; +} + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(NSString *key, PFFieldOperation *operation, BOOL *stop))block { + [self.dictionary enumerateKeysAndObjectsUsingBlock:block]; +} + +- (void)setObject:(id)anObject forKey:(id)aKey { + self.dictionary[aKey] = anObject; + self.updatedAt = [NSDate date]; +} + +- (void)setObject:(id)anObject forKeyedSubscript:(id)key { + [self setObject:anObject forKey:key]; +} + +- (void)removeObjectForKey:(id)key { + [self.dictionary removeObjectForKey:key]; + self.updatedAt = [NSDate date]; +} + +///-------------------------------------- +#pragma mark - NSFastEnumeration +///-------------------------------------- + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])buffer + count:(NSUInteger)len { + return [self.dictionary countByEnumeratingWithState:state objects:buffer count:len]; +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (instancetype)copyWithZone:(NSZone *)zone { + PFOperationSet *operationSet = [[[self class] allocWithZone:zone] initWithUUID:self.uuid]; + operationSet.dictionary = [self.dictionary mutableCopy]; + operationSet.updatedAt = [self.updatedAt copy]; + operationSet.saveEventually = self.saveEventually; + return operationSet; +} + +@end diff --git a/Parse/Internal/Object/PFObjectPrivate.h b/Parse/Internal/Object/PFObjectPrivate.h new file mode 100644 index 000000000..669713e40 --- /dev/null +++ b/Parse/Internal/Object/PFObjectPrivate.h @@ -0,0 +1,294 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +#import + +#import "PFDecoder.h" +#import "PFEncoder.h" +#import "PFMulticastDelegate.h" +#import "PFObjectControlling.h" + +@class BFTask; +@class PFCurrentUserController; +@class PFFieldOperation; +@class PFJSONCacheItem; +@class PFMultiCommand; +@class PFObjectEstimatedData; +@class PFObjectFileCodingLogic; +@class PFObjectState; +@class PFObjectSubclassingController; +@class PFOperationSet; +@class PFPinningObjectStore; +@class PFRESTCommand; +@class PFTaskQueue; + +///-------------------------------------- +#pragma mark - PFObjectPrivateSubclass +///-------------------------------------- + +@protocol PFObjectPrivateSubclass + +///-------------------------------------- +/// @name State +///-------------------------------------- + ++ (PFObjectState *)_newObjectStateWithParseClassName:(NSString *)className + objectId:(NSString *)objectId + isComplete:(BOOL)complete; + +///-------------------------------------- +/// @name Validation +///-------------------------------------- + +/*! + Validate the save eventually operation with the current state. + The result of this task is ignored. The error/cancellation/exception will prevent `saveEventually`. + + @returns Task that encapsulates the validtion. + */ +- (BFTask *)_validateSaveEventuallyAsync; + +@end + +///-------------------------------------- +#pragma mark - PFObject +///-------------------------------------- + +// Extension for property methods. +@interface PFObject () + +/*! + @returns Current object state. + */ +@property (nonatomic, copy) PFObjectState *_state; +@property (nonatomic, copy) NSMutableSet *_availableKeys; + +- (instancetype)initWithObjectState:(PFObjectState *)state; ++ (instancetype)objectWithClassName:(NSString *)className + objectId:(NSString *)objectid + completeData:(BOOL)completeData; ++ (instancetype)objectWithoutDataWithClassName:(NSString *)className localId:(NSString *)localId; + +- (PFTaskQueue *)taskQueue; + +- (PFObjectEstimatedData *)_estimatedData; + +#if PARSE_OSX_ONLY +// Not available publicly, but available for testing + +- (void)refresh; +- (void)refresh:(NSError **)error; +- (void)refreshInBackgroundWithBlock:(PFObjectResultBlock)block; +- (void)refreshInBackgroundWithTarget:(id)target selector:(SEL)selector; + +#endif + +///-------------------------------------- +/// @name Pin +///-------------------------------------- +- (BFTask *)_pinInBackgroundWithName:(NSString *)name includeChildren:(BOOL)includeChildren; ++ (BFTask *)_pinAllInBackground:(NSArray *)objects withName:(NSString *)name includeChildren:(BOOL)includeChildren; + ++ (PFPinningObjectStore *)pinningObjectStore; ++ (id)objectController; ++ (PFObjectFileCodingLogic *)objectFileCodingLogic; ++ (PFCurrentUserController *)currentUserController; + +///-------------------------------------- +#pragma mark - Subclassing +///-------------------------------------- + ++ (PFObjectSubclassingController *)subclassingController; + +@end + +@interface PFObject (Private) + +/*! + Returns the object that should be used to synchronize all internal data access. + */ +- (NSObject *)lock; + +/*! + Blocks until all outstanding operations have completed. + */ +- (void)waitUntilFinished; + +- (NSDictionary *)_collectFetchedObjects; + +///-------------------------------------- +#pragma mark - Static methods for Subclassing +///-------------------------------------- + +/*! + Unregisters a class registered using registerSubclass: + If we ever expose thsi method publicly, we must change the underlying implementation + to have stack behavior. Currently unregistering a custom class for a built-in will + leave the built-in unregistered as well. + @param subclass the subclass + */ ++ (void)unregisterSubclass:(Class)subclass; + +///-------------------------------------- +#pragma mark - Children helpers +///-------------------------------------- +- (BFTask *)_saveChildrenInBackgroundWithCurrentUser:(PFUser *)currentUser sessionToken:(NSString *)sessionToken; + +///-------------------------------------- +#pragma mark - Dirtiness helpers +///-------------------------------------- +- (BOOL)isDirty:(BOOL)considerChildren; +- (void)_setDirty:(BOOL)dirty; + +- (void)performOperation:(PFFieldOperation *)operation forKey:(NSString *)key; +- (void)setHasBeenFetched:(BOOL)fetched; +- (void)_setDeleted:(BOOL)deleted; + +- (BOOL)isDataAvailableForKey:(NSString *)key; + +- (BOOL)_hasChanges; +- (BOOL)_hasOutstandingOperations; +- (PFOperationSet *)unsavedChanges; + +///-------------------------------------- +#pragma mark - Validations +///-------------------------------------- +- (void)checkDeleteParams; +- (void)_checkSaveParametersWithCurrentUser:(PFUser *)currentUser; +/*! + Checks if Parse class name could be used to initialize a given instance of PFObject or it's subclass. + */ ++ (void)_assertValidInstanceClassName:(NSString *)className; + +///-------------------------------------- +#pragma mark - Serialization helpers +///-------------------------------------- +- (NSString *)getOrCreateLocalId; +- (void)resolveLocalId; + ++ (id)_objectFromDictionary:(NSDictionary *)dictionary + defaultClassName:(NSString *)defaultClassName + completeData:(BOOL)completeData; + ++ (id)_objectFromDictionary:(NSDictionary *)dictionary + defaultClassName:(NSString *)defaultClassName + selectedKeys:(NSArray *)selectedKeys; + ++ (id)_objectFromDictionary:(NSDictionary *)dictionary + defaultClassName:(NSString *)defaultClassName + completeData:(BOOL)completeData + decoder:(PFDecoder *)decoder; ++ (BFTask *)_migrateObjectInBackgroundFromFile:(NSString *)fileName toPin:(NSString *)pinName; ++ (BFTask *)_migrateObjectInBackgroundFromFile:(NSString *)fileName + toPin:(NSString *)pinName + usingMigrationBlock:(BFContinuationBlock)block; + +- (NSMutableDictionary *)_convertToDictionaryForSaving:(PFOperationSet *)changes + withObjectEncoder:(PFEncoder *)encoder; + +///-------------------------------------- +#pragma mark - REST operations +///-------------------------------------- +- (NSDictionary *)RESTDictionaryWithObjectEncoder:(PFEncoder *)objectEncoder + operationSetUUIDs:(NSArray **)operationSetUUIDs; +- (NSDictionary *)RESTDictionaryWithObjectEncoder:(PFEncoder *)objectEncoder + operationSetUUIDs:(NSArray **)operationSetUUIDs + state:(PFObjectState *)state + operationSetQueue:(NSArray *)operationSetQueue; + +- (void)mergeFromRESTDictionary:(NSDictionary *)object + withDecoder:(PFDecoder *)decoder; + +///-------------------------------------- +#pragma mark - Data helpers +///-------------------------------------- +- (void)checkForChangesToMutableContainers; +- (void)rebuildEstimatedData; + +///-------------------------------------- +#pragma mark - Command handlers +///-------------------------------------- +- (PFObject *)mergeFromObject:(PFObject *)other; + +- (void)_mergeAfterSaveWithResult:(NSDictionary *)result decoder:(PFDecoder *)decoder; +- (void)_mergeAfterFetchWithResult:(NSDictionary *)result decoder:(PFDecoder *)decoder completeData:(BOOL)completeData; +- (void)_mergeFromServerWithResult:(NSDictionary *)result decoder:(PFDecoder *)decoder completeData:(BOOL)completeData; + +- (BFTask *)handleSaveResultAsync:(NSDictionary *)result; + +///-------------------------------------- +#pragma mark - Asynchronous operations +///-------------------------------------- +- (void)startSave; +- (BFTask *)_enqueueSaveEventuallyWithChildren:(BOOL)saveChildren; +- (BFTask *)saveAsync:(BFTask *)toAwait; +- (BFTask *)fetchAsync:(BFTask *)toAwait; +- (BFTask *)deleteAsync:(BFTask *)toAwait; + +///-------------------------------------- +#pragma mark - Command constructors +///-------------------------------------- +- (PFRESTCommand *)_constructSaveCommandForChanges:(PFOperationSet *)changes + sessionToken:(NSString *)sessionToken + objectEncoder:(PFEncoder *)encoder; +- (PFRESTCommand *)_currentDeleteCommandWithSessionToken:(NSString *)sessionToken; + +///-------------------------------------- +#pragma mark - Misc helpers +///-------------------------------------- +- (NSString *)displayClassName; +- (NSString *)displayObjectId; + +- (void)registerSaveListener:(void (^)(id result, NSError *error))callback; +- (void)unregisterSaveListener:(void (^)(id result, NSError *error))callback; +- (PFACL *)ACLWithoutCopying; + +///-------------------------------------- +#pragma mark - Get and set +///-------------------------------------- + +- (void)_setObject:(id)object + forKey:(NSString *)key + onlyIfDifferent:(BOOL)onlyIfDifferent; + +///-------------------------------------- +#pragma mark - Subclass Helpers +///-------------------------------------- + +/*! + This method is called by -[PFObject init]; changes made to the object during this + method will not mark the object as dirty. PFObject uses this method to to apply the + default ACL; subclasses which override this method shold be sure to call the super + implementation if they want to honor the default ACL. + */ +- (void)setDefaultValues; + +/*! + This method allows subclasses to determine whether a default ACL should be applied + to new instances. + */ +- (BOOL)needsDefaultACL; + +@end + +@interface PFObject () { + PFMulticastDelegate *saveDelegate; +} + +@property (nonatomic, strong) PFMulticastDelegate *saveDelegate; + +@end diff --git a/Parse/Internal/Object/PinningStore/PFPinningObjectStore.h b/Parse/Internal/Object/PinningStore/PFPinningObjectStore.h new file mode 100644 index 000000000..49718fbe3 --- /dev/null +++ b/Parse/Internal/Object/PinningStore/PFPinningObjectStore.h @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@class BFTask; + +@interface PFPinningObjectStore : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)storeWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Pin +///-------------------------------------- + +/*! + Gets pin with name equals to given name. + + @param name Pin Name. + + @returns `BFTask` with `PFPin` result if pinning succeeds. + */ +- (BFTask *)fetchPinAsyncWithName:(NSString *)name; + +/*! + Pins given objects to the pin. Creates new pin if the pin with such name is not found. + + @param objects Array of `PFObject`s to pin. + @param name Pin Name. + @param includeChildren Whether children of `objects` should be pinned as well. + + @returns `BFTask` with `@YES` result. + */ +- (BFTask *)pinObjectsAsync:(nullable NSArray *)objects + withPinName:(NSString *)name + includeChildren:(BOOL)includeChildren; + +///-------------------------------------- +/// @name Unpin +///-------------------------------------- + +/*! + Unpins given array of objects from the pin. + + @param objects Objects to unpin. + @param name Pin name. + + @returns `BFTask` with `@YES` result. + */ +- (BFTask *)unpinObjectsAsync:(nullable NSArray *)objects withPinName:(NSString *)name; + +/*! + Unpins all objects from the pin. + + @param name Pin name. + + @returns `BFTask` with `YES` result. + */ +- (BFTask *)unpinAllObjectsAsyncWithPinName:(NSString *)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/PinningStore/PFPinningObjectStore.m b/Parse/Internal/Object/PinningStore/PFPinningObjectStore.m new file mode 100644 index 000000000..b731df1cc --- /dev/null +++ b/Parse/Internal/Object/PinningStore/PFPinningObjectStore.m @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPinningObjectStore.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFMacros.h" +#import "PFOfflineStore.h" +#import "PFPin.h" +#import "PFQueryPrivate.h" + +@interface PFPinningObjectStore () { + NSMapTable *_pinCacheTable; + dispatch_queue_t _pinCacheAccessQueue; + BFExecutor *_pinCacheAccessExecutor; +} + +@end + +@implementation PFPinningObjectStore + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _pinCacheTable = [NSMapTable strongToWeakObjectsMapTable]; + _pinCacheAccessQueue = dispatch_queue_create("com.parse.object.pin.cache", DISPATCH_QUEUE_SERIAL); + _pinCacheAccessExecutor = [BFExecutor executorWithDispatchQueue:_pinCacheAccessQueue]; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)storeWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Pin +///-------------------------------------- + +- (BFTask *)fetchPinAsyncWithName:(NSString *)name { + @weakify(self); + return [BFTask taskFromExecutor:_pinCacheAccessExecutor withBlock:^id{ + BFTask *cachedTask = [_pinCacheTable objectForKey:name] ?: [BFTask taskWithResult:nil]; + // We need to call directly to OfflineStore since we don't want/need a user to query for ParsePins + cachedTask = [cachedTask continueWithBlock:^id(BFTask *task) { + @strongify(self); + PFQuery *query = [[PFPin query] whereKey:PFPinKeyName equalTo:name]; + PFOfflineStore *store = self.dataSource.offlineStore; + return [[store findAsyncForQueryState:query.state + user:nil + pin:nil] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *result = task.result; + // TODO (hallucinogen): What do we do if there are more than 1 result? + PFPin *pin = (result.count != 0 ? result.firstObject : [PFPin pinWithName:name]); + return pin; + }]; + }]; + // Put the task back into the cache. + [_pinCacheTable setObject:cachedTask forKey:name]; + return cachedTask; + }]; +} + +- (BFTask *)pinObjectsAsync:(NSArray *)objects withPinName:(NSString *)name includeChildren:(BOOL)includeChildren { + if (objects.count == 0) { + return [BFTask taskWithResult:@YES]; + } + + @weakify(self); + return [[[self fetchPinAsyncWithName:name] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFPin *pin = task.result; + PFOfflineStore *store = self.dataSource.offlineStore; + //TODO (hallucinogen): some stuff @grantland mentioned can't be done maybe needs to be done here + //TODO (grantland): change to use relations. currently the related PO are only getting saved + //TODO (grantland): can't add and then remove + + // Hack to store collection in a pin + NSMutableArray *modified = pin.objects; + if (modified == nil) { + modified = [objects mutableCopy]; + } else { + for (PFObject *object in objects) { + if (![modified containsObject:object]) { + [modified addObject:object]; + } + } + } + pin.objects = modified; + + BFTask *saveTask = nil; + if (includeChildren) { + saveTask = [store saveObjectLocallyAsync:pin includeChildren:YES]; + } else { + saveTask = [store saveObjectLocallyAsync:pin withChildren:pin.objects]; + } + return saveTask; + }] continueWithSuccessResult:@YES]; +} + +///-------------------------------------- +#pragma mark - Unpin +///-------------------------------------- + +- (BFTask *)unpinObjectsAsync:(NSArray *)objects withPinName:(NSString *)name { + if (objects.count == 0) { + return [BFTask taskWithResult:@YES]; + } + + @weakify(self); + return [[[self fetchPinAsyncWithName:name] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFPin *pin = task.result; + NSMutableArray *modified = pin.objects; + if (!modified) { + // Nothing to unpin + return task; + } + + //TODO (hallucinogen): some stuff @grantland mentioned can't be done maybe needs to be done here + //TODO (grantland): change to use relations. currently the related PO are only getting saved + //TODO (grantland): can't add and then remove + + PFOfflineStore *store = self.dataSource.offlineStore; + + [modified removeObjectsInArray:objects]; + if (modified.count == 0) { + return [store unpinObjectAsync:pin]; + } + pin.objects = modified; + + return [store saveObjectLocallyAsync:pin includeChildren:YES]; + }] continueWithSuccessResult:@YES]; +} + +- (BFTask *)unpinAllObjectsAsyncWithPinName:(NSString *)name { + @weakify(self); + return [[self fetchPinAsyncWithName:name] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFPin *pin = task.result; + return [[self.dataSource.offlineStore unpinObjectAsync:pin] continueWithSuccessResult:@YES]; + }]; +} + +@end diff --git a/Parse/Internal/Object/State/PFMutableObjectState.h b/Parse/Internal/Object/State/PFMutableObjectState.h new file mode 100644 index 000000000..70cae09ee --- /dev/null +++ b/Parse/Internal/Object/State/PFMutableObjectState.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectState.h" + +@class PFOperationSet; + +@interface PFMutableObjectState : PFObjectState + +@property (nonatomic, copy, readwrite) NSString *parseClassName; +@property (nonatomic, copy, readwrite) NSString *objectId; + +@property (nonatomic, strong, readwrite) NSDate *createdAt; +@property (nonatomic, strong, readwrite) NSDate *updatedAt; + +@property (nonatomic, copy, readwrite) NSDictionary *serverData; + +@property (nonatomic, assign, readwrite, getter=isComplete) BOOL complete; +@property (nonatomic, assign, readwrite, getter=isDeleted) BOOL deleted; + +///-------------------------------------- +/// @name Accessors +///-------------------------------------- + +- (void)setServerDataObject:(id)object forKey:(NSString *)key; +- (void)removeServerDataObjectForKey:(NSString *)key; +- (void)removeServerDataObjectsForKeys:(NSArray *)keys; + +- (void)setCreatedAtFromString:(NSString *)string; +- (void)setUpdatedAtFromString:(NSString *)string; + +///-------------------------------------- +/// @name Apply +///-------------------------------------- + +- (void)applyState:(PFObjectState *)state; +- (void)applyOperationSet:(PFOperationSet *)operationSet; + +@end diff --git a/Parse/Internal/Object/State/PFMutableObjectState.m b/Parse/Internal/Object/State/PFMutableObjectState.m new file mode 100644 index 000000000..edfd65252 --- /dev/null +++ b/Parse/Internal/Object/State/PFMutableObjectState.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableObjectState.h" + +#import "PFDateFormatter.h" +#import "PFObjectState_Private.h" + +@implementation PFMutableObjectState + +@dynamic parseClassName; +@dynamic objectId; +@dynamic createdAt; +@dynamic updatedAt; +@dynamic serverData; +@dynamic complete; +@dynamic deleted; + +///-------------------------------------- +#pragma mark - PFMutableObjectState +///-------------------------------------- + +#pragma mark Accessors + +- (void)setServerDataObject:(id)object forKey:(NSString *)key { + [super setServerDataObject:object forKey:key]; +} + +- (void)removeServerDataObjectForKey:(NSString *)key { + [super removeServerDataObjectForKey:key]; +} + +- (void)removeServerDataObjectsForKeys:(NSArray *)keys { + [super removeServerDataObjectsForKeys:keys]; +} + +- (void)setCreatedAtFromString:(NSString *)string { + [super setCreatedAtFromString:string]; +} + +- (void)setUpdatedAtFromString:(NSString *)string { + [super setUpdatedAtFromString:string]; +} + +#pragma mark Apply + +- (void)applyState:(PFObjectState *)state { + [super applyState:state]; +} + +- (void)applyOperationSet:(PFOperationSet *)operationSet { + [super applyOperationSet:operationSet]; +} + +@end diff --git a/Parse/Internal/Object/State/PFObjectState.h b/Parse/Internal/Object/State/PFObjectState.h new file mode 100644 index 000000000..a7d974497 --- /dev/null +++ b/Parse/Internal/Object/State/PFObjectState.h @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFEncoder; + +@interface PFObjectState : NSObject + +@property (nonatomic, copy, readonly) NSString *parseClassName; +@property (nonatomic, copy, readonly) NSString *objectId; + +@property (nonatomic, strong, readonly) NSDate *createdAt; +@property (nonatomic, strong, readonly) NSDate *updatedAt; + +@property (nonatomic, copy, readonly) NSDictionary *serverData; + +@property (nonatomic, assign, readonly, getter=isComplete) BOOL complete; +@property (nonatomic, assign, readonly, getter=isDeleted) BOOL deleted; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithState:(PFObjectState *)state NS_REQUIRES_SUPER; +- (instancetype)initWithParseClassName:(NSString *)parseClassName; +- (instancetype)initWithParseClassName:(NSString *)parseClassName + objectId:(NSString *)objectId + isComplete:(BOOL)complete; + ++ (instancetype)stateWithState:(PFObjectState *)state NS_REQUIRES_SUPER; ++ (instancetype)stateWithParseClassName:(NSString *)parseClassName; ++ (instancetype)stateWithParseClassName:(NSString *)parseClassName + objectId:(NSString *)objectId + isComplete:(BOOL)complete; + +///-------------------------------------- +/// @name Coding +///-------------------------------------- + +/*! + Encodes all fields in `serverData`, `objectId`, `createdAt` and `updatedAt` into objects suitable for JSON/Persistence. + + @note `parseClassName` isn't automatically added to the dictionary. + + @param objectEncoder Encoder to use to encode custom objects. + + @returns `NSDictionary` instance representing object state. + */ +- (NSDictionary *)dictionaryRepresentationWithObjectEncoder:(PFEncoder *)objectEncoder NS_REQUIRES_SUPER; + +@end diff --git a/Parse/Internal/Object/State/PFObjectState.m b/Parse/Internal/Object/State/PFObjectState.m new file mode 100644 index 000000000..fec73f484 --- /dev/null +++ b/Parse/Internal/Object/State/PFObjectState.m @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectState.h" +#import "PFObjectState_Private.h" + +#import "PFDateFormatter.h" +#import "PFEncoder.h" +#import "PFMutableObjectState.h" +#import "PFObjectConstants.h" +#import "PFObjectUtilities.h" + +@implementation PFObjectState + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _serverData = [NSMutableDictionary dictionary]; + + return [super init]; +} + +- (instancetype)initWithState:(PFObjectState *)state { + self = [self init]; + if (!self) return nil; + + _parseClassName = [state.parseClassName copy]; + _objectId = [state.objectId copy]; + + _updatedAt = state.updatedAt; + _createdAt = state.createdAt; + + _serverData = [state.serverData mutableCopy] ?: [NSMutableDictionary dictionary]; + + _complete = state.complete; + _deleted = state.deleted; + + return self; +} + +- (instancetype)initWithParseClassName:(NSString *)parseClassName { + return [self initWithParseClassName:parseClassName objectId:nil isComplete:NO]; +} + +- (instancetype)initWithParseClassName:(NSString *)parseClassName + objectId:(NSString *)objectId + isComplete:(BOOL)complete { + self = [self init]; + if (!self) return nil; + + _parseClassName = [parseClassName copy]; + _objectId = [objectId copy]; + _complete = complete; + + return self; +} + ++ (instancetype)stateWithState:(PFObjectState *)state { + return [[self alloc] initWithState:state]; +} + ++ (instancetype)stateWithParseClassName:(NSString *)parseClassName { + return [[self alloc] initWithParseClassName:parseClassName]; +} + ++ (instancetype)stateWithParseClassName:(NSString *)parseClassName + objectId:(NSString *)objectId + isComplete:(BOOL)complete { + return [[self alloc] initWithParseClassName:parseClassName + objectId:objectId + isComplete:complete]; +} + +///-------------------------------------- +#pragma mark - Accessors +///--------------------------------------s + +- (void)setServerData:(NSDictionary *)serverData { + if (self.serverData != serverData) { + _serverData = [serverData mutableCopy]; + } +} + +///-------------------------------------- +#pragma mark - Coding +///-------------------------------------- + +- (NSDictionary *)dictionaryRepresentationWithObjectEncoder:(PFEncoder *)objectEncoder { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + if (self.objectId) { + result[PFObjectObjectIdRESTKey] = self.objectId; + } + if (self.createdAt) { + result[PFObjectCreatedAtRESTKey] = [[PFDateFormatter sharedFormatter] preciseStringFromDate:self.createdAt]; + } + if (self.updatedAt) { + result[PFObjectUpdatedAtRESTKey] = [[PFDateFormatter sharedFormatter] preciseStringFromDate:self.updatedAt]; + } + [self.serverData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + result[key] = [objectEncoder encodeObject:obj]; + }]; + return [result copy]; +} + +///-------------------------------------- +#pragma mark - PFObjectState (Mutable) +///-------------------------------------- + +#pragma mark Accessors + +- (void)setServerDataObject:(id)object forKey:(NSString *)key { + _serverData[key] = object; +} + +- (void)removeServerDataObjectForKey:(NSString *)key { + [_serverData removeObjectForKey:key]; +} + +- (void)removeServerDataObjectsForKeys:(NSArray *)keys { + [_serverData removeObjectsForKeys:keys]; +} + +- (void)setCreatedAtFromString:(NSString *)string { + self.createdAt = [[PFDateFormatter sharedFormatter] dateFromString:string]; +} + +- (void)setUpdatedAtFromString:(NSString *)string { + self.updatedAt = [[PFDateFormatter sharedFormatter] dateFromString:string]; +} + +#pragma mark Apply + +- (void)applyState:(PFObjectState *)state { + if (state.objectId) { + self.objectId = state.objectId; + } + if (state.createdAt) { + self.createdAt = state.createdAt; + } + if (state.updatedAt) { + self.updatedAt = state.updatedAt; + } + [_serverData addEntriesFromDictionary:state.serverData]; + + self.complete |= state.complete; +} + +- (void)applyOperationSet:(PFOperationSet *)operationSet { + [PFObjectUtilities applyOperationSet:operationSet toDictionary:_serverData]; +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (id)copyWithZone:(NSZone *)zone { + return [[PFObjectState allocWithZone:zone] initWithState:self]; +} + +///-------------------------------------- +#pragma mark - NSMutableCopying +///-------------------------------------- + +- (id)mutableCopyWithZone:(NSZone *)zone { + return [[PFMutableObjectState allocWithZone:zone] initWithState:self]; +} + +@end diff --git a/Parse/Internal/Object/State/PFObjectState_Private.h b/Parse/Internal/Object/State/PFObjectState_Private.h new file mode 100644 index 000000000..d5f8d639d --- /dev/null +++ b/Parse/Internal/Object/State/PFObjectState_Private.h @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectState.h" + +@class PFOperationSet; + +@interface PFObjectState () { +@protected + NSString *_parseClassName; + NSString *_objectId; + NSDate *_createdAt; + NSDate *_updatedAt; + NSMutableDictionary *_serverData; + + BOOL _complete; + BOOL _deleted; +} + +@property (nonatomic, copy, readwrite) NSString *parseClassName; +@property (nonatomic, copy, readwrite) NSString *objectId; +@property (nonatomic, strong, readwrite) NSDate *createdAt; +@property (nonatomic, strong, readwrite) NSDate *updatedAt; +@property (nonatomic, copy, readwrite) NSMutableDictionary *serverData; + +@property (nonatomic, assign, readwrite, getter=isComplete) BOOL complete; +@property (nonatomic, assign, readwrite, getter=isDeleted) BOOL deleted; + +@end + +@interface PFObjectState (Mutable) + +///-------------------------------------- +/// @name Accessors +///-------------------------------------- + +- (void)setServerDataObject:(id)object forKey:(NSString *)key; +- (void)removeServerDataObjectForKey:(NSString *)key; +- (void)removeServerDataObjectsForKeys:(NSArray *)keys; + +- (void)setCreatedAtFromString:(NSString *)string; +- (void)setUpdatedAtFromString:(NSString *)string; + +///-------------------------------------- +/// @name Apply +///-------------------------------------- + +- (void)applyState:(PFObjectState *)state NS_REQUIRES_SUPER; +- (void)applyOperationSet:(PFOperationSet *)operationSet NS_REQUIRES_SUPER; + +@end diff --git a/Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.h b/Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.h new file mode 100644 index 000000000..3db23d7da --- /dev/null +++ b/Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFPropertyInfo; + +@interface PFObjectSubclassInfo : NSObject + +@property (atomic, strong) Class subclass; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithSubclass:(Class)kls NS_DESIGNATED_INITIALIZER; ++ (instancetype)subclassInfoWithSubclass:(Class)kls; + +- (PFPropertyInfo *)propertyInfoForSelector:(SEL)cmd isSetter:(BOOL *)isSetter; +- (NSMethodSignature *)forwardingMethodSignatureForSelector:(SEL)cmd; + +@end diff --git a/Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.m b/Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.m new file mode 100644 index 000000000..8aeb6ec08 --- /dev/null +++ b/Parse/Internal/Object/Subclassing/PFObjectSubclassInfo.m @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectSubclassInfo.h" + +#import + +#import "PFAssert.h" +#import "PFLogging.h" +#import "PFMacros.h" +#import "PFPropertyInfo_Private.h" + +///-------------------------------------- +#pragma mark - Helper +///-------------------------------------- + +static BOOL startsWith(const char *string, const char *prefix) { + // Keep iterating in lockstep. If we run out of prefix letters first, + // this is a valid prefix. + for (; *string && *prefix && *prefix == *string; ++string, ++prefix) + ; + return !*prefix; +} + +// This method helps us get our bearings regardless of whether we were passed +// setFoo: or foo. We'll always exit this method by setting outPair to +// [accessor, mutator] and returns the property they correspond to. If the +// property cannot be found, returns NULL and outPair is undefined. +// An objc_property_t is an opaque struct pointer containing a SEL name and char * +// type information which follows a DSL explained in the Objective-C Runtime Reference. +static objc_property_t getAccessorMutatorPair(Class klass, SEL sel, SEL outPair[2]) { + const char *selName = sel_getName(sel); + ptrdiff_t selNameByteLen = strlen(selName) + 1; + char temp[selNameByteLen + 4]; + + if (startsWith(selName, "set")) { + outPair[1] = sel; + memcpy(temp, selName + 3, selNameByteLen - 3); + temp[0] -= 'A' - 'a'; + + temp[selNameByteLen - 5] = 0; // drop ':' + outPair[0] = sel_registerName(temp); + } else { + outPair[0] = sel; + sprintf(temp, "set%s:", selName); + if (selName[0] >= 'a' && selName[0] <= 'z') { + temp[3] += 'A' - 'a'; + } + outPair[1] = sel_registerName(temp); + } + + const char *propName = sel_getName(outPair[0]); + objc_property_t property = class_getProperty(klass, propName); + if (!property) { + // The user could have broken convention and declared an upper case property. + memcpy(temp, propName, strlen(propName) + 1); + temp[0] += 'A' - 'a'; + outPair[0] = sel_registerName(temp); + property = class_getProperty(klass, temp); + } + return property; +} + +@implementation PFObjectSubclassInfo { + dispatch_queue_t _dataAccessQueue; + NSMutableDictionary *_knownProperties; + NSMutableDictionary *_knownMethodSignatures; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithSubclass:(Class)kls { + self = [super init]; + if (!self) return nil; + + _dataAccessQueue = dispatch_queue_create("com.parse.object.subclassing.data.access", DISPATCH_QUEUE_SERIAL); + _subclass = kls; + + return self; +} + ++ (instancetype)subclassInfoWithSubclass:(Class)kls { + return [[self alloc] initWithSubclass:kls]; +} + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + +- (PFPropertyInfo *)propertyInfoForSelector:(SEL)cmd isSetter:(BOOL *)isSetter { + __block PFPropertyInfo *result = nil; + dispatch_sync(_dataAccessQueue, ^{ + result = [self _rawPropertyInfoForSelector:cmd]; + }); + + if (isSetter) { + *isSetter = (cmd == result.setterSelector); + } + + return result; +} + +- (NSMethodSignature *)forwardingMethodSignatureForSelector:(SEL)cmd { + __block NSMethodSignature *result = nil; + NSString *selectorString = NSStringFromSelector(cmd); + + // NSMethodSignature can be fairly heavyweight, so let's agressively cache this here. + dispatch_sync(_dataAccessQueue, ^{ + result = _knownMethodSignatures[selectorString]; + if (result) { + return; + } + + PFPropertyInfo *propertyInfo = [self _rawPropertyInfoForSelector:cmd]; + if (!propertyInfo) { + return; + } + + BOOL isSetter = (cmd == propertyInfo.setterSelector); + NSString *typeEncoding = propertyInfo.typeEncoding; + + // Property type encoding includes the class name as well. + // This is fine, except for the fact that NSMethodSignature hates that. + NSUInteger startLocation = [typeEncoding rangeOfString:@"\"" options:0].location; + NSUInteger endLocation = [typeEncoding rangeOfString:@"\"" + options:NSBackwardsSearch | NSAnchoredSearch].location; + + if (startLocation != NSNotFound && endLocation != NSNotFound) { + typeEncoding = [typeEncoding substringToIndex:startLocation]; + } + + NSString *objcTypes = ([NSString stringWithFormat:(isSetter ? @"v@:%@" : @"%@@:"), typeEncoding]); + result = [NSMethodSignature signatureWithObjCTypes:[objcTypes UTF8String]]; + + _knownMethodSignatures[selectorString] = result; + }); + + return result; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (PFPropertyInfo *)_rawPropertyInfoForSelector:(SEL)cmd { + PFPropertyInfo *result = nil; + NSString *selectorString = NSStringFromSelector(cmd); + result = _knownProperties[selectorString]; + if (result) { + return result; + } + + SEL propertySelectors[2]; + objc_property_t property = getAccessorMutatorPair(self.subclass, cmd, propertySelectors); + if (!property) { + return nil; + } + + // Check if we've registered this property with a different name. + NSString *propertyName = @(property_getName(property)); + result = _knownProperties[propertyName]; + if (result) { + // Re-register it with the name we just searched for for faster future lookup. + _knownProperties[selectorString] = result; + return result; + } + + const char *attributes = property_getAttributes(property); + if (strstr(attributes, "T@\"PFRelation\",") == attributes && !strstr(attributes, ",R")) { + PFLogWarning(PFLoggingTagCommon, + @"PFRelation properties are always readonly, but %@.%@ was declared otherwise.", + self.subclass, selectorString); + } + + result = [PFPropertyInfo propertyInfoWithClass:self.subclass name:propertyName]; + + _knownProperties[result.name] = result; + if (result.getterSelector) { + _knownProperties[NSStringFromSelector(result.getterSelector)] = result; + } + if (result.setterSelector) { + _knownProperties[NSStringFromSelector(result.setterSelector)] = result; + } + + return result; +} + +@end diff --git a/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h new file mode 100644 index 000000000..ef7977bbd --- /dev/null +++ b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class PFObject; +@protocol PFSubclassing; + +@interface PFObjectSubclassingController : NSObject + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +//TODO: (nlutsenko, richardross) Make it not terrible aka don't have singletons. ++ (instancetype)defaultController; ++ (void)clearDefaultController; + +///-------------------------------------- +/// @name Registration +///-------------------------------------- + +- (Class)subclassForParseClassName:(NSString *)parseClassName; +- (void)registerSubclass:(Class)kls; +- (void)unregisterSubclass:(Class)kls; + +///-------------------------------------- +/// @name Forwarding +///-------------------------------------- + +- (NSMethodSignature *)forwardingMethodSignatureForSelector:(SEL)cmd ofClass:(Class)kls; +- (BOOL)forwardObjectInvocation:(NSInvocation *)invocation withObject:(PFObject *)object; + +@end diff --git a/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m new file mode 100644 index 000000000..0873056a9 --- /dev/null +++ b/Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m @@ -0,0 +1,314 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectSubclassingController.h" + +#import + +#import "PFAssert.h" +#import "PFMacros.h" +#import "PFObject.h" +#import "PFObjectSubclassInfo.h" +#import "PFPropertyInfo_Private.h" +#import "PFPropertyInfo_Runtime.h" +#import "PFSubclassing.h" +#import "PFUser.h" +#import "PFSession.h" +#import "PFPin.h" +#import "PFRole.h" +#import "PFEventuallyPin.h" +#import "PFInstallation.h" + +#if TARGET_OS_IPHONE +#import "PFProduct.h" +#endif + +// CFNumber does not use number type 0, we take advantage of that here. +#define kCFNumberTypeUnknown 0 + +static CFNumberType PFNumberTypeForObjCType(const char *encodedType) { +// To save anyone in the future from some major headaches, sanity check here. +#if kCFNumberTypeMax > UINT8_MAX +#error kCFNumberTypeMax has been changed! This solution will no longer work. +#endif + + // Organizing the table this way makes it nicely fit into two cache lines. This makes lookups nearly free, even more + // so if repeated. + static uint8_t types[128] = { + // Core types. + ['c'] = kCFNumberCharType, + ['i'] = kCFNumberIntType, + ['s'] = kCFNumberShortType, + ['l'] = kCFNumberLongType, + ['q'] = kCFNumberLongLongType, + + // CFNumber (and NSNumber, actually) does not store unsigned types. + // This may cause some strange issues when dealing with values near the max for that type. + // We should investigate this if it becomes a problem. + ['C'] = kCFNumberCharType, + ['I'] = kCFNumberIntType, + ['S'] = kCFNumberShortType, + ['L'] = kCFNumberLongType, + ['Q'] = kCFNumberLongLongType, + + // Floating point + ['f'] = kCFNumberFloatType, + ['d'] = kCFNumberDoubleType, + + // C99 & CXX boolean + ['B'] = kCFNumberCharType, + }; + + return (CFNumberType)types[encodedType[0]]; +} + +@implementation PFObjectSubclassingController { + dispatch_queue_t _registeredSubclassesAccessQueue; + NSMutableDictionary *_registeredSubclasses; + NSMutableDictionary *_unregisteredSubclasses; +} + +static PFObjectSubclassingController *defaultController_; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _registeredSubclassesAccessQueue = dispatch_queue_create("com.parse.object.subclassing", DISPATCH_QUEUE_SERIAL); + _registeredSubclasses = [NSMutableDictionary dictionary]; + _unregisteredSubclasses = [NSMutableDictionary dictionary]; + + return self; +} + ++ (instancetype)defaultController { + if (!defaultController_) { + defaultController_ = [[PFObjectSubclassingController alloc] init]; + } + return defaultController_; +} + ++ (void)clearDefaultController { + defaultController_ = nil; +} + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + +- (Class)subclassForParseClassName:(NSString *)parseClassName { + __block Class result = nil; + pf_sync_with_throw(_registeredSubclassesAccessQueue, ^{ + result = [_registeredSubclasses[parseClassName] subclass]; + }); + return result; +} + +- (void)registerSubclass:(Class)kls { + pf_sync_with_throw(_registeredSubclassesAccessQueue, ^{ + [self _rawRegisterSubclass:kls]; + }); +} + +- (void)unregisterSubclass:(Class)class { + pf_sync_with_throw(_registeredSubclassesAccessQueue, ^{ + NSString *parseClassName = [class parseClassName]; + Class registeredClass = [_registeredSubclasses[parseClassName] subclass]; + + // Make it a no-op if the class itself is not registered or + // if there is another class registered under the same name. + if (registeredClass == nil || + ![registeredClass isEqual:class]) { + return; + } + + [_registeredSubclasses removeObjectForKey:parseClassName]; + }); +} + +- (BOOL)forwardObjectInvocation:(NSInvocation *)invocation withObject:(PFObject *)object { + PFObjectSubclassInfo *subclassInfo = [self _subclassInfoForClass:[object class]]; + + BOOL isSetter = NO; + PFPropertyInfo *propertyInfo = [subclassInfo propertyInfoForSelector:invocation.selector isSetter:&isSetter]; + if (!propertyInfo) { + return NO; + } + + if (isSetter) { + [self _forwardSetterInvocation:invocation forProperty:propertyInfo withObject:object]; + } else { + [self _forwardGetterInvocation:invocation forProperty:propertyInfo withObject:object]; + } + return YES; +} + +- (NSMethodSignature *)forwardingMethodSignatureForSelector:(SEL)cmd ofClass:(Class)kls { + PFObjectSubclassInfo *subclassInfo = [self _subclassInfoForClass:kls]; + return [subclassInfo forwardingMethodSignatureForSelector:cmd]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (void)_forwardGetterInvocation:(NSInvocation *)invocation + forProperty:(PFPropertyInfo *)propertyInfo + withObject:(PFObject *)object { + PFConsistencyAssert(invocation.methodSignature.numberOfArguments == 2, @"Getter should take no arguments!"); + PFConsistencyAssert(invocation.methodSignature.methodReturnType[0] != 'v', @"A getter cannot return void!"); + + const char *methodReturnType = [invocation.methodSignature methodReturnType]; + void *returnValueBytes = alloca([invocation.methodSignature methodReturnLength]); + + if (propertyInfo.ivar) { + object_getIvarValue_safe(object, propertyInfo.ivar, returnValueBytes, propertyInfo.associationType); + } else { + __autoreleasing id dictionaryValue = nil; + if ([propertyInfo.typeEncoding isEqualToString:@"@\"PFRelation\""]) { + dictionaryValue = [object relationForKey:propertyInfo.name]; + } else { + dictionaryValue = object[propertyInfo.name]; + + // TODO: (richardross) Investigate why we were orignally copying the result of -objectForKey, + // as this doens't seem right. + if (propertyInfo.associationType == PFPropertyInfoAssociationTypeCopy) { + dictionaryValue = [dictionaryValue copy]; + } + } + + if (dictionaryValue == nil || [dictionaryValue isKindOfClass:[NSNull class]]) { + memset(returnValueBytes, 0, invocation.methodSignature.methodReturnLength); + } else if (methodReturnType[0] == '@') { + memcpy(returnValueBytes, (void *) &dictionaryValue, sizeof(id)); + } else if ([dictionaryValue isKindOfClass:[NSNumber class]]) { + CFNumberGetValue((__bridge CFNumberRef) dictionaryValue, + PFNumberTypeForObjCType(methodReturnType), + returnValueBytes); + } else { + // TODO:(richardross)Support C-style structs that automatically convert to JSON via NSValue? + PFConsistencyAssert(false, @"Unsupported type encoding %s!", methodReturnType); + } + } + + [invocation setReturnValue:returnValueBytes]; +} + +- (void)_forwardSetterInvocation:(NSInvocation *)invocation + forProperty:(PFPropertyInfo *)propertyInfo + withObject:(PFObject *)object { + PFConsistencyAssert(invocation.methodSignature.numberOfArguments == 3, @"Setter should only take 1 argument!"); + + PFObject *sourceObject = object; + const char *argumentType = [invocation.methodSignature getArgumentTypeAtIndex:2]; + + NSUInteger argumentValueSize = 0; + NSGetSizeAndAlignment(argumentType, &argumentValueSize, NULL); + + void *argumentValueBytes = alloca(argumentValueSize); + [invocation getArgument:argumentValueBytes atIndex:2]; + + if (propertyInfo.ivar) { + object_setIvarValue_safe(sourceObject, propertyInfo.ivar, argumentValueBytes, propertyInfo.associationType); + } else { + id dictionaryValue = nil; + + if (argumentType[0] == '@') { + dictionaryValue = *(__unsafe_unretained id *)argumentValueBytes; + + if (propertyInfo.associationType == PFPropertyInfoAssociationTypeCopy) { + dictionaryValue = [dictionaryValue copy]; + } + } else { + CFNumberType numberType = PFNumberTypeForObjCType(argumentType); + PFConsistencyAssert(numberType != kCFNumberTypeUnknown, @"Unsupported type encoding %s!", argumentType); + + CFNumberRef number = CFNumberCreate(NULL, numberType, argumentValueBytes); + dictionaryValue = (__bridge_transfer id)number; + } + + if (dictionaryValue == nil) { + [sourceObject removeObjectForKey:propertyInfo.name]; + } else { + sourceObject[propertyInfo.name] = dictionaryValue; + } + } +} + +- (PFObjectSubclassInfo *)_subclassInfoForClass:(Class)kls { + __block PFObjectSubclassInfo *result = nil; + pf_sync_with_throw(_registeredSubclassesAccessQueue, ^{ + if (class_respondsToSelector(object_getClass(kls), @selector(parseClassName))) { + result = _registeredSubclasses[[kls parseClassName]]; + } + + // TODO: (nlutsenko, richardross) Don't let unregistered subclasses have dynamic property resolution. + if (!result) { + result = [PFObjectSubclassInfo subclassInfoWithSubclass:kls]; + _unregisteredSubclasses[NSStringFromClass(kls)] = result; + } + }); + return result; +} + +// Reverse compatibility note: many people may have built PFObject subclasses before +// we officially supported them. Our implementation can do cool stuff, but requires +// the parseClassName class method. +- (void)_rawRegisterSubclass:(Class)kls { + PFConsistencyAssert([kls conformsToProtocol:@protocol(PFSubclassing)], + @"Can only call +registerSubclass on subclasses conforming to PFSubclassing."); + + NSString *parseClassName = [kls parseClassName]; + + // Bug detection: don't allow subclasses of subclasses (i.e. custom user classes) + // to change the value of +parseClassName + if ([kls superclass] != [PFObject class]) { + // We compare Method definitions against the PFObject version witout invoking it + // because that Method could throw on an intermediary class which is + // not meant for direct use. + Method baseImpl = class_getClassMethod([PFObject class], @selector(parseClassName)); + Method superImpl = class_getClassMethod([kls superclass], @selector(parseClassName)); + + PFConsistencyAssert(superImpl == baseImpl || + [parseClassName isEqualToString:[[kls superclass] parseClassName]], + @"Subclasses of subclasses may not have separate +parseClassName " + "definitions. %@ should inherit +parseClassName from %@.", + kls, [kls superclass]); + } + + Class current = [_registeredSubclasses[parseClassName] subclass]; + if (current && current != kls) { + // We've already registered a more specific subclass (i.e. we're calling + // registerSubclass:PFUser after MYUser + if ([current isSubclassOfClass:kls]) { + return; + } + + PFConsistencyAssert([kls isSubclassOfClass:current], + @"Tried to register both %@ and %@ as the native PFObject subclass " + "of %@. Cannot determine the right class to use because neither " + "inherits from the other.", current, kls, parseClassName); + } + + // Move the subclass info from unregisteredSubclasses dictionary to registered ones, or create if it doesn't exist. + NSString *className = NSStringFromClass(kls); + PFObjectSubclassInfo *subclassInfo = _unregisteredSubclasses[className]; + if (subclassInfo) { + [_unregisteredSubclasses removeObjectForKey:className]; + } else { + subclassInfo = [PFObjectSubclassInfo subclassInfoWithSubclass:kls]; + } + _registeredSubclasses[[kls parseClassName]] = subclassInfo; +} + +@end diff --git a/Parse/Internal/Object/Utilities/PFObjectUtilities.h b/Parse/Internal/Object/Utilities/PFObjectUtilities.h new file mode 100644 index 000000000..a94952da2 --- /dev/null +++ b/Parse/Internal/Object/Utilities/PFObjectUtilities.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class PFFieldOperation; +@class PFOperationSet; + +@interface PFObjectUtilities : NSObject + +///-------------------------------------- +/// @name Operations +///-------------------------------------- + ++ (id)newValueByApplyingFieldOperation:(PFFieldOperation *)operation + toDictionary:(NSMutableDictionary *)dictionary + forKey:(NSString *)key; ++ (void)applyOperationSet:(PFOperationSet *)operationSet toDictionary:(NSMutableDictionary *)dictionary; + +///-------------------------------------- +/// @name Equality +///-------------------------------------- + ++ (BOOL)isObject:(nullable id)objectA equalToObject:(nullable id)objectB; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Object/Utilities/PFObjectUtilities.m b/Parse/Internal/Object/Utilities/PFObjectUtilities.m new file mode 100644 index 000000000..9b5f1e12b --- /dev/null +++ b/Parse/Internal/Object/Utilities/PFObjectUtilities.m @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectUtilities.h" + +#import "PFFieldOperation.h" +#import "PFOperationSet.h" + +@implementation PFObjectUtilities + +///-------------------------------------- +#pragma mark - Operations +///-------------------------------------- + ++ (id)newValueByApplyingFieldOperation:(PFFieldOperation *)operation + toDictionary:(NSMutableDictionary *)dictionary + forKey:(NSString *)key { + id oldValue = dictionary[key]; + id newValue = [operation applyToValue:oldValue forKey:key]; + if (newValue) { + dictionary[key] = newValue; + } else { + [dictionary removeObjectForKey:key]; + } + return newValue; +} + ++ (void)applyOperationSet:(PFOperationSet *)operationSet toDictionary:(NSMutableDictionary *)dictionary { + [operationSet enumerateKeysAndObjectsUsingBlock:^(NSString *key, PFFieldOperation *obj, BOOL *stop) { + [self newValueByApplyingFieldOperation:obj toDictionary:dictionary forKey:key]; + }]; +} + +///-------------------------------------- +#pragma mark - Equality +///-------------------------------------- + ++ (BOOL)isObject:(id)objectA equalToObject:(id)objectB { + return (objectA == objectB || (objectA != nil && [objectA isEqual:objectB])); +} + +@end diff --git a/Parse/Internal/PFAlertView.h b/Parse/Internal/PFAlertView.h new file mode 100644 index 000000000..ac678205b --- /dev/null +++ b/Parse/Internal/PFAlertView.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef void(^PFAlertViewCompletion)(NSUInteger selectedOtherButtonIndex); + +@interface PFAlertView : NSObject + ++ (void)showAlertWithTitle:(NSString *)title + message:(NSString *)message + cancelButtonTitle:(NSString *)cancelButtonTitle + otherButtonTitles:(NSArray *)otherButtonTitles + completion:(PFAlertViewCompletion)completion; + +@end diff --git a/Parse/Internal/PFAlertView.m b/Parse/Internal/PFAlertView.m new file mode 100644 index 000000000..0e542b0cd --- /dev/null +++ b/Parse/Internal/PFAlertView.m @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAlertView.h" + +#import + +@interface PFAlertView () + +@property (nonatomic, copy) PFAlertViewCompletion completion; + +@end + +@implementation PFAlertView + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (void)showAlertWithTitle:(NSString *)title + message:(NSString *)message + cancelButtonTitle:(NSString *)cancelButtonTitle + otherButtonTitles:(NSArray *)otherButtonTitles + completion:(PFAlertViewCompletion)completion { + if ([UIAlertController class] != nil) { + __block UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + void (^alertActionHandler)(UIAlertAction *) = [^(UIAlertAction *action){ + // This block intentionally retains alertController, and releases it afterwards. + NSUInteger index = [alertController.actions indexOfObject:action]; + completion(index - 1); + alertController = nil; + } copy]; + + [alertController addAction:[UIAlertAction actionWithTitle:cancelButtonTitle + style:UIAlertActionStyleCancel + handler:alertActionHandler]]; + + for (NSString *buttonTitle in otherButtonTitles) { + [alertController addAction:[UIAlertAction actionWithTitle:buttonTitle + style:UIAlertActionStyleDefault + handler:alertActionHandler]]; + } + + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + UIViewController *viewController = keyWindow.rootViewController; + + [viewController presentViewController:alertController animated:YES completion:nil]; + } else { + __block PFAlertView *pfAlertView = [[self alloc] init]; + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title + message:message + delegate:nil + cancelButtonTitle:cancelButtonTitle + otherButtonTitles:nil]; + + for (NSString *buttonTitle in otherButtonTitles) { + [alertView addButtonWithTitle:buttonTitle]; + } + + pfAlertView.completion = ^(NSUInteger index) { + if (completion) { + completion(index); + } + + pfAlertView = nil; + }; + + alertView.delegate = pfAlertView; + [alertView show]; + } +} + +///-------------------------------------- +#pragma mark - UIAlertViewDelegate +///-------------------------------------- + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + if (self.completion) { + self.completion(buttonIndex - alertView.firstOtherButtonIndex); + } +} + +@end diff --git a/Parse/Internal/PFApplication.h b/Parse/Internal/PFApplication.h new file mode 100644 index 000000000..fb87db63a --- /dev/null +++ b/Parse/Internal/PFApplication.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/*! + `PFApplication` class provides a centralized way to get the information about the current application, + or the environment it's running in. Please note, that all device specific things - should go to . + */ +@interface PFApplication : NSObject + +@property (nonatomic, assign, readonly, getter=isAppStoreEnvironment) BOOL appStoreEnvironment; +@property (nonatomic, assign, readonly, getter=isExtensionEnvironment) BOOL extensionEnvironment; + +@property (nonatomic, assign) NSInteger iconBadgeNumber; + ++ (instancetype)currentApplication; + +@end diff --git a/Parse/Internal/PFApplication.m b/Parse/Internal/PFApplication.m new file mode 100644 index 000000000..43e97aa00 --- /dev/null +++ b/Parse/Internal/PFApplication.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFApplication.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@implementation PFApplication + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)currentApplication { + static PFApplication *application; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + application = [[self alloc] init]; + }); + return application; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (BOOL)isAppStoreEnvironment { +#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + return ([[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"] == nil); +#endif + + return NO; +} + +- (BOOL)isExtensionEnvironment { + return [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]; +} + +- (NSInteger)iconBadgeNumber { +#if TARGET_OS_IPHONE + return [UIApplication sharedApplication].applicationIconBadgeNumber; +#else + // Make sure not to use `NSApp` here, because it doesn't work sometimes, + // `NSApplication +sharedApplication` does though. + NSString *badgeLabel = [[NSApplication sharedApplication] dockTile].badgeLabel; + if (badgeLabel.length == 0) { + return 0; + } + + NSScanner *scanner = [NSScanner localizedScannerWithString:badgeLabel]; + + NSInteger number = 0; + [scanner scanInteger:&number]; + if (scanner.scanLocation != badgeLabel.length) { + return 0; + } + + return number; +#endif +} + +- (void)setIconBadgeNumber:(NSInteger)iconBadgeNumber { + if (self.iconBadgeNumber != iconBadgeNumber) { +#if TARGET_OS_IPHONE + [UIApplication sharedApplication].applicationIconBadgeNumber = iconBadgeNumber; +#else + [[NSApplication sharedApplication] dockTile].badgeLabel = [@(iconBadgeNumber) stringValue]; +#endif + } +} + +@end diff --git a/Parse/Internal/PFAssert.h b/Parse/Internal/PFAssert.h new file mode 100644 index 000000000..35277e025 --- /dev/null +++ b/Parse/Internal/PFAssert.h @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMacros.h" + +#ifndef Parse_PFAssert_h +#define Parse_PFAssert_h + +/*! + Raises an `NSInvalidArgumentException` if the `condition` does not pass. + Use `description` to supply the way to fix the exception. + */ +#define PFParameterAssert(condition, description, ...) \ + do {\ + if (!(condition)) { \ + [NSException raise:NSInvalidArgumentException \ + format:description, ##__VA_ARGS__]; \ + } \ + } while(0) + +/*! + Raises an `NSRangeException` if the `condition` does not pass. + Use `description` to supply the way to fix the exception. + */ +#define PFRangeAssert(condition, description, ...) \ + do {\ + if (!(condition)) { \ + [NSException raise:NSRangeException \ + format:description, ##__VA_ARGS__]; \ + } \ +} while(0) + +/*! + Raises an `NSInternalInconsistencyException` if the `condition` does not pass. + Use `description` to supply the way to fix the exception. + */ +#define PFConsistencyAssert(condition, description, ...) \ + do { \ + if (!(condition)) { \ + [NSException raise:NSInternalInconsistencyException \ + format:description, ##__VA_ARGS__]; \ + } \ + } while(0) + +/*! + Always raises `NSInternalInconsistencyException` with details + about the method used and class that received the message + */ +#define PFNotDesignatedInitializer() \ +do { \ + PFConsistencyAssert(NO, \ + @"%@ is not the designated initializer for instances of %@.", \ + NSStringFromSelector(_cmd), \ + NSStringFromClass([self class])); \ + return nil; \ +} while (0) + +/*! + Raises `NSInternalInconsistencyException` if current thread is not main thread. + */ +#define PFAssertMainThread() \ +do { \ + PFConsistencyAssert([NSThread isMainThread], @"This method must be called on the main thread."); \ +} while (0) + +/*! + Raises `NSInternalInconsistencyException` if current thread is not the required one. + */ +#define PFAssertIsOnThread(thread) \ +do { \ + PFConsistencyAssert([NSThread currentThread] == thread, \ + @"This method must be called only on thread: %@.", thread); \ +} while (0) + +/*! + Raises `NSInternalInconsistencyException` if the current queue + is not the same as the queue provided. + Make sure you mark the queue first via `PFMarkDispatchQueue` + */ +#define PFAssertIsOnDispatchQueue(queue) \ +do { \ + void *mark = PFOSObjectPointer(queue); \ + PFConsistencyAssert(dispatch_get_specific(mark) == mark, \ + @"%s must be executed on %s", \ + __PRETTY_FUNCTION__, dispatch_queue_get_label(queue)); \ +} while (0) + +#endif diff --git a/Parse/Internal/PFAsyncTaskQueue.h b/Parse/Internal/PFAsyncTaskQueue.h new file mode 100644 index 000000000..45da5dc78 --- /dev/null +++ b/Parse/Internal/PFAsyncTaskQueue.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFAsyncTaskQueue : NSObject + ++ (instancetype)taskQueue; + +- (BFTask *)enqueue:(BFContinuationBlock)block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFAsyncTaskQueue.m b/Parse/Internal/PFAsyncTaskQueue.m new file mode 100644 index 000000000..2dae1672a --- /dev/null +++ b/Parse/Internal/PFAsyncTaskQueue.m @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAsyncTaskQueue.h" + +#import + +#import "BFTask+Private.h" + +@interface PFAsyncTaskQueue() + +@property (nonatomic, strong) dispatch_queue_t syncQueue; +@property (nonatomic, strong) BFTask *tail; + +@end + +@implementation PFAsyncTaskQueue + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _tail = [BFTask taskWithResult:nil]; + _syncQueue = dispatch_queue_create("com.parse.asynctaskqueue.sync", DISPATCH_QUEUE_SERIAL); + + return self; +} + ++ (instancetype)taskQueue { + return [[self alloc] init]; +} + +///-------------------------------------- +#pragma mark - Enqueue +///-------------------------------------- + +- (BFTask *)enqueue:(BFContinuationBlock)block { + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + dispatch_async(_syncQueue, ^{ + _tail = [_tail continueAsyncWithBlock:block]; + [_tail continueAsyncWithBlock:^id(BFTask *task) { + if (task.faulted) { + NSError *error = task.error; + if (error) { + [source trySetError:error]; + } else { + [source trySetException:task.exception]; + } + } else if (task.cancelled) { + [source trySetCancelled]; + } else { + [source trySetResult:task.result]; + } + return task; + }]; + }); + return source.task; +} + +@end diff --git a/Parse/Internal/PFBase64Encoder.h b/Parse/Internal/PFBase64Encoder.h new file mode 100644 index 000000000..4a7d44fb2 --- /dev/null +++ b/Parse/Internal/PFBase64Encoder.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFBase64Encoder : NSObject + ++ (NSData *)dataFromBase64String:(NSString *)string; ++ (NSString *)base64StringFromData:(NSData *)data; + +@end diff --git a/Parse/Internal/PFBase64Encoder.m b/Parse/Internal/PFBase64Encoder.m new file mode 100644 index 000000000..07fb5d086 --- /dev/null +++ b/Parse/Internal/PFBase64Encoder.m @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFBase64Encoder.h" + +@implementation PFBase64Encoder + ++ (NSData *)dataFromBase64String:(NSString *)string { + if (!string) { + return [NSData data]; + } + return [[NSData alloc] initWithBase64EncodedString:string options:NSDataBase64DecodingIgnoreUnknownCharacters]; +} + ++ (NSString *)base64StringFromData:(NSData *)data { + if (!data) { + return [NSString string]; + } + return [data base64EncodedStringWithOptions:0]; +} + +@end diff --git a/Parse/Internal/PFBaseState.h b/Parse/Internal/PFBaseState.h new file mode 100644 index 000000000..535403c18 --- /dev/null +++ b/Parse/Internal/PFBaseState.h @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef NS_ENUM(uint8_t, PFPropertyInfoAssociationType) { + PFPropertyInfoAssociationTypeDefault, // Assign for c-types, strong for objc-types. + PFPropertyInfoAssociationTypeAssign, + PFPropertyInfoAssociationTypeStrong, + PFPropertyInfoAssociationTypeWeak, + PFPropertyInfoAssociationTypeCopy, + PFPropertyInfoAssociationTypeMutableCopy, +}; + +@interface PFPropertyAttributes : NSObject + +@property (nonatomic, assign, readonly) PFPropertyInfoAssociationType associationType; + +- (instancetype)initWithAssociationType:(PFPropertyInfoAssociationType)associationType NS_DESIGNATED_INITIALIZER; + ++ (instancetype)attributes; ++ (instancetype)attributesWithAssociationType:(PFPropertyInfoAssociationType)associationType; + +@end + +@protocol PFBaseStateSubclass + +/*! + This is the list of properties that should be used automatically for the methods implemented by PFBaseState. + + It should be a dictionary in the format of @{ @"<#property name#>": [PFPropertyAttributes attributes] } + This will be automatically cached by PFBaseState, no need for you to cache it yourself. + + @return a dictionary of property attributes + */ ++ (NSDictionary *)propertyAttributes; + +@end + +/*! + Shared base class for all state objects. + Implements -init, -description, -debugDescription, -hash, -isEqual:, -compareTo:, etc. for you. + */ +@interface PFBaseState : NSObject + +- (instancetype)initWithState:(PFBaseState *)otherState; ++ (instancetype)stateWithState:(PFBaseState *)otherState; + +- (NSComparisonResult)compare:(PFBaseState *)other; + +/*! + Returns a dictionary representation of this object. + + Essentially, it takes the values for the keys of this object, and stuffs them in the dictionary. + It will call -dictionaryRepresentation on any objects it contains, in order to handle base states + contained in this base state. + + If a value is `nil`, it will be replaced with [NSNull null], to ensure all keys exist in the dictionary. + + If you don't like this behavior, you can overwrite the method + -nilValueForProperty:(NSString *) property + to return either nil to skip the key, or a value to use in it's place. + + @return A dictionary representation of this object state. + */ +- (NSDictionary *)dictionaryRepresentation; + +- (id)debugQuickLookObject; + +@end diff --git a/Parse/Internal/PFBaseState.m b/Parse/Internal/PFBaseState.m new file mode 100644 index 000000000..dc6af2283 --- /dev/null +++ b/Parse/Internal/PFBaseState.m @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFBaseState.h" + +#import +#import + +#import "PFAssert.h" +#import "PFHash.h" +#import "PFMacros.h" +#import "PFPropertyInfo.h" + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +@implementation PFPropertyAttributes + +- (instancetype)init { + return [self initWithAssociationType:PFPropertyInfoAssociationTypeDefault]; +} + +- (instancetype)initWithAssociationType:(PFPropertyInfoAssociationType)associationType { + self = [super init]; + if (!self) return nil; + + _associationType = associationType; + + return self; +} + ++ (instancetype)attributes { + return [[self alloc] init]; +} + ++ (instancetype)attributesWithAssociationType:(PFPropertyInfoAssociationType)associationType { + return [[self alloc] initWithAssociationType:associationType]; +} + +@end + +@interface PFBaseState () { + BOOL _initializing; +} + +@end + +@implementation PFBaseState + +///-------------------------------------- +#pragma mark - Property Info +///-------------------------------------- + ++ (NSSet *)_propertyInfo { + static void *_propertyMapKey = &_propertyMapKey; + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.parse.basestate.propertyinfo", DISPATCH_QUEUE_SERIAL); + }); + + __block NSMutableSet *results = nil; + dispatch_sync(queue, ^{ + results = objc_getAssociatedObject(self, _propertyMapKey); + if (results) { + return; + } + + NSDictionary *attributesMap = [(id)self propertyAttributes]; + results = [[NSMutableSet alloc] initWithCapacity:attributesMap.count]; + + [attributesMap enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [results addObject:[PFPropertyInfo propertyInfoWithClass:self + name:key + associationType:[obj associationType]]]; + }]; + + objc_setAssociatedObject(self, _propertyMapKey, results, OBJC_ASSOCIATION_RETAIN); + }); + + return results; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + // To prevent a recursive init function. + if (_initializing) { + return [super init]; + } + + _initializing = YES; + return [self initWithState:nil]; +} + +- (instancetype)initWithState:(id)otherState { + if (!_initializing) { + _initializing = YES; + + self = [self init]; + if (!self) return nil; + } + + NSSet *ourProperties = [[self class] _propertyInfo]; + NSSet *theirProperties = [[otherState class] _propertyInfo]; + + NSMutableSet *shared = [ourProperties mutableCopy]; + [shared intersectSet:theirProperties]; + + for (PFPropertyInfo *property in shared) { + [property takeValueFrom:otherState toObject:self]; + } + + return self; +} + ++ (instancetype)stateWithState:(PFBaseState *)otherState { + return [[self alloc] initWithState:otherState]; +} + +///-------------------------------------- +#pragma mark - Hashing +///-------------------------------------- + +- (NSUInteger)hash { + NSUInteger result = 0; + + for (PFPropertyInfo *property in [[self class] _propertyInfo]) { + result = PFIntegerPairHash(result, [[property getWrappedValueFrom:self] hash]); + } + + return result; +} + +///-------------------------------------- +#pragma mark - Comparison +///-------------------------------------- + +- (NSComparisonResult)compare:(PFBaseState *)other { + PFParameterAssert([other isKindOfClass:[PFBaseState class]], + @"Cannot compatre to an object that isn't a PFBaseState"); + + NSSet *ourProperties = [[self class] _propertyInfo]; + NSSet *theirProperties = [[other class] _propertyInfo]; + + NSMutableSet *shared = [ourProperties mutableCopy]; + [shared intersectSet:theirProperties]; + + for (PFPropertyInfo *info in shared) { + id ourValue = [info getWrappedValueFrom:self]; + id theirValue = [info getWrappedValueFrom:other]; + + if (![ourValue respondsToSelector:@selector(compare:)]) { + continue; + } + + NSComparisonResult result = [ourValue compare:theirValue]; + if (result != NSOrderedSame) { + return result; + } + } + + return NSOrderedSame; +} + +///-------------------------------------- +#pragma mark - Equality +///-------------------------------------- + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + + if (![other isKindOfClass:[PFBaseState class]]) { + return NO; + } + + NSSet *ourProperties = [[self class] _propertyInfo]; + NSSet *theirProperties = [[other class] _propertyInfo]; + + NSMutableSet *shared = [ourProperties mutableCopy]; + [shared intersectSet:theirProperties]; + + for (PFPropertyInfo *info in shared) { + id ourValue = [info getWrappedValueFrom:self]; + id theirValue = [info getWrappedValueFrom:other]; + + if (ourValue != theirValue && ![ourValue isEqual:theirValue]) { + return NO; + } + } + + return YES; +} + +///-------------------------------------- +#pragma mark - Description +///-------------------------------------- + +// This allows us to easily use the same implementation for description and debugDescription +- (NSString *)descriptionWithValueSelector:(SEL)toPerform { + NSMutableString *results = [NSMutableString stringWithFormat:@"<%@: %p", [self class], self]; + + for (PFPropertyInfo *property in [[self class] _propertyInfo]) { + id propertyValue = [property getWrappedValueFrom:self]; + NSString *propertyDescription = objc_msgSend_safe(NSString *)(propertyValue, toPerform); + + [results appendFormat:@", %@: %@", property.name, propertyDescription]; + } + + [results appendString:@">"]; + return results; +} + +- (NSString *)description { + return [self descriptionWithValueSelector:_cmd]; +} + +- (NSString *)debugDescription { + return [self descriptionWithValueSelector:_cmd]; +} + +///-------------------------------------- +#pragma mark - Dictionary/QuickLook representation +///-------------------------------------- + +- (id)nilValueForProperty:(NSString *)propertyName { + return [NSNull null]; +} + +// Implementation detail - this returns a mutable dictionary with mutable leaves. +- (NSDictionary *)dictionaryRepresentation { + NSSet *properties = [[self class] _propertyInfo]; + NSMutableDictionary *results = [[NSMutableDictionary alloc] initWithCapacity:properties.count]; + + for (PFPropertyInfo *info in properties) { + id value = [info getWrappedValueFrom:self]; + + if (value == nil) { + value = [self nilValueForProperty:info.name]; + + if (value == nil) { + continue; + } + } + + results[info.name] = value; + } + + return results; +} + +- (id)debugQuickLookObject { + return [[self dictionaryRepresentation] description]; +} + +@end diff --git a/Parse/Internal/PFBlockRetryer.h b/Parse/Internal/PFBlockRetryer.h new file mode 100644 index 000000000..17ba4ae7d --- /dev/null +++ b/Parse/Internal/PFBlockRetryer.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; + +NS_ASSUME_NONNULL_BEGIN + +typedef BFTask * __nonnull (^PFBlockRetryerBlock)(); + +/*! + This class will retry block for a specified number of times. + */ +@interface PFBlockRetryer : NSObject + +/*! + @abstract Runs the given block repeatedly. + + @discussion Runs the given block repeatedly until either: + - the block returns a successful task + - the block returns a cancelled task + - the block has been run attempts time. + After every run of the block, it waits twice as long as the previous time, + starting with the default initial delay. + + @returns `BFTask` which is the result of last run of the block. + */ ++ (BFTask *)retryBlock:(PFBlockRetryerBlock)block forAttempts:(NSUInteger)attempts; ++ (BFTask *)retryBlock:(PFBlockRetryerBlock)block forAttempts:(NSUInteger)attempts delay:(NSTimeInterval)delay; + +///-------------------------------------- +/// @name Initial Retry Delay +///-------------------------------------- + ++ (void)setInitialRetryDelay:(NSTimeInterval)delay; ++ (NSTimeInterval)initialRetryDelay; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFBlockRetryer.m b/Parse/Internal/PFBlockRetryer.m new file mode 100644 index 000000000..6e9971c89 --- /dev/null +++ b/Parse/Internal/PFBlockRetryer.m @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFBlockRetryer.h" + +#import + +#import "BFTask+Private.h" + +@implementation PFBlockRetryer + ++ (BFTask *)retryBlock:(PFBlockRetryerBlock)block forAttempts:(NSUInteger)attempts { + NSTimeInterval delay = initialRetryDelay_; + delay += initialRetryDelay_ * ((double)arc4random_uniform(0x0FFFF) / 0x0FFFF); + return [self retryBlock:block forAttempts:attempts delay:delay]; +} + ++ (BFTask *)retryBlock:(PFBlockRetryerBlock)block forAttempts:(NSUInteger)attempts delay:(NSTimeInterval)delay { + return [block() continueWithBlock:^id(BFTask *task) { + if (!task.error && !task.exception) { + return task; + } + + if (attempts <= 1) { + return task; + } + + return [[BFTask taskWithDelay:(int)(delay * 1000)] continueWithBlock:^id(BFTask *task) { + return [self retryBlock:block forAttempts:(attempts - 1)]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Delay +///-------------------------------------- + +static NSTimeInterval initialRetryDelay_ = 1.0; + ++ (void)setInitialRetryDelay:(NSTimeInterval)newDelay { + initialRetryDelay_ = newDelay; +} + ++ (NSTimeInterval)initialRetryDelay { + return initialRetryDelay_; +} + +@end diff --git a/Parse/Internal/PFCategoryLoader.h b/Parse/Internal/PFCategoryLoader.h new file mode 100644 index 000000000..9c298e259 --- /dev/null +++ b/Parse/Internal/PFCategoryLoader.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFCategoryLoader : NSObject + ++ (void)loadPrivateCategories; + +@end diff --git a/Parse/Internal/PFCategoryLoader.m b/Parse/Internal/PFCategoryLoader.m new file mode 100644 index 000000000..1a38f4024 --- /dev/null +++ b/Parse/Internal/PFCategoryLoader.m @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCategoryLoader.h" + +#import "BFTask+Private.h" + +@implementation PFCategoryLoader + ++ (void)loadPrivateCategories { + forceLoadCategory_BFTask_Private(); +} + +@end diff --git a/Parse/Internal/PFCommandCache.h b/Parse/Internal/PFCommandCache.h new file mode 100644 index 000000000..af5689ae1 --- /dev/null +++ b/Parse/Internal/PFCommandCache.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFEventuallyQueue.h" + +@class BFTask; +@class PFCommandCacheTestHelper; +@class PFObject; + +/*! + ParseCommandCache manages an on-disk cache of commands to be executed, and a thread with a standard run loop + that executes the commands. There should only ever be one instance of this class, because multiple instances + would be running separate threads trying to read and execute the same commands. + */ +@interface PFCommandCache : PFEventuallyQueue + +@property (nonatomic, copy, readonly) NSString *diskCachePath; +@property (nonatomic, assign, readonly) unsigned long long diskCacheSize; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +/*! + Creates the command cache object for all ParseObjects with default configuration. + This command cache is used to locally store save commands created by the [PFObject saveEventually]. + When a PFCommandCache is instantiated, it will begin running its run loop, + which will start by processing any commands already stored in the on-disk queue. + */ ++ (instancetype)newDefaultCommandCacheWithCommandRunner:(id)commandRunner + cacheFolderPath:(NSString *)cacheFolderPath; + +- (instancetype)initWithCommandRunner:(id)commandRunner + maxAttemptsCount:(NSUInteger)attemptsCount + retryInterval:(NSTimeInterval)retryInterval NS_UNAVAILABLE; + +- (instancetype)initWithCommandRunner:(id)commandRunner + maxAttemptsCount:(NSUInteger)attemptsCount + retryInterval:(NSTimeInterval)retryInterval + diskCachePath:(NSString *)diskCachePath + diskCacheSize:(unsigned long long)diskCacheSize NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Parse/Internal/PFCommandCache.m b/Parse/Internal/PFCommandCache.m new file mode 100644 index 000000000..6889964e2 --- /dev/null +++ b/Parse/Internal/PFCommandCache.m @@ -0,0 +1,330 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCommandCache.h" + +#include +#include + +#import +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCoreManager.h" +#import "PFErrorUtilities.h" +#import "PFEventuallyQueue_Private.h" +#import "PFFileManager.h" +#import "PFLogging.h" +#import "PFMacros.h" +#import "PFMultiProcessFileLockController.h" +#import "PFObject.h" +#import "PFObjectLocalIdStore.h" +#import "PFObjectPrivate.h" +#import "PFRESTCommand.h" +#import "Parse_Private.h" + +static NSString *const _PFCommandCacheDiskCacheDirectoryName = @"Command Cache"; + +static const NSString *PFCommandCachePrefixString = @"Command"; +static unsigned long long const PFCommandCacheDefaultDiskCacheSize = 10 * 1024 * 1024; // 10 MB + +@interface PFCommandCache () { + unsigned int _fileCounter; +} + +@property (nonatomic, assign, readwrite, setter=_setDiskCacheSize:) unsigned long long diskCacheSize; + +@end + +@implementation PFCommandCache + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)newDefaultCommandCacheWithCommandRunner:(id)commandRunner + cacheFolderPath:(NSString *)cacheFolderPath { + NSString *diskCachePath = [cacheFolderPath stringByAppendingPathComponent:_PFCommandCacheDiskCacheDirectoryName]; + diskCachePath = [diskCachePath stringByStandardizingPath]; + PFCommandCache *cache = [[PFCommandCache alloc] initWithCommandRunner:commandRunner + maxAttemptsCount:PFEventuallyQueueDefaultMaxAttemptsCount + retryInterval:PFEventuallyQueueDefaultTimeoutRetryInterval + diskCachePath:diskCachePath + diskCacheSize:PFCommandCacheDefaultDiskCacheSize]; + [cache start]; + return cache; +} + +- (instancetype)initWithCommandRunner:(id)commandRunner + maxAttemptsCount:(NSUInteger)attemptsCount + retryInterval:(NSTimeInterval)retryInterval { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommandRunner:(id)commandRunner + maxAttemptsCount:(NSUInteger)attemptsCount + retryInterval:(NSTimeInterval)retryInterval + diskCachePath:(NSString *)diskCachePath + diskCacheSize:(unsigned long long)diskCacheSize { + self = [super initWithCommandRunner:commandRunner maxAttemptsCount:attemptsCount retryInterval:retryInterval]; + if (!self) return nil; + + _diskCachePath = diskCachePath; + _diskCacheSize = diskCacheSize; + _fileCounter = 0; + + [self _createDiskCachePathIfNeeded]; + + return self; +} + +///-------------------------------------- +#pragma mark - Controlling Queue +///-------------------------------------- + +- (void)removeAllCommands { + [self pause]; + + [super removeAllCommands]; + + NSArray *commandIdentifiers = [self _pendingCommandIdentifiers]; + NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:[commandIdentifiers count]]; + + for (NSString *identifier in commandIdentifiers) { + BFTask *task = [self _removeFileForCommandWithIdentifier:identifier]; + [tasks addObject:task]; + } + + [[BFTask taskForCompletionOfAllTasks:tasks] waitUntilFinished]; + + [self resume]; +} + +///-------------------------------------- +#pragma mark - PFEventuallyQueue +///-------------------------------------- + +- (void)_simulateReboot { + [super _simulateReboot]; + [self _createDiskCachePathIfNeeded]; +} + +///-------------------------------------- +#pragma mark - PFEventuallyQueueSubclass +///-------------------------------------- + +- (NSString *)_newIdentifierForCommand:(id)command { + // Start with current time - so we can sort identifiers and get the oldest one first. + return [NSString stringWithFormat:@"%@-%016qx-%08x-%@", + PFCommandCachePrefixString, + (unsigned long long)[NSDate timeIntervalSinceReferenceDate], + _fileCounter++, + [[NSUUID UUID] UUIDString]]; +} + +- (NSArray *)_pendingCommandIdentifiers { + NSArray *result = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.diskCachePath error:nil]; + // Only accept files that starts with "Command" since sometimes the directory is filled with garbage + // e.g.: https://phab.parse.com/file/info/PHID-FILE-qgbwk7sm7kcyaks6n4j7/ + result = [result filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF BEGINSWITH %@", PFCommandCachePrefixString]]; + + return [result sortedArrayUsingSelector:@selector(compare:)]; +} + +- (id)_commandWithIdentifier:(NSString *)identifier error:(NSError **)error { + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:self.diskCachePath]; + + NSError *innerError = nil; + NSData *jsonData = [NSData dataWithContentsOfFile:[self _filePathForCommandWithIdentifier:identifier] + options:NSDataReadingUncached + error:&innerError]; + + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:self.diskCachePath]; + + if (innerError || !jsonData) { + NSString *message = [NSString stringWithFormat:@"Failed to read command from cache. %@", + innerError ? [innerError localizedDescription] : @""]; + innerError = [PFErrorUtilities errorWithCode:kPFErrorInternalServer + message:message]; + if (error) { + *error = innerError; + } + return nil; + } + + id jsonObject = [NSJSONSerialization JSONObjectWithData:jsonData + options:0 + error:&innerError]; + if (innerError) { + NSString *message = [NSString stringWithFormat:@"Failed to deserialiaze command from cache. %@", + [innerError localizedDescription]]; + innerError = [PFErrorUtilities errorWithCode:kPFErrorInternalServer + message:message]; + } else { + if ([PFRESTCommand isValidDictionaryRepresentation:jsonObject]) { + return [PFRESTCommand commandFromDictionaryRepresentation:jsonObject]; + } + innerError = [PFErrorUtilities errorWithCode:kPFErrorInternalServer + message:@"Failed to construct eventually command from cache." + shouldLog:NO]; + } + if (innerError && error) { + *error = innerError; + } + + return nil; +} + +- (BFTask *)_enqueueCommandInBackground:(id)command + object:(PFObject *)object + identifier:(NSString *)identifier { + return [self _saveCommandToCacheInBackground:command object:object identifier:identifier]; +} + +- (BFTask *)_didFinishRunningCommand:(id)command + withIdentifier:(NSString *)identifier + resultTask:(BFTask *)resultTask { + // Get the new objectId and mark the new localId so it can be resolved. + if (command.localId) { + NSDictionary *dictionaryResult = nil; + if ([resultTask.result isKindOfClass:[NSDictionary class]]) { + dictionaryResult = resultTask.result; + } else if ([resultTask.result isKindOfClass:[PFCommandResult class]]) { + PFCommandResult *commandResult = resultTask.result; + dictionaryResult = commandResult.result; + } + + if (dictionaryResult != nil) { + NSString *objectId = dictionaryResult[@"objectId"]; + if (objectId) { + [[Parse _currentManager].coreManager.objectLocalIdStore setObjectId:objectId forLocalId:command.localId]; + } + } + } + + [[self _removeFileForCommandWithIdentifier:identifier] waitUntilFinished]; + return [super _didFinishRunningCommand:command withIdentifier:identifier resultTask:resultTask]; +} + +- (BFTask *)_waitForOperationSet:(PFOperationSet *)operationSet eventuallyPin:(PFEventuallyPin *)eventuallyPin { + // Do nothing. This is only relevant in PFPinningEventuallyQueue. Looks super hacky you said? Yes it is! + return [BFTask taskWithResult:nil]; +} + +///-------------------------------------- +#pragma mark - Disk Cache +///-------------------------------------- + +- (BFTask *)_cleanupDiskCacheWithRequiredFreeSize:(NSUInteger)requiredSize { + return [BFTask taskFromExecutor:[BFExecutor defaultExecutor] withBlock:^id{ + NSUInteger size = requiredSize; + + NSMutableDictionary *commandSizes = [NSMutableDictionary dictionary]; + + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:self.diskCachePath]; + NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:self.diskCachePath]; + + NSString *identifier = nil; + while ((identifier = [enumerator nextObject])) { + NSNumber *fileSize = [enumerator fileAttributes][NSFileSize]; + if (fileSize) { + commandSizes[identifier] = fileSize; + size += [fileSize unsignedIntegerValue]; + } + } + + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:self.diskCachePath]; + + if (size > self.diskCacheSize) { + // Get identifiers and sort them to remove oldest commands first + NSArray *identifiers = [[commandSizes allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *identifier in identifiers) @autoreleasepool { + [self _removeFileForCommandWithIdentifier:identifier]; + size -= [commandSizes[identifier] unsignedIntegerValue]; + + if (size <= self.diskCacheSize) { + break; + } + [commandSizes removeObjectForKey:identifier]; + } + } + + return [BFTask taskWithResult:nil]; + }]; +} + +- (void)_setDiskCacheSize:(unsigned long long)diskCacheSize { + _diskCacheSize = diskCacheSize; +} + +///-------------------------------------- +#pragma mark - Files +///-------------------------------------- + +- (BFTask *)_saveCommandToCacheInBackground:(id)command + object:(PFObject *)object + identifier:(NSString *)identifier { + if (object != nil && object.objectId == nil) { + command.localId = [object getOrCreateLocalId]; + } + + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:[command dictionaryRepresentation] + options:0 + error:&error]; + NSUInteger commandSize = [data length]; + if (commandSize > self.diskCacheSize) { + error = [PFErrorUtilities errorWithCode:kPFErrorInternalServer + message:@"Failed to run command, because it's too big."]; + } else if (commandSize == 0) { + error = [PFErrorUtilities errorWithCode:kPFErrorInternalServer + message:@"Failed to run command, because it's empty."]; + } + + if (error) { + return [BFTask taskWithError:error]; + } + + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:self.diskCachePath]; + return [[[self _cleanupDiskCacheWithRequiredFreeSize:commandSize] continueWithBlock:^id(BFTask *task) { + NSString *filePath = [self _filePathForCommandWithIdentifier:identifier]; + return [PFFileManager writeDataAsync:data toFile:filePath]; + }] continueWithBlock:^id(BFTask *task) { + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:self.diskCachePath]; + return task; + }]; + }]; +} + +- (BFTask *)_removeFileForCommandWithIdentifier:(NSString *)identifier { + NSString *filePath = [self _filePathForCommandWithIdentifier:identifier]; + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:self.diskCachePath]; + return [PFFileManager removeItemAtPathAsync:filePath withFileLock:NO]; + }] continueWithBlock:^id(BFTask *task) { + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:self.diskCachePath]; + return task; // Roll-forward the previous task. + }]; +} + +- (NSString *)_filePathForCommandWithIdentifier:(NSString *)identifier { + return [self.diskCachePath stringByAppendingPathComponent:identifier]; +} + +- (void)_createDiskCachePathIfNeeded { + [[PFFileManager createDirectoryIfNeededAsyncAtPath:_diskCachePath] waitForResult:nil withMainThreadWarning:NO]; +} + +@end diff --git a/Parse/Internal/PFCommandCache_Private.h b/Parse/Internal/PFCommandCache_Private.h new file mode 100644 index 000000000..869ce66a2 --- /dev/null +++ b/Parse/Internal/PFCommandCache_Private.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCommandCache.h" + +@interface PFCommandCache () + +- (void)_setDiskCacheSize:(unsigned long long)diskCacheSize; + +@end; diff --git a/Parse/Internal/PFCommandResult.h b/Parse/Internal/PFCommandResult.h new file mode 100644 index 000000000..a87d6670e --- /dev/null +++ b/Parse/Internal/PFCommandResult.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFCommandResult : NSObject + +@property (nonatomic, strong, readonly) id result; +@property (nullable, nonatomic, copy, readonly) NSString *resultString; +@property (nullable, nonatomic, strong, readonly) NSHTTPURLResponse *httpResponse; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithResult:(NSDictionary *)result + resultString:(nullable NSString *)resultString + httpResponse:(nullable NSHTTPURLResponse *)response NS_DESIGNATED_INITIALIZER; ++ (instancetype)commandResultWithResult:(NSDictionary *)result + resultString:(nullable NSString *)resultString + httpResponse:(nullable NSHTTPURLResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFCommandResult.m b/Parse/Internal/PFCommandResult.m new file mode 100644 index 000000000..e83f2c3c7 --- /dev/null +++ b/Parse/Internal/PFCommandResult.m @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCommandResult.h" + +#import "PFAssert.h" + +@implementation PFCommandResult + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithResult:(NSDictionary *)result + resultString:(NSString *)resultString + httpResponse:(NSHTTPURLResponse *)response { + self = [super init]; + if (!self) return nil; + + _result = result; + _resultString = [resultString copy]; + _httpResponse = response; + + return self; +} + ++ (instancetype)commandResultWithResult:(NSDictionary *)result + resultString:(NSString *)resultString + httpResponse:(NSHTTPURLResponse *)response { + return [[self alloc] initWithResult:result resultString:resultString httpResponse:response]; +} + +@end diff --git a/Parse/Internal/PFCoreDataProvider.h b/Parse/Internal/PFCoreDataProvider.h new file mode 100644 index 000000000..021e9749e --- /dev/null +++ b/Parse/Internal/PFCoreDataProvider.h @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef Parse_PFCoreDataProvider_h +#define Parse_PFCoreDataProvider_h + +NS_ASSUME_NONNULL_BEGIN + +///-------------------------------------- +/// @name Object +///-------------------------------------- + +@class PFObjectController; + +@protocol PFObjectControllerProvider + +@property (nonatomic, strong) PFObjectController *objectController; + +@end + +@class PFObjectBatchController; + +@protocol PFObjectBatchController + +@property (nonatomic, strong, readonly) PFObjectBatchController *objectBatchController; + +@end + +@class PFObjectFilePersistenceController; + +@protocol PFObjectFilePersistenceControllerProvider + +@property (nonatomic, strong, readonly) PFObjectFilePersistenceController *objectFilePersistenceController; + +@end + +@class PFObjectLocalIdStore; + +@protocol PFObjectLocalIdStoreProvider + +@property (nonatomic, strong) PFObjectLocalIdStore *objectLocalIdStore; + +@end + +///-------------------------------------- +/// @name User +///-------------------------------------- + +@class PFUserAuthenticationController; + +@protocol PFUserAuthenticationControllerProvider + +@property (nonatomic, strong) PFUserAuthenticationController *userAuthenticationController; + +@end + +@class PFCurrentUserController; + +@protocol PFCurrentUserControllerProvider + +@property (nonatomic, strong) PFCurrentUserController *currentUserController; + +@end + +@class PFUserController; + +@protocol PFUserControllerProvider + +@property (nonatomic, strong) PFUserController *userController; + +@end + +///-------------------------------------- +/// @name Installation +///-------------------------------------- + +@class PFCurrentInstallationController; + +@protocol PFCurrentInstallationControllerProvider + +@property (nonatomic, strong) PFCurrentInstallationController *currentInstallationController; + +@end + +@class PFInstallationController; + +@protocol PFInstallationControllerProvider + +@property (nonatomic, strong) PFInstallationController *installationController; + +@end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFCoreManager.h b/Parse/Internal/PFCoreManager.h new file mode 100644 index 000000000..19e2fcad9 --- /dev/null +++ b/Parse/Internal/PFCoreManager.h @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreDataProvider.h" +#import "PFDataProvider.h" + +@class PFInstallationIdentifierStore; + +NS_ASSUME_NONNULL_BEGIN + +@protocol PFCoreManagerDataSource + + +@property (nonatomic, strong, readonly) PFInstallationIdentifierStore *installationIdentifierStore; + +@end + +@class PFCloudCodeController; +@class PFConfigController; +@class PFFileController; +@class PFObjectFilePersistenceController; +@class PFObjectSubclassingController; +@class PFPinningObjectStore; +@class PFQueryController; +@class PFSessionController; + +@interface PFCoreManager : NSObject + + +@property (nonatomic, weak, readonly) id dataSource; + +@property (nonatomic, strong) PFQueryController *queryController; +@property (nonatomic, strong) PFFileController *fileController; +@property (nonatomic, strong) PFCloudCodeController *cloudCodeController; +@property (nonatomic, strong) PFConfigController *configController; +@property (nonatomic, strong) PFSessionController *sessionController; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; + ++ (instancetype)managerWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name ObjectFilePersistenceController +///-------------------------------------- + +- (void)unloadObjectFilePersistenceController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFCoreManager.m b/Parse/Internal/PFCoreManager.m new file mode 100644 index 000000000..3284924ac --- /dev/null +++ b/Parse/Internal/PFCoreManager.m @@ -0,0 +1,439 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCoreManager.h" + +#import "PFAssert.h" +#import "PFCachedQueryController.h" +#import "PFCloudCodeController.h" +#import "PFConfigController.h" +#import "PFCurrentInstallationController.h" +#import "PFCurrentUserController.h" +#import "PFFileController.h" +#import "PFInstallationController.h" +#import "PFLocationManager.h" +#import "PFMacros.h" +#import "PFObjectBatchController.h" +#import "PFObjectController.h" +#import "PFObjectFilePersistenceController.h" +#import "PFObjectLocalIdStore.h" +#import "PFObjectSubclassingController.h" +#import "PFOfflineObjectController.h" +#import "PFOfflineQueryController.h" +#import "PFPinningObjectStore.h" +#import "PFSessionController.h" +#import "PFUserAuthenticationController.h" +#import "PFUserController.h" + +@interface PFCoreManager () { + dispatch_queue_t _locationManagerAccessQueue; + dispatch_queue_t _controllerAccessQueue; + dispatch_queue_t _objectLocalIdStoreAccessQueue; +} + +@end + +@implementation PFCoreManager + +@synthesize locationManager = _locationManager; + +@synthesize queryController = _queryController; +@synthesize fileController = _fileController; +@synthesize cloudCodeController = _cloudCodeController; +@synthesize configController = _configController; +@synthesize objectController = _objectController; +@synthesize objectBatchController = _objectBatchController; +@synthesize objectFilePersistenceController = _objectFilePersistenceController; +@synthesize objectLocalIdStore = _objectLocalIdStore; +@synthesize pinningObjectStore = _pinningObjectStore; +@synthesize userAuthenticationController = _userAuthenticationController; +@synthesize sessionController = _sessionController; +@synthesize currentInstallationController = _currentInstallationController; +@synthesize currentUserController = _currentUserController; +@synthesize userController = _userController; +@synthesize installationController = _installationController; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + _locationManagerAccessQueue = dispatch_queue_create("com.parse.core.locationManager", DISPATCH_QUEUE_SERIAL); + _controllerAccessQueue = dispatch_queue_create("com.parse.core.controller.accessQueue", DISPATCH_QUEUE_SERIAL); + _objectLocalIdStoreAccessQueue = dispatch_queue_create("com.parse.core.object.localIdStore", DISPATCH_QUEUE_SERIAL); + + return self; +} + ++ (instancetype)managerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - LocationManager +///-------------------------------------- + +- (PFLocationManager *)locationManager { + __block PFLocationManager *manager; + dispatch_sync(_locationManagerAccessQueue, ^{ + if (!_locationManager) { + _locationManager = [[PFLocationManager alloc] init]; + } + manager = _locationManager; + }); + return manager; +} + +///-------------------------------------- +#pragma mark - QueryController +///-------------------------------------- + +- (PFQueryController *)queryController { + __block PFQueryController *queryController; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_queryController) { + id dataSource = self.dataSource; + if (dataSource.offlineStoreLoaded) { + _queryController = [PFOfflineQueryController controllerWithCommonDataSource:dataSource + coreDataSource:self]; + } else { + _queryController = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + } + } + queryController = _queryController; + }); + return queryController; +} + +- (void)setQueryController:(PFQueryController *)queryController { + dispatch_sync(_controllerAccessQueue, ^{ + _queryController = queryController; + }); +} + +///-------------------------------------- +#pragma mark - FileController +///-------------------------------------- + +- (PFFileController *)fileController { + __block PFFileController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_fileController) { + _fileController = [PFFileController controllerWithDataSource:self.dataSource]; + } + controller = _fileController; + }); + return controller; +} + +- (void)setFileController:(PFFileController *)fileController { + dispatch_sync(_controllerAccessQueue, ^{ + _fileController = fileController; + }); +} + +///-------------------------------------- +#pragma mark - CloudCodeController +///-------------------------------------- + +- (PFCloudCodeController *)cloudCodeController { + __block PFCloudCodeController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_cloudCodeController) { + _cloudCodeController = [[PFCloudCodeController alloc] initWithCommandRunner:self.dataSource.commandRunner]; + } + controller = _cloudCodeController; + }); + return controller; +} + +- (void)setCloudCodeController:(PFCloudCodeController *)cloudCodeController { + dispatch_sync(_controllerAccessQueue, ^{ + _cloudCodeController = cloudCodeController; + }); +} + +///-------------------------------------- +#pragma mark - ConfigController +///-------------------------------------- + +- (PFConfigController *)configController { + __block PFConfigController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_configController) { + id dataSource = self.dataSource; + _configController = [[PFConfigController alloc] initWithFileManager:dataSource.fileManager + commandRunner:dataSource.commandRunner]; + } + controller = _configController; + }); + return controller; +} + +- (void)setConfigController:(PFConfigController *)configController { + dispatch_sync(_controllerAccessQueue, ^{ + _configController = configController; + }); +} + +///-------------------------------------- +#pragma mark - ObjectController +///-------------------------------------- + +- (PFObjectController *)objectController { + __block PFObjectController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_objectController) { + id dataSource = self.dataSource; + if (dataSource.offlineStoreLoaded) { + _objectController = [PFOfflineObjectController controllerWithDataSource:dataSource]; + } else { + _objectController = [PFObjectController controllerWithDataSource:dataSource]; + } + } + controller = _objectController; + }); + return controller; +} + +- (void)setObjectController:(PFObjectController *)controller { + dispatch_sync(_controllerAccessQueue, ^{ + _objectController = controller; + }); +} + +///-------------------------------------- +#pragma mark - ObjectBatchController +///-------------------------------------- + +- (PFObjectBatchController *)objectBatchController { + __block PFObjectBatchController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_objectBatchController) { + _objectBatchController = [PFObjectBatchController controllerWithDataSource:self.dataSource]; + } + controller = _objectBatchController; + }); + return controller; +} + +///-------------------------------------- +#pragma mark - ObjectFilePersistenceController +///-------------------------------------- + +- (PFObjectFilePersistenceController *)objectFilePersistenceController { + __block PFObjectFilePersistenceController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_objectFilePersistenceController) { + _objectFilePersistenceController = [PFObjectFilePersistenceController controllerWithDataSource:self.dataSource]; + } + controller = _objectFilePersistenceController; + }); + return controller; +} + +- (void)unloadObjectFilePersistenceController { + dispatch_sync(_controllerAccessQueue, ^{ + _objectFilePersistenceController = nil; + }); +} + +///-------------------------------------- +#pragma mark - Pinning Object Store +///-------------------------------------- + +- (PFPinningObjectStore *)pinningObjectStore { + __block PFPinningObjectStore *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_pinningObjectStore) { + _pinningObjectStore = [PFPinningObjectStore storeWithDataSource:self.dataSource]; + } + controller = _pinningObjectStore; + }); + return controller; +} + +- (void)setPinningObjectStore:(PFPinningObjectStore *)pinningObjectStore { + dispatch_sync(_controllerAccessQueue, ^{ + _pinningObjectStore = pinningObjectStore; + }); +} + +///-------------------------------------- +#pragma mark - Object LocalId Store +///-------------------------------------- + +- (PFObjectLocalIdStore *)objectLocalIdStore { + __block PFObjectLocalIdStore *store = nil; + @weakify(self); + dispatch_sync(_objectLocalIdStoreAccessQueue, ^{ + @strongify(self); + if (!_objectLocalIdStore) { + _objectLocalIdStore = [[PFObjectLocalIdStore alloc] initWithDataSource:self.dataSource]; + } + store = _objectLocalIdStore; + }); + return store; +} + +- (void)setObjectLocalIdStore:(PFObjectLocalIdStore *)objectLocalIdStore { + dispatch_sync(_objectLocalIdStoreAccessQueue, ^{ + _objectLocalIdStore = objectLocalIdStore; + }); +} + +///-------------------------------------- +#pragma mark - UserAuthenticationController +///-------------------------------------- + +- (PFUserAuthenticationController *)userAuthenticationController { + __block PFUserAuthenticationController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_userAuthenticationController) { + _userAuthenticationController = [[PFUserAuthenticationController alloc] init]; + } + controller = _userAuthenticationController; + }); + return controller; +} + +- (void)setUserAuthenticationController:(PFUserAuthenticationController *)userAuthenticationController { + dispatch_sync(_controllerAccessQueue, ^{ + _userAuthenticationController = userAuthenticationController; + }); +} + +///-------------------------------------- +#pragma mark - SessionController +///-------------------------------------- + +- (PFSessionController *)sessionController { + __block PFSessionController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_sessionController) { + _sessionController = [PFSessionController controllerWithDataSource:self.dataSource]; + } + controller = _sessionController; + }); + return controller; +} + +- (void)setSessionController:(PFSessionController *)sessionController { + dispatch_sync(_controllerAccessQueue, ^{ + _sessionController = sessionController; + }); +} + +///-------------------------------------- +#pragma mark - Current Installation Controller +///-------------------------------------- + +- (PFCurrentInstallationController *)currentInstallationController { + __block PFCurrentInstallationController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_currentInstallationController) { + id dataSource = self.dataSource; + PFCurrentObjectStorageType storageType = (dataSource.offlineStore ? + PFCurrentObjectStorageTypeOfflineStore : + PFCurrentObjectStorageTypeFile); + _currentInstallationController = [PFCurrentInstallationController controllerWithStorageType:storageType + commonDataSource:dataSource + coreDataSource:self]; + } + controller = _currentInstallationController; + }); + return controller; +} + +- (void)setCurrentInstallationController:(PFCurrentInstallationController *)controller { + dispatch_sync(_controllerAccessQueue, ^{ + _currentInstallationController = controller; + }); +} + +///-------------------------------------- +#pragma mark - Current User Controller +///-------------------------------------- + +- (PFCurrentUserController *)currentUserController { + __block PFCurrentUserController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_currentUserController) { + id dataSource = self.dataSource; + PFCurrentObjectStorageType storageType = (dataSource.offlineStore ? + PFCurrentObjectStorageTypeOfflineStore : + PFCurrentObjectStorageTypeFile); + _currentUserController = [PFCurrentUserController controllerWithStorageType:storageType + commonDataSource:dataSource + coreDataSource:self]; + } + controller = _currentUserController; + }); + return controller; +} + +- (void)setCurrentUserController:(PFCurrentUserController *)currentUserController { + dispatch_sync(_controllerAccessQueue, ^{ + _currentUserController = currentUserController; + }); +} + +///-------------------------------------- +#pragma mark - Installation Controller +///-------------------------------------- + +- (PFInstallationController *)installationController { + __block PFInstallationController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_installationController) { + _installationController = [PFInstallationController controllerWithDataSource:self]; + } + controller = _installationController; + }); + return controller; +} + +- (void)setInstallationController:(PFInstallationController *)installationController { + dispatch_sync(_controllerAccessQueue, ^{ + _installationController = installationController; + }); +} + +///-------------------------------------- +#pragma mark - User Controller +///-------------------------------------- + +- (PFUserController *)userController { + __block PFUserController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_installationController) { + _userController = [PFUserController controllerWithCommonDataSource:self.dataSource + coreDataSource:self]; + } + controller = _userController; + }); + return controller; +} + +- (void)setUserController:(PFUserController *)userController { + dispatch_sync(_controllerAccessQueue, ^{ + _userController = userController; + }); +} + +@end diff --git a/Parse/Internal/PFDataProvider.h b/Parse/Internal/PFDataProvider.h new file mode 100644 index 000000000..3813c4d16 --- /dev/null +++ b/Parse/Internal/PFDataProvider.h @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef Parse_PFDataProviders_h +#define Parse_PFDataProviders_h + +NS_ASSUME_NONNULL_BEGIN + +@protocol PFCommandRunning; + +@protocol PFCommandRunnerProvider + +@property (nonatomic, strong, readonly) id commandRunner; + +@end + +@class PFFileManager; + +@protocol PFFileManagerProvider + +@property (nonatomic, strong, readonly) PFFileManager *fileManager; + +@end + +@class PFOfflineStore; + +@protocol PFOfflineStoreProvider + +@property (nullable, nonatomic, strong) PFOfflineStore *offlineStore; +@property (nonatomic, assign, readonly, getter=isOfflineStoreLoaded) BOOL offlineStoreLoaded; + +@end + +@class PFEventuallyQueue; + +@protocol PFEventuallyQueueProvider + +@property (nonatomic, strong, readonly) PFEventuallyQueue *eventuallyQueue; + +@end + +@class PFKeychainStore; + +@protocol PFKeychainStoreProvider + +@property (nonatomic, strong, readonly) PFKeychainStore *keychainStore; + +@end + +@class PFKeyValueCache; + +@protocol PFKeyValueCacheProvider + +@property (nonatomic, strong, readonly) PFKeyValueCache *keyValueCache; + +@end + +@class PFLocationManager; + +@protocol PFLocationManagerProvider + +@property (nonatomic, strong, readonly) PFLocationManager *locationManager; + +@end + +@class PFPinningObjectStore; + +@protocol PFPinningObjectStoreProvider + +@property (nonatomic, strong) PFPinningObjectStore *pinningObjectStore; + +@end + +@class PFInstallationIdentifierStore; + +@protocol PFInstallationIdentifierStoreProvider + +@property (nonatomic, strong, readonly) PFInstallationIdentifierStore *installationIdentifierStore; + +@end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFDateFormatter.h b/Parse/Internal/PFDateFormatter.h new file mode 100644 index 000000000..d258e7695 --- /dev/null +++ b/Parse/Internal/PFDateFormatter.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFDateFormatter : NSObject + ++ (instancetype)sharedFormatter; + +///-------------------------------------- +/// @name String from Date +///-------------------------------------- + +/*! + Converts `NSDate` into `NSString` representation using the following format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + + @param date `NSDate` to convert. + + @returns Formatted `NSString` representation. + */ +- (NSString *)preciseStringFromDate:(NSDate *)date; + +///-------------------------------------- +/// @name Date from String +///-------------------------------------- + +/*! + Converts `NSString` representation of a date into `NSDate` object. + + @discussion Following date formats are supported: + YYYY-MM-DD + YYYY-MM-DD HH:MM'Z' + YYYY-MM-DD HH:MM:SS'Z' + YYYY-MM-DD HH:MM:SS.SSS'Z' + YYYY-MM-DDTHH:MM'Z' + YYYY-MM-DDTHH:MM:SS'Z' + YYYY-MM-DDTHH:MM:SS.SSS'Z' + + @param string `NSString` representation to convert. + + @returns `NSDate` incapsulating the date. + */ +- (NSDate *)dateFromString:(NSString *)string; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFDateFormatter.m b/Parse/Internal/PFDateFormatter.m new file mode 100644 index 000000000..c4a6ba267 --- /dev/null +++ b/Parse/Internal/PFDateFormatter.m @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDateFormatter.h" + +#import + +@interface PFDateFormatter () { + dispatch_queue_t _synchronizationQueue; + + sqlite3 *_sqliteDatabase; + sqlite3_stmt *_stringToDateStatement; +} + +@property (nonatomic, strong, readonly) NSDateFormatter *preciseDateFormatter; + +@end + +@implementation PFDateFormatter + +@synthesize preciseDateFormatter = _preciseDateFormatter; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)sharedFormatter { + static PFDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[self alloc] init]; + }); + return formatter; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _synchronizationQueue = dispatch_queue_create("com.parse.dateFormatter", DISPATCH_QUEUE_SERIAL); + + //TODO: (nlutsenko) Check for error here. + sqlite3_open(":memory:", &_sqliteDatabase); + sqlite3_prepare_v2(_sqliteDatabase, + "SELECT strftime('%s', ?), strftime('%f', ?);", + -1, + &_stringToDateStatement, + NULL); + + return self; +} + +- (void)dealloc { + sqlite3_finalize(_stringToDateStatement); + sqlite3_close(_sqliteDatabase); +} + +///-------------------------------------- +#pragma mark - Date Formatters +///-------------------------------------- + +- (NSDateFormatter *)preciseDateFormatter { + if (!_preciseDateFormatter) { + _preciseDateFormatter = [[NSDateFormatter alloc] init]; + _preciseDateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + _preciseDateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + _preciseDateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + } + return _preciseDateFormatter; +} + +///-------------------------------------- +#pragma mark - String from Date +///-------------------------------------- + +- (NSString *)preciseStringFromDate:(NSDate *)date { + __block NSString *string = nil; + dispatch_sync(_synchronizationQueue, ^{ + string = [self.preciseDateFormatter stringFromDate:date]; + }); + return string; +} + +///-------------------------------------- +#pragma mark - Date from String +///-------------------------------------- + +- (NSDate *)dateFromString:(NSString *)string { + __block sqlite3_int64 interval = 0; + __block double seconds = 0.0; + dispatch_sync(_synchronizationQueue, ^{ + const char *utf8String = [string UTF8String]; + + sqlite3_bind_text(_stringToDateStatement, 1, utf8String, -1, SQLITE_STATIC); + sqlite3_bind_text(_stringToDateStatement, 2, utf8String, -1, SQLITE_STATIC); + + if (sqlite3_step(_stringToDateStatement) == SQLITE_ROW) { + interval = sqlite3_column_int64(_stringToDateStatement, 0); + seconds = sqlite3_column_double(_stringToDateStatement, 1); + } + + sqlite3_reset(_stringToDateStatement); + sqlite3_clear_bindings(_stringToDateStatement); + }); + // Extract the fraction component of the seconds + double sintegral = 0.0; + double sfraction = modf(seconds, &sintegral); + + return [NSDate dateWithTimeIntervalSince1970:(double)interval + sfraction]; +} + +@end diff --git a/Parse/Internal/PFDecoder.h b/Parse/Internal/PFDecoder.h new file mode 100644 index 000000000..b0e5074e2 --- /dev/null +++ b/Parse/Internal/PFDecoder.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFDecoder : NSObject + +/*! + Globally available shared instance of PFDecoder. + */ ++ (PFDecoder *)objectDecoder; + +/*! + Takes a complex object that was deserialized and converts encoded + dictionaries into the proper Parse types. This is the inverse of + encodeObject:allowUnsaved:allowObjects:seenObjects:. + */ +- (nullable id)decodeObject:(nullable id)object; + +@end + +/*! + Extends the normal JSON to PFObject decoding to also deal with placeholders for new objects + that have been saved offline. + */ +@interface PFOfflineDecoder : PFDecoder + ++ (instancetype)decoderWithOfflineObjects:(nullable NSDictionary *)offlineObjects; + +@end + +/*! + A subclass of PFDecoder which can keep PFObject that has been fetched instead of creating a new instance. + */ +@interface PFKnownParseObjectDecoder : PFDecoder + ++ (instancetype)decoderWithFetchedObjects:(nullable NSDictionary *)fetchedObjects; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFDecoder.m b/Parse/Internal/PFDecoder.m new file mode 100644 index 000000000..9cd045a16 --- /dev/null +++ b/Parse/Internal/PFDecoder.m @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDecoder.h" + +#import "PFBase64Encoder.h" +#import "PFDateFormatter.h" +#import "PFFieldOperation.h" +#import "PFFieldOperationDecoder.h" +#import "PFFile_Private.h" +#import "PFGeoPointPrivate.h" +#import "PFInternalUtils.h" +#import "PFMacros.h" +#import "PFObjectPrivate.h" +#import "PFRelationPrivate.h" + +///-------------------------------------- +#pragma mark - PFDecoder +///-------------------------------------- + +@implementation PFDecoder + +#pragma mark Init + ++ (PFDecoder *)objectDecoder { + static PFDecoder *decoder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + decoder = [[PFDecoder alloc] init]; + }); + return decoder; +} + +#pragma mark Decode + +- (id)decodeDictionary:(NSDictionary *)dictionary { + NSString *op = dictionary[@"__op"]; + if (op) { + return [[PFFieldOperationDecoder defaultDecoder] decode:dictionary withDecoder:self]; + } + + NSString *type = dictionary[@"__type"]; + if (type) { + if ([type isEqualToString:@"Date"]) { + return [[PFDateFormatter sharedFormatter] dateFromString:dictionary[@"iso"]]; + + } else if ([type isEqualToString:@"Bytes"]) { + return [PFBase64Encoder dataFromBase64String:dictionary[@"base64"]]; + + } else if ([type isEqualToString:@"GeoPoint"]) { + return [PFGeoPoint geoPointWithDictionary:dictionary]; + + } else if ([type isEqualToString:@"Relation"]) { + return [PFRelation relationFromDictionary:dictionary withDecoder:self]; + + } else if ([type isEqualToString:@"File"]) { + return [PFFile fileWithName:dictionary[@"name"] + url:dictionary[@"url"]]; + + } else if ([type isEqualToString:@"Pointer"]) { + NSString *objectId = dictionary[@"objectId"]; + NSString *localId = dictionary[@"localId"]; + NSString *className = dictionary[@"className"]; + if (localId) { + // This is a PFObject deserialized off the local disk, which has a localId + // that will need to be resolved before the object can be sent over the network. + // Its localId should be known to PFObjectLocalIdStore. + return [self _decodePointerForClassName:className localId:localId]; + } else { + return [self _decodePointerForClassName:className objectId:objectId]; + } + + } else if ([type isEqualToString:@"Object"]) { + NSString *className = dictionary[@"className"]; + + NSMutableDictionary *data = [dictionary mutableCopy]; + [data removeObjectForKey:@"__type"]; + [data removeObjectForKey:@"className"]; + NSDictionary *result = [self decodeDictionary:data]; + + return [PFObject _objectFromDictionary:result + defaultClassName:className + completeData:YES + decoder:self]; + + } else { + // We don't know how to decode this, so just leave it as a dictionary. + return dictionary; + } + } + + NSMutableDictionary *newDictionary = [NSMutableDictionary dictionaryWithCapacity:[dictionary count]]; + [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + newDictionary[key] = [self decodeObject:obj]; + }]; + return newDictionary; +} + +- (id)_decodePointerForClassName:(NSString *)className objectId:(NSString *)objectId { + return [PFObject objectWithoutDataWithClassName:className objectId:objectId]; +} + +- (id)_decodePointerForClassName:(NSString *)className localId:(NSString *)localId { + return [PFObject objectWithoutDataWithClassName:className localId:localId]; +} + +- (id)decodeArray:(NSArray *)array { + NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:array.count]; + for (id value in array) { + [newArray addObject:[self decodeObject:value]]; + } + return newArray; +} + +- (id)decodeObject:(id)object { + if ([object isKindOfClass:[NSDictionary class]]) { + return [self decodeDictionary:object]; + } else if ([object isKindOfClass:[NSArray class]]) { + return [self decodeArray:object]; + } + return object; +} + +@end + +///-------------------------------------- +#pragma mark - PFOfflineDecoder +///-------------------------------------- + +@interface PFOfflineDecoder () + +/*! + A map of UUID to Task that will be finished once the given PFObject is loaded. + The Tasks should all be finished before decode is called. + */ +@property (nonatomic, copy) NSDictionary *offlineObjects; + +@end + +@implementation PFOfflineDecoder + ++ (instancetype)decoderWithOfflineObjects:(NSDictionary *)offlineObjects { + PFOfflineDecoder *decoder = [[self alloc] init]; + decoder.offlineObjects = offlineObjects; + return decoder; +} + +#pragma mark PFDecoder + +- (id)decodeObject:(id)object { + if ([object isKindOfClass:[NSDictionary class]] && + [((NSDictionary *)object)[@"__type"] isEqualToString:@"OfflineObject"]) { + NSString *uuid = ((NSDictionary *)object)[@"uuid"]; + return ((BFTask *)self.offlineObjects[uuid]).result; + } + + // Embedded objects can't show up here, because we never stored them that way offline. + return [super decodeObject:object]; +} + +@end + +///-------------------------------------- +#pragma mark - PFKnownParseObjectDecoder +///-------------------------------------- + +@interface PFKnownParseObjectDecoder () + +@property (nonatomic, copy) NSDictionary *fetchedObjects; + +@end + +@implementation PFKnownParseObjectDecoder + ++ (instancetype)decoderWithFetchedObjects:(NSDictionary *)fetchedObjects { + PFKnownParseObjectDecoder *decoder = [[self alloc] init]; + decoder.fetchedObjects = fetchedObjects; + return decoder; +} + +- (id)_decodePointerForClassName:(NSString *)className objectId:(NSString *)objectId { + if (_fetchedObjects != nil && _fetchedObjects[objectId]) { + return _fetchedObjects[objectId]; + } + return [super _decodePointerForClassName:className objectId:objectId]; +} + +@end diff --git a/Parse/Internal/PFDevice.h b/Parse/Internal/PFDevice.h new file mode 100644 index 000000000..6e62a3c51 --- /dev/null +++ b/Parse/Internal/PFDevice.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFDevice : NSObject + +@property (nonatomic, copy, readonly) NSString *detailedModel; + +@property (nonatomic, copy, readonly) NSString *operatingSystemFullVersion; +@property (nonatomic, copy, readonly) NSString *operatingSystemVersion; +@property (nonatomic, copy, readonly) NSString *operatingSystemBuild; + +@property (nonatomic, assign, readonly, getter=isJailbroken) BOOL jailbroken; + ++ (instancetype)currentDevice; + +@end diff --git a/Parse/Internal/PFDevice.m b/Parse/Internal/PFDevice.m new file mode 100644 index 000000000..05b93501e --- /dev/null +++ b/Parse/Internal/PFDevice.m @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDevice.h" + +#if TARGET_OS_IPHONE +#import +#elif TARGET_OS_MAC +#import +#endif + +#include +#include +#include + +static NSString *PFDeviceSysctlByName(NSString *name) { + const char *charName = [name UTF8String]; + + size_t size; + sysctlbyname(charName, NULL, &size, NULL, 0); + char *answer = (char*)malloc(size); + + if (answer == NULL) { + return nil; + } + + sysctlbyname(charName, answer, &size, NULL, 0); + NSString *string = [NSString stringWithUTF8String:answer]; + free(answer); + + return string; +} + +@implementation PFDevice + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)currentDevice { + static PFDevice *device; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + device = [[self alloc] init]; + }); + return device; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (NSString *)detailedModel { + NSString *name = PFDeviceSysctlByName(@"hw.machine"); + if (!name) { +#if TARGET_OS_IPHONE + name = [UIDevice currentDevice].model; +#elif TARGET_OS_MAC + name = @"Mac"; +#endif + } + return name; +} + +- (NSString *)operatingSystemFullVersion { + NSString *version = self.operatingSystemVersion; + NSString *build = self.operatingSystemBuild; + if (build.length) { + version = [version stringByAppendingFormat:@" (%@)", build]; + } + return version; +} +- (NSString *)operatingSystemVersion { +#if TARGET_OS_IPHONE + return [UIDevice currentDevice].systemVersion; +#elif TARGET_OS_MAC + NSProcessInfo *info = [NSProcessInfo processInfo]; + if ([info respondsToSelector:@selector(operatingSystemVersion)]) { + NSOperatingSystemVersion version = info.operatingSystemVersion; + return [NSString stringWithFormat:@"%d.%d.%d", + (int)version.majorVersion, + (int)version.minorVersion, + (int)version.patchVersion]; + } else { + // TODO: (nlutsenko) Remove usage of this method, when we drop support for OSX 10.9 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + SInt32 major, minor, bugfix; + if (Gestalt(gestaltSystemVersionMajor, &major) == noErr && + Gestalt(gestaltSystemVersionMinor, &minor) == noErr && + Gestalt(gestaltSystemVersionBugFix, &bugfix) == noErr) { + return [NSString stringWithFormat:@"%d.%d.%d", major, minor, bugfix]; + } +#pragma clang diagnostic pop + return [[NSProcessInfo processInfo] operatingSystemVersionString]; + } +#endif +} + +- (NSString *)operatingSystemBuild { + return PFDeviceSysctlByName(@"kern.osversion"); +} + +- (BOOL)isJailbroken { + BOOL jailbroken = NO; +#if TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR + DIR *dir = opendir("/"); + if (dir != NULL) { + jailbroken = YES; + closedir(dir); + } +#endif + return jailbroken; +} + +@end diff --git a/Parse/Internal/PFEncoder.h b/Parse/Internal/PFEncoder.h new file mode 100644 index 000000000..2e1eda385 --- /dev/null +++ b/Parse/Internal/PFEncoder.h @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFObject; +@class PFOfflineStore; +@class PFSQLiteDatabase; + +///-------------------------------------- +/// @name Encoders +///-------------------------------------- + +@interface PFEncoder : NSObject + ++ (instancetype)objectEncoder; + +- (id)encodeObject:(id)object; +- (id)encodeParseObject:(PFObject *)object; + +@end + +/*! + Encoding strategy that rejects PFObject. + */ +@interface PFNoObjectEncoder : PFEncoder + +@end + +/*! + Encoding strategy that encodes PFObject to PFPointer with objectId or with localId. + */ +@interface PFPointerOrLocalIdObjectEncoder : PFEncoder + +@end + +/*! + Encoding strategy that encodes PFObject to PFPointer with objectId and rejects + unsaved PFObject. + */ +@interface PFPointerObjectEncoder : PFPointerOrLocalIdObjectEncoder + +@end + +/*! + Encoding strategy that can encode objects that are available offline. After using this encoder, + you must call encodeFinished and wait for its result to be finished before the results of the + encoding will be valid. + */ +@interface PFOfflineObjectEncoder : PFEncoder + ++ (instancetype)objectEncoderWithOfflineStore:(PFOfflineStore *)store database:(PFSQLiteDatabase *)database; + +- (BFTask *)encodeFinished; + +@end diff --git a/Parse/Internal/PFEncoder.m b/Parse/Internal/PFEncoder.m new file mode 100644 index 000000000..e8f56ab08 --- /dev/null +++ b/Parse/Internal/PFEncoder.m @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFEncoder.h" + +#import "PFACLPrivate.h" +#import "PFAssert.h" +#import "PFBase64Encoder.h" +#import "PFDateFormatter.h" +#import "PFFieldOperation.h" +#import "PFFile.h" +#import "PFGeoPointPrivate.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFRelationPrivate.h" + +@implementation PFEncoder + ++ (instancetype)objectEncoder { + static PFEncoder *encoder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + encoder = [[PFEncoder alloc] init]; + }); + return encoder; +} + +- (id)encodeObject:(id)object { + if ([object isKindOfClass:[PFObject class]]) { + return [self encodeParseObject:object]; + } else if ([object isKindOfClass:[NSData class]]) { + return @{ + @"__type" : @"Bytes", + @"base64" : [PFBase64Encoder base64StringFromData:object] + }; + + } else if ([object isKindOfClass:[NSDate class]]) { + return @{ + @"__type" : @"Date", + @"iso" : [[PFDateFormatter sharedFormatter] preciseStringFromDate:object] + }; + + } else if ([object isKindOfClass:[PFFile class]]) { + if (((PFFile *)object).isDirty) { + // TODO: (nlutsenko) Figure out what to do with things like an unsaved file + // in a mutable container, where we don't normally want to allow serializing + // such a thing inside an object. + // + // Returning this empty object is strictly wrong, but we have to have *something* + // to put into an object's mutable container cache, and this is just about the + // best we can do right now. + // + // [NSException raise:NSInternalInconsistencyException + // format:@"Tried to serialize an unsaved file."]; + return @{ @"__type" : @"File" }; + } + return @{ + @"__type" : @"File", + @"url" : ((PFFile *)object).url, + @"name" : ((PFFile *)object).name + }; + + } else if ([object isKindOfClass:[PFFieldOperation class]]) { + // Always encode PFFieldOperation with PFPointerOrLocalId + return [object encodeWithObjectEncoder:[PFPointerOrLocalIdObjectEncoder objectEncoder]]; + } else if ([object isKindOfClass:[PFACL class]]) { + // TODO (hallucinogen): pass object encoder here + return [object encodeIntoDictionary]; + + } else if ([object isKindOfClass:[PFGeoPoint class]]) { + // TODO (hallucinogen): pass object encoder here + return [object encodeIntoDictionary]; + + } else if ([object isKindOfClass:[PFRelation class]]) { + // TODO (hallucinogen): pass object encoder here + return [object encodeIntoDictionary]; + + } else if ([object isKindOfClass:[NSArray class]]) { + NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:[object count]]; + for (id elem in object) { + [newArray addObject:[self encodeObject:elem]]; + } + return newArray; + + } else if ([object isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:[object count]]; + [object enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + dict[key] = [self encodeObject:obj]; + }]; + return dict; + } + + return object; +} + +- (id)encodeParseObject:(PFObject *)object { + // Do nothing here + return nil; +} + +@end + +///-------------------------------------- +#pragma mark - PFNoObjectEncoder +///-------------------------------------- + +@implementation PFNoObjectEncoder + ++ (instancetype)objectEncoder { + static PFNoObjectEncoder *encoder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + encoder = [[PFNoObjectEncoder alloc] init]; + }); + return encoder; +} + +- (id)encodeParseObject:(PFObject *)object { + [NSException raise:NSInternalInconsistencyException format:@"PFObjects are not allowed here."]; + return nil; +} + +@end + +///-------------------------------------- +#pragma mark - PFPointerOrLocalIdObjectEncoder +///-------------------------------------- + +@implementation PFPointerOrLocalIdObjectEncoder + ++ (instancetype)objectEncoder { + static PFPointerOrLocalIdObjectEncoder *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[PFPointerOrLocalIdObjectEncoder alloc] init]; + }); + return instance; +} + +- (id)encodeParseObject:(PFObject *)object { + if (object.objectId) { + return @{ + @"__type" : @"Pointer", + @"objectId" : object.objectId, + @"className" : object.parseClassName + }; + } + return @{ + @"__type" : @"Pointer", + @"localId" : [object getOrCreateLocalId], + @"className" : object.parseClassName + }; +} + +@end + +///-------------------------------------- +#pragma mark - PFPointerObjectEncoder +///-------------------------------------- + +@implementation PFPointerObjectEncoder + ++ (instancetype)objectEncoder { + static PFPointerObjectEncoder *encoder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + encoder = [[self alloc] init]; + }); + return encoder; +} + +- (id)encodeParseObject:(PFObject *)object { + PFConsistencyAssert(object.objectId, @"Tried to save an object with a new, unsaved child."); + return [super encodeParseObject:object]; +} + +@end + +///-------------------------------------- +#pragma mark - PFOfflineObjectEncoder +///-------------------------------------- + +@interface PFOfflineObjectEncoder () + +@property (nonatomic, assign) PFOfflineStore *store; +@property (nonatomic, assign) PFSQLiteDatabase *database; +@property (nonatomic, strong) NSMutableArray *tasks; +@property (nonatomic, strong) NSObject *tasksLock; // TODO: (nlutsenko) Avoid using @synchronized + +@end + +@implementation PFOfflineObjectEncoder + ++ (instancetype)objectEncoder { + PFNotDesignatedInitializer(); + return nil; +} + ++ (instancetype)objectEncoderWithOfflineStore:(PFOfflineStore *)store database:(PFSQLiteDatabase *)database { + PFOfflineObjectEncoder *encoder = [[self alloc] init]; + encoder.store = store; + encoder.database = database; + encoder.tasks = [NSMutableArray array]; + encoder.tasksLock = [[NSObject alloc] init]; + return encoder; +} + +- (id)encodeParseObject:(PFObject *)object { + if (object.objectId) { + return @{ + @"__type" : @"Pointer", + @"objectId" : object.objectId, + @"className" : object.parseClassName + }; + } else { + NSMutableDictionary *result = [@{ @"__type" : @"OfflineObject" } mutableCopy]; + @synchronized(self.tasksLock) { + BFTask *uuidTask = [self.store getOrCreateUUIDAsyncForObject:object database:self.database]; + [uuidTask continueWithSuccessBlock:^id(BFTask *task) { + result[@"uuid"] = task.result; + return nil; + }]; + [self.tasks addObject:uuidTask]; + } + return result; + } +} + +- (BFTask *)encodeFinished { + return [[BFTask taskForCompletionOfAllTasks:self.tasks] continueWithBlock:^id(BFTask *ignore) { + @synchronized (self.tasksLock) { + // TODO (hallucinogen) It might be better to return an aggregate error here + for (BFTask *task in self.tasks) { + if (task.cancelled || task.error != nil) { + return task; + } + } + [self.tasks removeAllObjects]; + return [BFTask taskWithResult:nil]; + } + }]; +} + +@end diff --git a/Parse/Internal/PFErrorUtilities.h b/Parse/Internal/PFErrorUtilities.h new file mode 100644 index 000000000..a5656f6f9 --- /dev/null +++ b/Parse/Internal/PFErrorUtilities.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFErrorUtilities : NSObject + +/*! + Construct an error object from a code and a message. + + @description Note that this logs all errors given to it. + You should use `errorWithCode:message:shouldLog:` to explicitly control whether it logs. + + @param code Parse Error Code + @param message Error description + + @return Instance of `NSError` or `nil`. + */ ++ (nullable NSError *)errorWithCode:(NSInteger)code message:(NSString *)message; ++ (nullable NSError *)errorWithCode:(NSInteger)code message:(NSString *)message shouldLog:(BOOL)shouldLog; + +/*! + Construct an error object from a result dictionary the API returned. + + @description Note that this logs all errors given to it. + You should use `errorFromResult:shouldLog:` to explicitly control whether it logs. + + @param result Network command result. + + @return Instance of `NSError` or `nil`. + */ ++ (nullable NSError *)errorFromResult:(NSDictionary *)result; ++ (nullable NSError *)errorFromResult:(NSDictionary *)result shouldLog:(BOOL)shouldLog; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFErrorUtilities.m b/Parse/Internal/PFErrorUtilities.m new file mode 100644 index 000000000..a3f060dd8 --- /dev/null +++ b/Parse/Internal/PFErrorUtilities.m @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFErrorUtilities.h" + +#import "PFConstants.h" +#import "PFLogging.h" + +@implementation PFErrorUtilities + ++ (NSError *)errorWithCode:(NSInteger)code message:(NSString *)message { + return [self errorWithCode:code message:message shouldLog:YES]; +} + ++ (NSError *)errorWithCode:(NSInteger)code message:(NSString *)message shouldLog:(BOOL)shouldLog { + NSDictionary *result = @{ @"code" : @(code), + @"error" : message }; + return [self errorFromResult:result shouldLog:shouldLog]; +} + ++ (NSError *)errorFromResult:(NSDictionary *)result { + return [self errorFromResult:result shouldLog:YES]; +} + ++ (NSError *)errorFromResult:(NSDictionary *)result shouldLog:(BOOL)shouldLog { + NSInteger errorCode = [[result objectForKey:@"code"] integerValue]; + + NSString *errorExplanation = [result objectForKey:@"error"]; + + if (shouldLog) { + PFLogError(PFLoggingTagCommon, + @"%@ (Code: %ld, Version: %@)", errorExplanation, (long)errorCode, PARSE_VERSION); + } + + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:result]; + if (errorExplanation) { + userInfo[NSLocalizedDescriptionKey] = errorExplanation; + } + return [NSError errorWithDomain:PFParseErrorDomain code:errorCode userInfo:userInfo]; +} + +@end diff --git a/Parse/Internal/PFEventuallyPin.h b/Parse/Internal/PFEventuallyPin.h new file mode 100644 index 000000000..c9c3ad6d5 --- /dev/null +++ b/Parse/Internal/PFEventuallyPin.h @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFObject.h" +#import "PFSubclassing.h" + +@class BFTask; +@protocol PFNetworkCommand; + +extern NSString *const PFEventuallyPinPinName; + +// Cache policies +typedef NS_ENUM(NSUInteger, PFEventuallyPinType) { + PFEventuallyPinTypeSave = 1, + PFEventuallyPinTypeDelete, + PFEventuallyPinTypeCommand +}; + +/*! + PFEventuallyPin represents PFCommand that's save locally so that it can be executed eventually. + + Properties of PFEventuallyPin: + - time + Used for sort order when querying for all EventuallyPins. + - type + PFEventuallyPinTypeSave or PFEventuallyPinTypeDelete. + - object + The object the operation should notify when complete. + - operationSetUUID + The operationSet to be completed. + - sessionToken + The user that instantiated the operation. + */ +@interface PFEventuallyPin : PFObject + +@property (nonatomic, copy, readonly) NSString *uuid; + +@property (nonatomic, assign, readonly) PFEventuallyPinType type; + +@property (nonatomic, strong, readonly) PFObject *object; + +@property (nonatomic, copy, readonly) NSString *operationSetUUID; + +@property (nonatomic, copy, readonly) NSString *sessionToken; + +@property (nonatomic, strong, readonly) id command; + +///-------------------------------------- +#pragma mark - Eventually Pin +///-------------------------------------- + +/*! + Wrap given PFObject and PFCommand in a PFEventuallyPin with auto-generated UUID. + */ ++ (BFTask *)pinEventually:(PFObject *)object forCommand:(id)command; + +/*! + Wrap given PFObject and PFCommand in a PFEventuallyPin with given UUID. + */ ++ (BFTask *)pinEventually:(PFObject *)object forCommand:(id)command withUUID:(NSString *)uuid; + ++ (BFTask *)findAllEventuallyPin; + ++ (BFTask *)findAllEventuallyPinWithExcludeUUIDs:(NSArray *)excludeUUIDs; + +@end diff --git a/Parse/Internal/PFEventuallyPin.m b/Parse/Internal/PFEventuallyPin.m new file mode 100644 index 000000000..bce93eded --- /dev/null +++ b/Parse/Internal/PFEventuallyPin.m @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFEventuallyPin.h" + +#import + +#import "PFAssert.h" +#import "PFHTTPRequest.h" +#import "PFInternalUtils.h" +#import "PFObject+Subclass.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFQuery.h" +#import "PFRESTCommand.h" + +NSString *const PFEventuallyPinPinName = @"_eventuallyPin"; + +NSString *const PFEventuallyPinKeyUUID = @"uuid"; +NSString *const PFEventuallyPinKeyTime = @"time"; +NSString *const PFEventuallyPinKeyType = @"type"; +NSString *const PFEventuallyPinKeyObject = @"object"; +NSString *const PFEventuallyPinKeyOperationSetUUID = @"operationSetUUID"; +NSString *const PFEventuallyPinKeySessionToken = @"sessionToken"; +NSString *const PFEventuallyPinKeyCommand = @"command"; + +@implementation PFEventuallyPin + +///-------------------------------------- +#pragma mark - PFSubclassing +///-------------------------------------- + ++ (NSString *)parseClassName { + return @"_EventuallyPin"; +} + +// Validates a class name. We override this to only allow the pin class name. ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert([className isEqualToString:[self parseClassName]], + @"Cannot initialize a PFEventuallyPin with a custom class name."); +} + +- (BOOL)needsDefaultACL { + return NO; +} + +///-------------------------------------- +#pragma mark - Getter +///-------------------------------------- + +- (NSString *)uuid { + return self[PFEventuallyPinKeyUUID]; +} + +- (PFEventuallyPinType)type { + return [self[PFEventuallyPinKeyType] intValue]; +} + +- (PFObject *)object { + return self[PFEventuallyPinKeyObject]; +} + +- (NSString *)operationSetUUID { + return self[PFEventuallyPinKeyOperationSetUUID]; +} + +- (NSString *)sessionToken { + return self[PFEventuallyPinKeySessionToken]; +} + +- (id)command { + NSDictionary *dictionary = self[PFEventuallyPinKeyCommand]; + if ([PFRESTCommand isValidDictionaryRepresentation:dictionary]) { + return [PFRESTCommand commandFromDictionaryRepresentation:dictionary]; + } + return nil; +} + +///-------------------------------------- +#pragma mark - Eventually Pin +///-------------------------------------- + ++ (BFTask *)pinEventually:(PFObject *)object forCommand:(id)command { + return [self pinEventually:object forCommand:command withUUID:[[NSUUID UUID] UUIDString]]; +} + ++ (BFTask *)pinEventually:(PFObject *)object forCommand:(id)command withUUID:(NSString *)uuid { + PFEventuallyPinType type = [self _pinTypeForCommand:command]; + NSDictionary *commandDictionary = (type == PFEventuallyPinTypeCommand ? [command dictionaryRepresentation] : nil); + return [self _pinEventually:object + type:type + uuid:uuid + operationSetUUID:command.operationSetUUID + sessionToken:command.sessionToken + commandDictionary:commandDictionary]; +} + ++ (BFTask *)findAllEventuallyPin { + return [self findAllEventuallyPinWithExcludeUUIDs:nil]; +} + ++ (BFTask *)findAllEventuallyPinWithExcludeUUIDs:(NSArray *)excludeUUIDs { + PFQuery *query = [PFQuery queryWithClassName:self.parseClassName]; + [query fromPinWithName:PFEventuallyPinPinName]; + [query orderByAscending:PFEventuallyPinKeyTime]; + + if (excludeUUIDs != nil) { + [query whereKey:PFEventuallyPinKeyUUID notContainedIn:excludeUUIDs]; + } + + return [[query findObjectsInBackground] continueWithBlock:^id(BFTask *task) { + NSArray *pins = task.result; + NSMutableArray *fetchTasks = [NSMutableArray array]; + + for (PFEventuallyPin *pin in pins) { + PFObject *object = pin.object; + if (object != nil) { + [fetchTasks addObject:[object fetchFromLocalDatastoreInBackground]]; + } + } + + return [[BFTask taskForCompletionOfAllTasks:fetchTasks] continueWithBlock:^id(BFTask *task) { + return [BFTask taskWithResult:pins]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + ++ (BFTask *)_pinEventually:(PFObject *)object + type:(PFEventuallyPinType)type + uuid:(NSString *)uuid + operationSetUUID:(NSString *)operationSetUUID + sessionToken:(NSString *)sessionToken + commandDictionary:(NSDictionary *)commandDictionary { + PFEventuallyPin *pin = [[PFEventuallyPin alloc] init]; + pin[PFEventuallyPinKeyUUID] = uuid; + pin[PFEventuallyPinKeyTime] = [NSDate date]; + pin[PFEventuallyPinKeyType] = @(type); + if (object != nil) { + pin[PFEventuallyPinKeyObject] = object; + } + if (operationSetUUID != nil) { + pin[PFEventuallyPinKeyOperationSetUUID] = operationSetUUID; + } + if (sessionToken != nil) { + pin[PFEventuallyPinKeySessionToken] = sessionToken; + } + if (commandDictionary != nil) { + pin[PFEventuallyPinKeyCommand] = commandDictionary; + } + + return [[pin pinInBackgroundWithName:PFEventuallyPinPinName] continueWithBlock:^id(BFTask *task) { + return pin; + }]; +} + ++ (PFEventuallyPinType)_pinTypeForCommand:(id)command { + PFEventuallyPinType type = PFEventuallyPinTypeCommand; + NSString *path = [(PFRESTCommand *)command httpPath]; + NSString *method = [(PFRESTCommand *)command httpMethod]; + if ([path hasPrefix:@"classes"]) { + if ([method isEqualToString:PFHTTPRequestMethodPOST] || + [method isEqualToString:PFHTTPRequestMethodPUT]) { + type = PFEventuallyPinTypeSave; + } else if ([method isEqualToString:PFHTTPRequestMethodDELETE]) { + type = PFEventuallyPinTypeDelete; + } + } + return type; +} + +@end diff --git a/Parse/Internal/PFEventuallyQueue.h b/Parse/Internal/PFEventuallyQueue.h new file mode 100644 index 000000000..6924d02d3 --- /dev/null +++ b/Parse/Internal/PFEventuallyQueue.h @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFMacros.h" +#import "PFNetworkCommand.h" + +@class BFTask; +@class PFEventuallyQueueTestHelper; +@class PFObject; +@protocol PFCommandRunning; + +extern NSUInteger const PFEventuallyQueueDefaultMaxAttemptsCount; +extern NSTimeInterval const PFEventuallyQueueDefaultTimeoutRetryInterval; + +@interface PFEventuallyQueue : NSObject + +@property (nonatomic, strong, readonly) id commandRunner; + +@property (nonatomic, assign, readonly) NSUInteger maxAttemptsCount; +@property (nonatomic, assign, readonly) NSTimeInterval retryInterval; + +@property (nonatomic, assign, readonly) NSUInteger commandCount; + +/*! + Controls whether the queue should monitor network reachability and pause itself when there is no connection. + Default: `YES`. + */ +@property (atomic, assign, readonly) BOOL monitorsReachability; +@property (nonatomic, assign, readonly, getter=isConnected) BOOL connected; + +// Gets notifications of various events happening in the command cache, so that tests can be synchronized. +@property (nonatomic, strong, readonly) PFEventuallyQueueTestHelper *testHelper; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCommandRunner:(id)commandRunner + maxAttemptsCount:(NSUInteger)attemptsCount + retryInterval:(NSTimeInterval)retryInterval NS_DESIGNATED_INITIALIZER; + +///-------------------------------------- +/// @name Running Commands +///-------------------------------------- + +- (BFTask *)enqueueCommandInBackground:(id)command; +- (BFTask *)enqueueCommandInBackground:(id)command withObject:(PFObject *)object; + +///-------------------------------------- +/// @name Controlling Queue +///-------------------------------------- + +- (void)start NS_REQUIRES_SUPER; +- (void)resume NS_REQUIRES_SUPER; +- (void)pause NS_REQUIRES_SUPER; + +- (void)removeAllCommands NS_REQUIRES_SUPER; + +@end + +typedef enum { + PFEventuallyQueueEventCommandEnqueued, // A command was placed into the queue. + PFEventuallyQueueEventCommandNotEnqueued, // A command could not be placed into the queue. + + PFEventuallyQueueEventCommandSucceded, // A command has successfully running on the server. + PFEventuallyQueueEventCommandFailed, // A command has failed on the server. + + PFEventuallyQueueEventObjectUpdated, // An object's data was updated after a command completed. + PFEventuallyQueueEventObjectRemoved, // An object was removed because it was deleted before creation. + + PFEventuallyQueueEventCount // The total number of items in this enum. +} PFEventuallyQueueTestHelperEvent; + +@interface PFEventuallyQueueTestHelper : NSObject { + dispatch_semaphore_t events[PFEventuallyQueueEventCount]; +} + +- (void)clear; +- (void)notify:(PFEventuallyQueueTestHelperEvent)event; +- (BOOL)waitFor:(PFEventuallyQueueTestHelperEvent)event; + +@end diff --git a/Parse/Internal/PFEventuallyQueue.m b/Parse/Internal/PFEventuallyQueue.m new file mode 100644 index 000000000..f5e536c6f --- /dev/null +++ b/Parse/Internal/PFEventuallyQueue.m @@ -0,0 +1,497 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFEventuallyQueue.h" +#import "PFEventuallyQueue_Private.h" + +#import +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFErrorUtilities.h" +#import "PFLogging.h" +#import "PFMacros.h" +#import "PFRESTCommand.h" +#import "PFReachability.h" +#import "PFTaskQueue.h" + +NSUInteger const PFEventuallyQueueDefaultMaxAttemptsCount = 5; +NSTimeInterval const PFEventuallyQueueDefaultTimeoutRetryInterval = 600.0f; + +@interface PFEventuallyQueue () + +@property (atomic, assign, readwrite) BOOL monitorsReachability; +@property (atomic, assign, getter=isRunning) BOOL running; + +@end + +@implementation PFEventuallyQueue + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommandRunner:(id)commandRunner + maxAttemptsCount:(NSUInteger)attemptsCount + retryInterval:(NSTimeInterval)retryInterval { + self = [super init]; + if (!self) return nil; + + _commandRunner = commandRunner; + _maxAttemptsCount = attemptsCount; + _retryInterval = retryInterval; + + // Set up all the queues + NSString *queueBaseLabel = [NSString stringWithFormat:@"com.parse.%@", NSStringFromClass([self class])]; + + _synchronizationQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.synchronization", + queueBaseLabel] UTF8String], + DISPATCH_QUEUE_SERIAL); + PFMarkDispatchQueue(_synchronizationQueue); + _synchronizationExecutor = [BFExecutor executorWithDispatchQueue:_synchronizationQueue]; + + _processingQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.processing", + queueBaseLabel] UTF8String], + DISPATCH_QUEUE_SERIAL); + PFMarkDispatchQueue(_processingQueue); + + _processingQueueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, _processingQueue); + + _commandEnqueueTaskQueue = [[PFTaskQueue alloc] init]; + + _taskCompletionSources = [NSMutableDictionary dictionary]; + _testHelper = [[PFEventuallyQueueTestHelper alloc] init]; + + [self _startMonitoringNetworkReachability]; + + return self; +} + +- (void)dealloc { + [self _stopMonitoringNetworkReachability]; +} + +///-------------------------------------- +#pragma mark - Enqueueing Commands +///-------------------------------------- + +- (BFTask *)enqueueCommandInBackground:(id)command { + return [self enqueueCommandInBackground:command withObject:nil]; +} + +- (BFTask *)enqueueCommandInBackground:(id)command withObject:(PFObject *)object { + PFParameterAssert(command, @"Cannot enqueue nil command."); + + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + + @weakify(self); + [_commandEnqueueTaskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + @strongify(self); + + NSString *identifier = [self _newIdentifierForCommand:command]; + return [[[self _enqueueCommandInBackground:command + object:object + identifier:identifier] continueWithBlock:^id(BFTask *task) { + if (task.error || task.exception || task.cancelled) { + [self.testHelper notify:PFEventuallyQueueEventCommandNotEnqueued]; + if (task.error) { + taskCompletionSource.error = task.error; + } else if (task.exception) { + taskCompletionSource.exception = task.exception; + } else if (task.cancelled) { + [taskCompletionSource cancel]; + } + } else { + [self.testHelper notify:PFEventuallyQueueEventCommandEnqueued]; + } + + return task; + }] continueWithExecutor:_synchronizationExecutor withSuccessBlock:^id(BFTask *task) { + [self _didEnqueueCommand:command withIdentifier:identifier taskCompletionSource:taskCompletionSource]; + return nil; + }]; + }]; + }]; + + return taskCompletionSource.task; +} + +- (BFTask *)_enqueueCommandInBackground:(id)command + object:(PFObject *)object + identifier:(NSString *)identifier { + // This enforces implementing this method in subclasses + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (void)_didEnqueueCommand:(id)command + withIdentifier:(NSString *)identifier + taskCompletionSource:(BFTaskCompletionSource *)taskCompletionSource { + PFAssertIsOnDispatchQueue(_synchronizationQueue); + + _taskCompletionSources[identifier] = taskCompletionSource; + dispatch_source_merge_data(_processingQueueSource, 1); + + if (_retryingSemaphore) { + dispatch_semaphore_signal(_retryingSemaphore); + } +} + +///-------------------------------------- +#pragma mark - Pending Commands +///-------------------------------------- + +- (NSArray *)_pendingCommandIdentifiers { + return nil; +} + +- (id)_commandWithIdentifier:(NSString *)identifier error:(NSError **)error { + return nil; +} + +- (NSString *)_newIdentifierForCommand:(id)command { + return nil; +} + +- (NSUInteger)commandCount { + return [[self _pendingCommandIdentifiers] count]; +} + +///-------------------------------------- +#pragma mark - Controlling Queue +///-------------------------------------- + +- (void)start { + dispatch_source_set_event_handler(_processingQueueSource, ^{ + [self _runCommands]; + }); + [self resume]; +} + +- (void)resume { + if (self.running) { + return; + } + self.running = YES; + dispatch_resume(_processingQueueSource); + dispatch_source_merge_data(_processingQueueSource, 1); +} + +- (void)pause { + if (!self.running) { + return; + } + self.running = NO; + dispatch_suspend(_processingQueueSource); +} + +- (void)removeAllCommands { + dispatch_sync(_synchronizationQueue, ^{ + [_taskCompletionSources removeAllObjects]; + }); +} + +///-------------------------------------- +#pragma mark - Running Commands +///-------------------------------------- + +- (void)_runCommands { + PFAssertIsOnDispatchQueue(_processingQueue); + + [self _runCommandsWithRetriesCount:self.maxAttemptsCount]; +} + +- (void)_runCommandsWithRetriesCount:(NSUInteger)retriesCount { + PFAssertIsOnDispatchQueue(_processingQueue); + + if (!self.running || !self.connected) { + return; + } + + // Expect sorted result from _pendingCommandIdentifiers + NSArray *commandIdentifiers = [self _pendingCommandIdentifiers]; + BOOL shouldRetry = NO; + for (NSString *identifier in commandIdentifiers) { + NSError *error = nil; + id command = [self _commandWithIdentifier:identifier error:&error]; + if (!command || error) { + if (!error) { + error = [PFErrorUtilities errorWithCode:kPFErrorInternalServer + message:@"Failed to dequeue an eventually command." + shouldLog:NO]; + } + BFTask *task = [BFTask taskWithError:error]; + [self _didFinishRunningCommand:command withIdentifier:identifier resultTask:task]; + continue; + } + + __block BFTaskCompletionSource *taskCompletionSource = nil; + dispatch_sync(_synchronizationQueue, ^{ + taskCompletionSource = _taskCompletionSources[identifier]; + }); + + BFTask *resultTask = nil; + PFCommandResult *result = nil; + @try { + resultTask = [self _runCommand:command withIdentifier:identifier]; + result = [resultTask waitForResult:&error]; + } + @catch (NSException *exception) { + error = [NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorInvalidPointer + userInfo:@{ @"message" : @"Failed to run an eventually command.", + @"exception" : exception }]; + resultTask = [BFTask taskWithError:error]; + } + + if (error) { + BOOL permanent = (![error.userInfo[@"temporary"] boolValue] && + ([[error domain] isEqualToString:PFParseErrorDomain] || + [error code] != kPFErrorConnectionFailed)); + + if (!permanent) { + PFLogWarning(PFLoggingTagCommon, + @"Attempt at runEventually command timed out. Waiting %f seconds. %d retries remaining.", + self.retryInterval, + (int)retriesCount); + + __block dispatch_semaphore_t semaphore = NULL; + dispatch_sync(_synchronizationQueue, ^{ + _retryingSemaphore = dispatch_semaphore_create(0); + semaphore = _retryingSemaphore; + }); + + dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(self.retryInterval * NSEC_PER_SEC)); + + long waitResult = dispatch_semaphore_wait(semaphore, timeoutTime); + dispatch_sync(_synchronizationQueue, ^{ + _retryingSemaphore = NULL; + }); + + if (waitResult == 0) { + // We haven't waited long enough, but if we lost the connection, or should stop, just quit. + return; + } + + // We need to go out of the loop. + if (retriesCount > 0) { + shouldRetry = YES; + break; + } + } + + PFLogError(PFLoggingTagCommon, @"Failed to run command eventually with error: %@", error); + } + + // Post processing shouldn't make the queue retry the command. + resultTask = [self _didFinishRunningCommand:command withIdentifier:identifier resultTask:resultTask]; + [resultTask waitForResult:nil]; + + // Notify anyone waiting that the operation is completed. + if (resultTask.error) { + taskCompletionSource.error = resultTask.error; + } else if (resultTask.exception) { + taskCompletionSource.exception = resultTask.exception; + } else if (resultTask.cancelled) { + [taskCompletionSource cancel]; + } else { + taskCompletionSource.result = resultTask.result; + } + } + + // Retry here so that we're in cleaner state. + if (shouldRetry) { + return [self _runCommandsWithRetriesCount:(retriesCount - 1)]; + } +} + +- (BFTask *)_runCommand:(id)command withIdentifier:(NSString *)identifier { + if ([command isKindOfClass:[PFRESTCommand class]]) { + return [self.commandRunner runCommandAsync:(PFRESTCommand *)command withOptions:0]; + } + + NSString *reason = [NSString stringWithFormat:@"Can't find a compatible runner for command %@.", command]; + NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException + reason:reason + userInfo:nil]; + return [BFTask taskWithException:exception]; +} + +- (BFTask *)_didFinishRunningCommand:(id)command + withIdentifier:(NSString *)identifier + resultTask:(BFTask *)resultTask { + PFConsistencyAssert(resultTask.completed, @"Task should be completed."); + + dispatch_sync(_synchronizationQueue, ^{ + [_taskCompletionSources removeObjectForKey:identifier]; + }); + + if (resultTask.exception || resultTask.error || resultTask.cancelled) { + [self.testHelper notify:PFEventuallyQueueEventCommandFailed]; + } else { + [self.testHelper notify:PFEventuallyQueueEventCommandSucceded]; + } + + return resultTask; +} + +- (BFTask *)_waitForOperationSet:(PFOperationSet *)operationSet + eventuallyPin:(PFEventuallyPin *)eventuallyPin { + return [BFTask taskWithResult:nil]; +} + +///-------------------------------------- +#pragma mark - Reachability +///-------------------------------------- + +- (void)_startMonitoringNetworkReachability { + if (self.monitorsReachability) { + return; + } + self.monitorsReachability = YES; + + [[PFReachability sharedParseReachability] addListener:self]; + + // Set the initial connected status + self.connected = ([PFReachability sharedParseReachability].currentState != PFReachabilityStateNotReachable); +} + +- (void)_stopMonitoringNetworkReachability { + if (!self.monitorsReachability) { + return; + } + + [[PFReachability sharedParseReachability] removeListener:self]; + + if (_reachability != NULL) { + SCNetworkReachabilitySetCallback(_reachability, NULL, NULL); + SCNetworkReachabilitySetDispatchQueue(_reachability, NULL); + CFRelease(_reachability); + _reachability = NULL; + } + + self.monitorsReachability = NO; + self.connected = YES; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +/*! Manually sets the network connection status. */ +- (void)setConnected:(BOOL)connected { + BFTaskCompletionSource *barrier = [BFTaskCompletionSource taskCompletionSource]; + dispatch_async(_processingQueue, ^{ + dispatch_sync(_synchronizationQueue, ^{ + if (self.connected != connected) { + _connected = connected; + if (connected) { + dispatch_source_merge_data(_processingQueueSource, 1); + } + } + }); + barrier.result = nil; + }); + if (connected) { + dispatch_async(_synchronizationQueue, ^{ + if (_retryingSemaphore) { + dispatch_semaphore_signal(_retryingSemaphore); + } + }); + } + [barrier.task waitForResult:nil]; +} + +///-------------------------------------- +#pragma mark - Test Helper Method +///-------------------------------------- + +/*! Makes this command cache forget all the state it keeps during a single run of the app. */ +- (void)_simulateReboot { + // Make sure there is no command pending enqueuing + [[[[_commandEnqueueTaskQueue enqueue:^BFTask *(BFTask *toAwait) { + return toAwait; + }] continueWithExecutor:_synchronizationExecutor withBlock:^id(BFTask *task) { + // Remove all state task completion sources + [_taskCompletionSources removeAllObjects]; + return nil; + }] continueWithExecutor:[BFExecutor executorWithDispatchQueue:_processingQueue] withBlock:^id(BFTask *task) { + // Let all operations in the queue run at least once + return nil; + }] waitUntilFinished]; +} + +/*! Test helper to return how many commands are being retained in memory by the cache. */ +- (int)_commandsInMemory { + return (int)[_taskCompletionSources count]; +} + +/*! Called by PFObject whenever an object has been updated after a saveEventually. */ +- (void)_notifyTestHelperObjectUpdated { + [self.testHelper notify:PFEventuallyQueueEventObjectUpdated]; +} + +- (void)_setMaxAttemptsCount:(NSUInteger)attemptsCount { + _maxAttemptsCount = attemptsCount; +} + +- (void)_setRetryInterval:(NSTimeInterval)retryInterval { + _retryInterval = retryInterval; +} + +///-------------------------------------- +#pragma mark - Reachability +///-------------------------------------- + +- (void)reachability:(PFReachability *)reachability didChangeReachabilityState:(PFReachabilityState)state { + if (self.monitorsReachability) { + self.connected = (state != PFReachabilityStateNotReachable); + } +} + +@end + +// PFEventuallyQueueTestHelper gets notifications of various events happening in the command cache, +// so that tests can be synchronized. See CommandTests.m for examples of how to use this. + +@implementation PFEventuallyQueueTestHelper + +- (instancetype)init { + self = [super init]; + if (self) { + [self clear]; + } + return self; +} + +- (void)clear { + for (int i = 0; i < PFEventuallyQueueEventCount; ++i) { + events[i] = dispatch_semaphore_create(0); + } +} + +- (void)notify:(PFEventuallyQueueTestHelperEvent)event { + dispatch_semaphore_signal(events[event]); +} + +- (BOOL)waitFor:(PFEventuallyQueueTestHelperEvent)event { + // Wait 1 second for a permit from the semaphore. + return (dispatch_semaphore_wait(events[event], dispatch_time(DISPATCH_TIME_NOW, 10LL * NSEC_PER_SEC)) == 0); +} + +@end diff --git a/Parse/Internal/PFEventuallyQueue_Private.h b/Parse/Internal/PFEventuallyQueue_Private.h new file mode 100644 index 000000000..b7ec91853 --- /dev/null +++ b/Parse/Internal/PFEventuallyQueue_Private.h @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFEventuallyQueue.h" + +@class BFExecutor; +@class PFEventuallyPin; +@class PFObject; +@class PFOperationSet; +@class PFTaskQueue; + +extern NSUInteger const PFEventuallyQueueDefaultMaxAttemptsCount; +extern NSTimeInterval const PFEventuallyQueueDefaultTimeoutRetryInterval; + +@class BFTaskCompletionSource; + +@interface PFEventuallyQueue () +{ +@protected + BFExecutor *_synchronizationExecutor; + dispatch_queue_t _synchronizationQueue; + + // Object for getting network status. + SCNetworkReachabilityRef _reachability; + dispatch_queue_t _reachabilityQueue; + +@private + dispatch_queue_t _processingQueue; + dispatch_source_t _processingQueueSource; + + dispatch_semaphore_t _retryingSemaphore; + + NSMutableDictionary *_taskCompletionSources; + + /*! + Task queue that will enqueue command enqueueing task so that we enqueue the command + one at a time. + */ + PFTaskQueue *_commandEnqueueTaskQueue; +} + +@property (nonatomic, assign, readwrite, getter=isConnected) BOOL connected; + +/*! + This method is used to do some work after the command is finished running and + either succeeded or dropped from queue with error/exception. + + @param command Command that was run. + @param identifier Unique identifier of the command + @param resultTask Task that represents the result of running a command. + @returns A continuation task in case the EventuallyQueue need to do something. + Typically this will return back given resultTask. + */ +- (BFTask *)_didFinishRunningCommand:(id)command + withIdentifier:(NSString *)identifier + resultTask:(BFTask *)resultTask; + +///-------------------------------------- +/// @name Reachability +///-------------------------------------- + +- (void)_startMonitoringNetworkReachability; +- (void)_stopMonitoringNetworkReachability; + +///-------------------------------------- +/// @name Test Helper +///-------------------------------------- + +- (void)_setMaxAttemptsCount:(NSUInteger)attemptsCount; + +- (void)_setRetryInterval:(NSTimeInterval)retryInterval; + +- (void)_simulateReboot NS_REQUIRES_SUPER; + +- (int)_commandsInMemory; + +- (void)_notifyTestHelperObjectUpdated; + +@end + +@protocol PFEventuallyQueueSubclass + +///-------------------------------------- +/// @name Pending Commands +///-------------------------------------- + + +/*! + Generates a new identifier for a command so that it can be sorted later by this identifier. + */ +- (NSString *)_newIdentifierForCommand:(id)command; + +/*! + This method is triggered on batch processing of the queue. + It will capture the identifiers and use them to execute commands. + + @returns An array of identifiers of all commands that are pending sorted by the order they're enqueued. + */ +- (NSArray *)_pendingCommandIdentifiers; + +/*! + This method should return a command for a given identifier. + + @param identifier An identifier of a command, that was in array returned by <_pendingCommandIdentifiers> + @param error Pointer to `NSError *` that should be set if the method failed to construct/retrieve a command. + + @returns A command that needs to be run, or `nil` if there was an error. + */ +- (id)_commandWithIdentifier:(NSString *)identifier error:(NSError **)error; + +///-------------------------------------- +/// @name Running Commands +///-------------------------------------- + +/*! + This method serves as a way to do any kind of work to enqueue a command properly. + If the task fails with an error/exception or is cancelled - execution won't start. + + @param command Command that needs to be enqueued + @param object The object on which the command is run against. + @param identifier Unique identifier used to represent a command. + @returns Task that is resolved when the command is complete enqueueing. + */ +- (BFTask *)_enqueueCommandInBackground:(id)command + object:(PFObject *)object + identifier:(NSString *)identifier; + +- (BFTask *)_waitForOperationSet:(PFOperationSet *)operationSet + eventuallyPin:(PFEventuallyPin *)eventuallyPin; + +@end diff --git a/Parse/Internal/PFFileManager.h b/Parse/Internal/PFFileManager.h new file mode 100644 index 000000000..ee0dbee7b --- /dev/null +++ b/Parse/Internal/PFFileManager.h @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFExecutor; +@class BFTask; + +typedef NS_OPTIONS(uint8_t, PFFileManagerOptions) { + PFFileManagerOptionSkipBackup = 1 << 0, +}; + +@interface PFFileManager : NSObject + +///-------------------------------------- +/// @name Class +///-------------------------------------- + ++ (BOOL)isApplicationGroupContainerReachableForGroupIdentifier:(NSString *)applicationGroup; + ++ (BFTask *)createDirectoryIfNeededAsyncAtPath:(NSString *)path; ++ (BFTask *)createDirectoryIfNeededAsyncAtPath:(NSString *)path + withOptions:(PFFileManagerOptions)options + executor:(BFExecutor *)executor; + ++ (BFTask *)writeStringAsync:(NSString *)string toFile:(NSString *)filePath; ++ (BFTask *)writeDataAsync:(NSData *)data toFile:(NSString *)filePath; + ++ (BFTask *)copyItemAsyncAtPath:(NSString *)fromPath toPath:(NSString *)toPath; + ++ (BFTask *)moveContentsOfDirectoryAsyncAtPath:(NSString *)fromPath + toDirectoryAtPath:(NSString *)toPath + executor:(BFExecutor *)executor; + ++ (BFTask *)removeItemAtPathAsync:(NSString *)path; ++ (BFTask *)removeItemAtPathAsync:(NSString *)path withFileLock:(BOOL)useFileLock; ++ (BFTask *)removeDirectoryContentsAsyncAtPath:(NSString *)path; + +///-------------------------------------- +/// @name Instance +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithApplicationIdentifier:(NSString *)applicationIdentifier + applicationGroupIdentifier:(NSString *)applicationGroupIdentifier NS_DESIGNATED_INITIALIZER; + +/*! + Returns /Library/Private Documents/Parse + for non-user generated data that shouldn't be deleted by iOS, such as "offline data". + + See https://developer.apple.com/library/ios/#qa/qa1699/_index.html + */ +- (NSString *)parseDefaultDataDirectoryPath; +- (NSString *)parseLocalSandboxDataDirectoryPath; +- (NSString *)parseDataDirectoryPath_DEPRECATED; + +/*! + The path including directories that we save data to for a given filename. + If the file isn't found in the new "Private Documents" location, but is in the old "Documents" location, + moves it and returns the new location. + */ +- (NSString *)parseDataItemPathForPathComponent:(NSString *)pathComponent; + +- (NSString *)parseCacheItemPathForPathComponent:(NSString *)component; + +@end diff --git a/Parse/Internal/PFFileManager.m b/Parse/Internal/PFFileManager.m new file mode 100644 index 000000000..18663a940 --- /dev/null +++ b/Parse/Internal/PFFileManager.m @@ -0,0 +1,356 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFileManager.h" + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFLogging.h" +#import "PFMultiProcessFileLockController.h" + +static NSString *const _PFFileManagerParseDirectoryName = @"Parse"; + +static NSDictionary *_PFFileManagerDefaultDirectoryFileAttributes() { +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + return @{ NSFileProtectionKey : NSFileProtectionCompleteUntilFirstUserAuthentication }; +#else + return nil; +#endif +} + +static NSDataWritingOptions _PFFileManagerDefaultDataWritingOptions() { + NSDataWritingOptions options = NSDataWritingAtomic; +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + options |= NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication; +#endif + return options; +} + +@interface PFFileManager () + +@property (nonatomic, copy) NSString *applicationIdentifier; +@property (nonatomic, copy) NSString *applicationGroupIdentifier; + +@end + +@implementation PFFileManager + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (BOOL)isApplicationGroupContainerReachableForGroupIdentifier:(NSString *)applicationGroup { + return ([[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:applicationGroup] != nil); +} + ++ (BFTask *)writeStringAsync:(NSString *)string toFile:(NSString *)filePath { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + return [self writeDataAsync:data toFile:filePath]; + }]; +} + ++ (BFTask *)writeDataAsync:(NSData *)data toFile:(NSString *)filePath { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSError *error = nil; + [data writeToFile:filePath + options:_PFFileManagerDefaultDataWritingOptions() + error:&error]; + if (error) { + return [BFTask taskWithError:error]; + } + return nil; + }]; +} + ++ (BFTask *)createDirectoryIfNeededAsyncAtPath:(NSString *)path { + return [self createDirectoryIfNeededAsyncAtPath:path + withOptions:PFFileManagerOptionSkipBackup + executor:[BFExecutor defaultPriorityBackgroundExecutor]]; +} + ++ (BFTask *)createDirectoryIfNeededAsyncAtPath:(NSString *)path + withOptions:(PFFileManagerOptions)options + executor:(BFExecutor *)executor { + return [BFTask taskFromExecutor:executor withBlock:^id{ + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSError *error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:path + withIntermediateDirectories:YES + attributes:_PFFileManagerDefaultDirectoryFileAttributes() + error:&error]; + if (error) { + return [BFTask taskWithError:error]; + } + } + +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + if (options & PFFileManagerOptionSkipBackup) { + [self _skipBackupOnPath:path]; + } +#endif + return nil; + }]; +} + ++ (BFTask *)copyItemAsyncAtPath:(NSString *)fromPath toPath:(NSString *)toPath { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSError *error = nil; + [[NSFileManager defaultManager] copyItemAtPath:fromPath toPath:toPath error:&error]; + if (error) { + return [BFTask taskWithError:error]; + } + return nil; + }]; +} + ++ (BFTask *)moveContentsOfDirectoryAsyncAtPath:(NSString *)fromPath + toDirectoryAtPath:(NSString *)toPath + executor:(BFExecutor *)executor { + if ([fromPath isEqualToString:toPath]) { + return [BFTask taskWithResult:nil]; + } + + return [[[self createDirectoryIfNeededAsyncAtPath:toPath + withOptions:PFFileManagerOptionSkipBackup + executor:executor] continueWithSuccessBlock:^id(BFTask *task) { + NSError *error = nil; + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:fromPath + error:&error]; + if (error) { + return [BFTask taskWithError:error]; + } + return contents; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *contents = task.result; + if ([contents count] == 0) { + return nil; + } + + NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:[contents count]]; + for (NSString *path in contents) { + BFTask *task = [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSError *error = nil; + NSString *fromFilePath = [fromPath stringByAppendingPathComponent:path]; + NSString *toFilePath = [toPath stringByAppendingPathComponent:path]; + [[NSFileManager defaultManager] moveItemAtPath:fromFilePath + toPath:toFilePath + error:&error]; + if (error) { + return [BFTask taskWithError:error]; + } + return nil; + }]; + [tasks addObject:task]; + } + return [BFTask taskForCompletionOfAllTasks:tasks]; + }]; +} + ++ (BFTask *)removeDirectoryContentsAsyncAtPath:(NSString *)path { + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:path]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSError *error = nil; + NSArray *fileNames = [fileManager contentsOfDirectoryAtPath:path error:&error]; + if (error) { + PFLogError(PFLoggingTagCommon, @"Failed to list directory: %@", path); + return [BFTask taskWithError:error]; + } + + NSMutableArray *fileTasks = [NSMutableArray array]; + for (NSString *fileName in fileNames) { + NSString *filePath = [path stringByAppendingPathComponent:fileName]; + BFTask *fileTask = [[self removeItemAtPathAsync:filePath withFileLock:NO] continueWithBlock:^id(BFTask *task) { + if (task.faulted) { + PFLogError(PFLoggingTagCommon, @"Failed to delete file: %@ with error: %@", filePath, task.error); + } + return task; + }]; + [fileTasks addObject:fileTask]; + } + return [BFTask taskForCompletionOfAllTasks:fileTasks]; + }] continueWithBlock:^id(BFTask *task) { + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:path]; + return task; + }]; +} + ++ (BFTask *)removeItemAtPathAsync:(NSString *)path { + return [self removeItemAtPathAsync:path withFileLock:YES]; +} + ++ (BFTask *)removeItemAtPathAsync:(NSString *)path withFileLock:(BOOL)useFileLock { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + if (useFileLock) { + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:path]; + } + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:path]) { + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; + if (error) { + if (useFileLock) { + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:path]; + } + return [BFTask taskWithError:error]; + } + } + if (useFileLock) { + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:path]; + } + return nil; + }]; +} + +///-------------------------------------- +#pragma mark - Instance +///-------------------------------------- + +#pragma mark Init + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithApplicationIdentifier:(NSString *)applicationIdentifier + applicationGroupIdentifier:(NSString *)applicationGroupIdentifier { + self = [super init]; + if (!self) return nil; + + _applicationIdentifier = [applicationIdentifier copy]; + _applicationGroupIdentifier = [applicationGroupIdentifier copy]; + + return self; +} + +#pragma mark Public + +- (NSString *)parseDefaultDataDirectoryPath { + // NSHomeDirectory: Returns the path to either the user's or application's + // home directory, depending on the platform. Sandboxed by default on iOS. +#if PARSE_IOS_ONLY + NSString *directoryPath = nil; + if (self.applicationGroupIdentifier) { + NSURL *containerPath = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:self.applicationGroupIdentifier]; + directoryPath = [[containerPath path] stringByAppendingPathComponent:_PFFileManagerParseDirectoryName]; + directoryPath = [directoryPath stringByAppendingPathComponent:self.applicationIdentifier]; + } else { + return [self parseLocalSandboxDataDirectoryPath]; + } +#else + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *directoryPath = [paths firstObject]; + directoryPath = [directoryPath stringByAppendingPathComponent:_PFFileManagerParseDirectoryName]; + directoryPath = [directoryPath stringByAppendingPathComponent:self.applicationIdentifier]; +#endif + + BFTask *createDirectoryTask = [[self class] createDirectoryIfNeededAsyncAtPath:directoryPath + withOptions:PFFileManagerOptionSkipBackup + executor:[BFExecutor immediateExecutor]]; + [createDirectoryTask waitForResult:nil withMainThreadWarning:NO]; + + return directoryPath; +} + +- (NSString *)parseLocalSandboxDataDirectoryPath { +#if TARGET_OS_IPHONE + NSString *library = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"]; + NSString *privateDocuments = [library stringByAppendingPathComponent:@"Private Documents"]; + NSString *directoryPath = [privateDocuments stringByAppendingPathComponent:_PFFileManagerParseDirectoryName]; + BFTask *createDirectoryTask = [[self class] createDirectoryIfNeededAsyncAtPath:directoryPath + withOptions:PFFileManagerOptionSkipBackup + executor:[BFExecutor immediateExecutor]]; + [createDirectoryTask waitForResult:nil withMainThreadWarning:NO]; + + return directoryPath; +#else + return [self parseDefaultDataDirectoryPath]; +#endif +} + +- (NSString *)parseDataDirectoryPath_DEPRECATED { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents folder + NSString *parseDirPath = [documentsDirectory stringByAppendingPathComponent:_PFFileManagerParseDirectoryName]; + + // If this old directory is still on disk, but empty, delete it. + if ([[NSFileManager defaultManager] fileExistsAtPath:parseDirPath]) { + NSError *error = nil; + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:parseDirPath error:&error]; + if (error == nil && [contents count] == 0) { + [[NSFileManager defaultManager] removeItemAtPath:parseDirPath error:nil]; + } + } + + return parseDirPath; +} + +- (NSString *)parseDataItemPathForPathComponent:(NSString *)pathComponent { + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSString *currentLocation = [[self parseDefaultDataDirectoryPath] stringByAppendingPathComponent:pathComponent]; + if (![fileManager fileExistsAtPath:currentLocation]) { + NSString *deprecatedDir = [self parseDataDirectoryPath_DEPRECATED]; + NSString *deprecatedLocation = [deprecatedDir stringByAppendingPathComponent:pathComponent]; + if ([fileManager fileExistsAtPath:deprecatedLocation]) { + [fileManager moveItemAtPath:deprecatedLocation toPath:currentLocation error:nil]; + // If the deprecated dir is still on disk, delete it. + if ([fileManager fileExistsAtPath:deprecatedDir]) { + NSError *error = nil; + NSArray *contents = [fileManager contentsOfDirectoryAtPath:deprecatedDir error:&error]; + if (!error && [contents count] == 0) { + [fileManager removeItemAtPath:deprecatedDir error:nil]; + } + } + } + } + return currentLocation; +} + +- (NSString *)parseCacheItemPathForPathComponent:(NSString *)component { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *folderPath = [paths firstObject]; + folderPath = [folderPath stringByAppendingPathComponent:_PFFileManagerParseDirectoryName]; +#if !TARGET_OS_IPHONE + // We append the applicationId in case the OS X application isn't sandboxed, + // to avoid collisions in the generic ~/Library/Caches/Parse/------ dir. + folderPath = [folderPath stringByAppendingPathComponent:self.applicationIdentifier]; +#endif + folderPath = [folderPath stringByAppendingPathComponent:component]; + return [folderPath stringByStandardizingPath]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +// Skips all backups on the provided path. ++ (BOOL)_skipBackupOnPath:(NSString *)path { + if (path == nil) { + return NO; + } + + NSError *error = nil; + + NSURL *url = [NSURL fileURLWithPath:path]; + BOOL success = [url setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error]; + if (!success) { + PFLogError(PFLoggingTagCommon, + @"Unable to exclude %@ from backup with error: %@", [url lastPathComponent], error); + } + + return success; +} + +@end diff --git a/Parse/Internal/PFGeoPointPrivate.h b/Parse/Internal/PFGeoPointPrivate.h new file mode 100644 index 000000000..4a84da051 --- /dev/null +++ b/Parse/Internal/PFGeoPointPrivate.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +extern const double EARTH_RADIUS_MILES; +extern const double EARTH_RADIUS_KILOMETERS; + +@class PFGeoPoint; + +@interface PFGeoPoint (Private) + +// Internal commands + +/* + Gets the encoded format for an GeoPoint. + */ +- (NSDictionary *)encodeIntoDictionary; + +/*! + Creates an GeoPoint from its encoded format. + */ ++ (PFGeoPoint *)geoPointWithDictionary:(NSDictionary *)dictionary; + +@end diff --git a/Parse/Internal/PFHash.h b/Parse/Internal/PFHash.h new file mode 100644 index 000000000..e97af69c7 --- /dev/null +++ b/Parse/Internal/PFHash.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +extern NSUInteger PFIntegerPairHash(NSUInteger a, NSUInteger b); + +extern NSUInteger PFDoublePairHash(double a, double b); + +extern NSUInteger PFDoubleHash(double d); + +extern NSUInteger PFLongHash(unsigned long long l); + +extern NSString *PFMD5HashFromData(NSData *data); +extern NSString *PFMD5HashFromString(NSString *string); diff --git a/Parse/Internal/PFHash.m b/Parse/Internal/PFHash.m new file mode 100644 index 000000000..20ed7dcad --- /dev/null +++ b/Parse/Internal/PFHash.m @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHash.h" + +#import + +// +// Thank you Thomas Wang for 32/64 bit mix hash +// http://www.concentric.net/~Ttwang/tech/inthash.htm +// +// Above link is now dead, please visit +// http://web.archive.org/web/20121102023700/http://www.concentric.net/~Ttwang/tech/inthash.htm +// + +extern NSUInteger PFIntegerPairHash(NSUInteger a, NSUInteger b) { + return PFLongHash(((unsigned long long)a) << 32 | b); +} + +extern NSUInteger PFDoublePairHash(double a, double b) { + return PFIntegerPairHash(PFDoubleHash(a), PFDoubleHash(b)); +} + +extern NSUInteger PFDoubleHash(double d) { + union { + double key; + uint64_t bits; + } u; + u.key = d; + return PFLongHash(u.bits); +} + +extern NSUInteger PFLongHash(unsigned long long l) { + l = (~l) + (l << 18); // key = (key << 18) - key - 1; + l ^= (l >> 31); + l *= 21; // key = (key + (key << 2)) + (key << 4); + l ^= (l >> 11); + l += (l << 6); + l ^= (l >> 22); + return (NSUInteger)l; +} + +extern NSString *PFMD5HashFromData(NSData *data) { + unsigned char md[CC_MD5_DIGEST_LENGTH]; + + __block CC_MD5_CTX _ctx; + CC_MD5_Init(&_ctx); + [data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) { + CC_MD5_Update(&_ctx , bytes, (CC_LONG)byteRange.length); + }]; + CC_MD5_Final(md, &_ctx); + + NSString *string = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + md[0], md[1], + md[2], md[3], + md[4], md[5], + md[6], md[7], + md[8], md[9], + md[10], md[11], + md[12], md[13], + md[14], md[15]]; + return string; +} + +extern NSString *PFMD5HashFromString(NSString *string) { + return PFMD5HashFromData([string dataUsingEncoding:NSUTF8StringEncoding]); +} diff --git a/Parse/Internal/PFInternalUtils.h b/Parse/Internal/PFInternalUtils.h new file mode 100644 index 000000000..6602e413d --- /dev/null +++ b/Parse/Internal/PFInternalUtils.h @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#if TARGET_OS_IPHONE +# import +# import "PFPushPrivate.h" +#else +# import +#endif + +#import "PFEncoder.h" + +@class PFFileManager; +@class PFKeychainStore; +@class PFNetworkCommand; + +@interface PFInternalUtils : NSObject + ++ (NSString *)parseServerURLString; ++ (void)setParseServer:(NSString *)server; + ++ (NSNumber *)fileSizeOfFileAtPath:(NSString *)filePath error:(NSError **)error; + +/** + Clears system time zone cache, gets the name of the time zone + and caches it. This method is completely thread-safe. + */ ++ (NSString *)currentSystemTimeZoneName; + +/** + * Performs selector on the target, only if the target and selector are non-nil, + * as well as target responds to selector + */ ++ (void)safePerformSelector:(SEL)selector withTarget:(id)target object:(id)object object:(id)anotherObject; + ++ (NSNumber *)addNumber:(NSNumber *)first withNumber:(NSNumber *)second; + +// +// Given an NSDictionary/NSArray/NSNumber/NSString even nested ones +// Generates a cache key that can be used to identify this object ++ (NSString *)cacheKeyForObject:(id)object; + +/**! + * Does a deep traversal of every item in object, calling block on every one. + * @param object The object or array to traverse deeply. + * @param block The block to call for every item. It will be passed the item + * as an argument. If it returns a truthy value, that value will replace the + * item in its parent container. + * @return The result of calling block on the top-level object itself. + **/ ++ (id)traverseObject:(id)object usingBlock:(id (^)(id object))block; + +/*! + This method will split an array into multiple arrays, each with up to maximum components count. + + @param array Array to split. + @param components Number of components that should be used as a max per each subarray. + + @return Array of arrays constructed by splitting the array. + */ ++ (NSArray *)arrayBySplittingArray:(NSArray *)array withMaximumComponentsPerSegment:(NSUInteger)components; + ++ (id)_stringWithFormat:(NSString *)format arguments:(NSArray *)arguments; +@end + +@interface PFJSONCacheItem : NSObject + +@property (nonatomic, copy, readonly) NSString *hashValue; + +- (instancetype)initWithObject:(id)object; ++ (PFJSONCacheItem *)cacheFromObject:(id)object; + +@end diff --git a/Parse/Internal/PFInternalUtils.m b/Parse/Internal/PFInternalUtils.m new file mode 100644 index 000000000..66f0f705c --- /dev/null +++ b/Parse/Internal/PFInternalUtils.m @@ -0,0 +1,303 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInternalUtils.h" + +#include +#include + +#import + +#import "PFACLPrivate.h" +#import "PFAssert.h" +#import "PFDateFormatter.h" +#import "BFTask+Private.h" +#import "PFFieldOperation.h" +#import "PFFile_Private.h" +#import "PFGeoPointPrivate.h" +#import "PFKeyValueCache.h" +#import "PFKeychainStore.h" +#import "PFLogging.h" +#import "PFEncoder.h" +#import "PFObjectPrivate.h" +#import "PFRelationPrivate.h" +#import "PFUserPrivate.h" +#import "Parse.h" +#import "PFFileManager.h" +#import "PFJSONSerialization.h" +#import "PFMultiProcessFileLockController.h" +#import "PFHash.h" + +#if PARSE_IOS_ONLY +#import "PFNetworkActivityIndicatorManager.h" +#import "PFProduct.h" +#endif + +static NSString *parseServer_; + +@implementation PFInternalUtils + ++ (void)initialize { + if (self == [PFInternalUtils class]) { + [self setParseServer:kPFParseServer]; + +#if PARSE_IOS_ONLY + [PFNetworkActivityIndicatorManager sharedManager].enabled = YES; +#endif + } +} + ++ (NSString *)parseServerURLString { + return parseServer_; +} + +// Useful for testing. +// Beware of race conditions if you call setParseServer while something else may be using +// httpClient. ++ (void)setParseServer:(NSString *)server { + parseServer_ = [server copy]; +} + ++ (NSString *)currentSystemTimeZoneName { + static NSLock *methodLock; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + methodLock = [[NSLock alloc] init]; + }); + + [methodLock lock]; + [NSTimeZone resetSystemTimeZone]; + NSString *systemTimeZoneName = [[NSTimeZone systemTimeZone].name copy]; + [methodLock unlock]; + + return systemTimeZoneName; +} + ++ (void)safePerformSelector:(SEL)selector withTarget:(id)target object:(id)object object:(id)anotherObject { + if (target == nil || selector == nil || ![target respondsToSelector:selector]) { + return; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [target performSelector:selector withObject:object withObject:anotherObject]; +#pragma clang diagnostic pop +} + ++ (NSNumber *)fileSizeOfFileAtPath:(NSString *)filePath error:(NSError **)error { + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath + error:error]; + return attributes[NSFileSize]; +} + +///-------------------------------------- +#pragma mark - Serialization +///-------------------------------------- + ++ (NSNumber *)addNumber:(NSNumber *)first withNumber:(NSNumber *)second { + const char *objcType = [first objCType]; + + if (strcmp(objcType, @encode(BOOL)) == 0) { + return @([first boolValue] + [second boolValue]); + } else if (strcmp(objcType, @encode(char)) == 0) { + return @([first charValue] + [second charValue]); + } else if (strcmp(objcType, @encode(double)) == 0) { + return @([first doubleValue] + [second doubleValue]); + } else if (strcmp(objcType, @encode(float)) == 0) { + return @([first floatValue] + [second floatValue]); + } else if (strcmp(objcType, @encode(int)) == 0) { + return @([first intValue] + [second intValue]); + } else if (strcmp(objcType, @encode(long)) == 0) { + return @([first longValue] + [second longValue]); + } else if (strcmp(objcType, @encode(long long)) == 0) { + return @([first longLongValue] + [second longLongValue]); + } else if (strcmp(objcType, @encode(short)) == 0) { + return @([first shortValue] + [second shortValue]); + } else if (strcmp(objcType, @encode(unsigned char)) == 0) { + return @([first unsignedCharValue] + [second unsignedCharValue]); + } else if (strcmp(objcType, @encode(unsigned int)) == 0) { + return @([first unsignedIntValue] + [second unsignedIntValue]); + } else if (strcmp(objcType, @encode(unsigned long)) == 0) { + return @([first unsignedLongValue] + [second unsignedLongValue]); + } else if (strcmp(objcType, @encode(unsigned long long)) == 0) { + return @([first unsignedLongLongValue] + [second unsignedLongLongValue]); + } else if (strcmp(objcType, @encode(unsigned short)) == 0) { + return @([first unsignedShortValue] + [second unsignedShortValue]); + } + + // Fall back to int? + return @([first intValue] + [second intValue]); +} + +///-------------------------------------- +#pragma mark - CacheKey +///-------------------------------------- + +#pragma mark Public + ++ (NSString *)cacheKeyForObject:(id)object { + NSMutableString *string = [NSMutableString string]; + [self appendObject:object toString:string]; + return string; +} + +#pragma mark Private + ++ (void)appendObject:(id)object toString:(NSMutableString *)string { + if ([object isKindOfClass:[NSDictionary class]]) { + [self appendDictionary:object toString:string]; + } else if ([object isKindOfClass:[NSArray class]]) { + [self appendArray:object toString:string]; + } else if ([object isKindOfClass:[NSString class]]) { + [string appendFormat:@"\"%@\"", object]; + } else if ([object isKindOfClass:[NSNumber class]]) { + [self appendNumber:object toString:string]; + } else if ([object isKindOfClass:[NSNull class]]) { + [self appendNullToString:string]; + } else { + [NSException raise:NSInvalidArgumentException + format:@"Couldn't create cache key from %@", object]; + } +} + ++ (void)appendDictionary:(NSDictionary *)dictionary toString:(NSMutableString *)string { + [string appendString:@"{"]; + + NSArray *keys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *key in keys) { + [string appendFormat:@"%@:", key]; + + id value = [dictionary objectForKey:key]; + [self appendObject:value toString:string]; + + [string appendString:@","]; + } + + [string appendString:@"}"]; +} + ++ (void)appendArray:(NSArray *)array toString:(NSMutableString *)string { + [string appendString:@"["]; + for (id object in array) { + [self appendObject:object toString:string]; + [string appendString:@","]; + } + [string appendString:@"]"]; +} + ++ (void)appendNumber:(NSNumber *)number toString:(NSMutableString *)string { + [string appendFormat:@"%@", [number stringValue]]; +} + ++ (void)appendNullToString:(NSMutableString *)string { + [string appendString:@"null"]; +} + ++ (id)traverseObject:(id)object usingBlock:(id (^)(id object))block seenObjects:(NSMutableSet *)seen { + if ([object isKindOfClass:[PFObject class]]) { + if ([seen containsObject:object]) { + // We've already visited this object in this call. + return object; + } + [seen addObject:object]; + + for (NSString *key in [(PFObject *)object allKeys]) { + [self traverseObject:object[key] usingBlock:block seenObjects:seen]; + } + + return block(object); + } else if ([object isKindOfClass:[NSArray class]]) { + NSMutableArray *newArray = [object mutableCopy]; + [object enumerateObjectsUsingBlock:^(id child, NSUInteger idx, BOOL *stop) { + id newChild = [self traverseObject:child usingBlock:block seenObjects:seen]; + if (newChild) { + newArray[idx] = newChild; + } + }]; + return block(newArray); + } else if ([object isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *newDictionary = [object mutableCopy]; + [object enumerateKeysAndObjectsUsingBlock:^(id key, id child, BOOL *stop) { + id newChild = [self traverseObject:child usingBlock:block seenObjects:seen]; + if (newChild) { + newDictionary[key] = newChild; + } + }]; + return block(newDictionary); + } + + return block(object); +} + ++ (id)traverseObject:(id)object usingBlock:(id (^)(id object))block { + NSMutableSet *seen = [[NSMutableSet alloc] init]; + id result = [self traverseObject:object usingBlock:block seenObjects:seen]; + return result; +} + ++ (NSArray *)arrayBySplittingArray:(NSArray *)array withMaximumComponentsPerSegment:(NSUInteger)components { + if ([array count] <= components) { + return @[ array ]; + } + + NSMutableArray *splitArray = [NSMutableArray array]; + NSInteger index = 0; + + while (index < [array count]) { + NSInteger length = MIN([array count] - index, components); + + NSArray *subarray = [array subarrayWithRange:NSMakeRange(index, length)]; + [splitArray addObject:subarray]; + + index += length; + } + + return splitArray; +} + ++ (id)_stringWithFormat:(NSString *)format arguments:(NSArray *)arguments { + // We cannot reliably construct a va_list for 64-bit, so hard code up to N args. + const int maxNumArgs = 10; + PFRangeAssert(arguments.count <= maxNumArgs, @"Maximum of %d format args allowed", maxNumArgs); + NSMutableArray *args = [arguments mutableCopy]; + for (NSUInteger i = arguments.count; i < maxNumArgs; i++) { + [args addObject:@""]; + } + return [NSString stringWithFormat:format, + args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]]; +} + +@end + +// A PFJSONCacheItem is a pairing of a json string with its hash value. +// This is used by our mutable container checking. +@implementation PFJSONCacheItem + +- (instancetype)initWithObject:(id)object { + if (self = [super init]) { + NSObject *encoded = [[PFPointerOrLocalIdObjectEncoder objectEncoder] encodeObject:object]; + NSData *jsonData = [PFJSONSerialization dataFromJSONObject:encoded]; + _hashValue = PFMD5HashFromData(jsonData); + } + return self; +} + +- (BOOL)isEqual:(id)otherCache { + if (![otherCache isKindOfClass:[PFJSONCacheItem class]]) { + return NO; + } + + return [self.hashValue isEqualToString:[otherCache hashValue]]; +} + ++ (PFJSONCacheItem *)cacheFromObject:(id)object { + return [[PFJSONCacheItem alloc] initWithObject:object]; +} + +@end diff --git a/Parse/Internal/PFJSONSerialization.h b/Parse/Internal/PFJSONSerialization.h new file mode 100644 index 000000000..b921dd434 --- /dev/null +++ b/Parse/Internal/PFJSONSerialization.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFJSONSerialization : NSObject + +/*! + The object passed in must be one of: + * NSString + * NSNumber + * NSDictionary + * NSArray + * NSNull + + @returns NSData of JSON representing the passed in object. + */ ++ (NSData *)dataFromJSONObject:(id)object; + +/*! + The object passed in must be one of: + * NSString + * NSNumber + * NSDictionary + * NSArray + * NSNull + + @returns NSString of JSON representing the passed in object. + */ ++ (NSString *)stringFromJSONObject:(id)object; + +/*! + Takes a JSON string and returns the NSDictionaries and NSArrays in it. + You should still call decodeObject if you want Parse types. + */ ++ (id)JSONObjectFromData:(NSData *)data; + +/*! + Takes a JSON string and returns the NSDictionaries and NSArrays in it. + You should still call decodeObject if you want Parse types. + */ ++ (id)JSONObjectFromString:(NSString *)string; + +@end diff --git a/Parse/Internal/PFJSONSerialization.m b/Parse/Internal/PFJSONSerialization.m new file mode 100644 index 000000000..5958019cb --- /dev/null +++ b/Parse/Internal/PFJSONSerialization.m @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFJSONSerialization.h" + +#import "PFLogging.h" + +@implementation PFJSONSerialization + ++ (NSData *)dataFromJSONObject:(id)object { + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error]; + if (!data || error != nil) { + [NSException raise:NSInvalidArgumentException + format:@"PFObject values must be serializable to JSON"]; + } + return data; +} + ++ (NSString *)stringFromJSONObject:(id)object { + NSData *data = [self dataFromJSONObject:object]; + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + ++ (id)JSONObjectFromData:(NSData *)data { + NSError *error = nil; + id object = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&error]; + if (!object || error != nil) { + PFLogError(PFLoggingTagCommon, @"JSON deserialization failed with error: %@", [error description]); + } + + return object; +} + ++ (id)JSONObjectFromString:(NSString *)string { + return [self JSONObjectFromData:[string dataUsingEncoding:NSUTF8StringEncoding]]; +} + +@end diff --git a/Parse/Internal/PFKeychainStore.h b/Parse/Internal/PFKeychainStore.h new file mode 100644 index 000000000..3d7c06e00 --- /dev/null +++ b/Parse/Internal/PFKeychainStore.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const PFKeychainStoreDefaultService; + +/*! + PFKeychainStore is NSUserDefaults-like wrapper on top of Keychain. + It supports any object, with NSCoding support. Every object is serialized using NSKeyedArchiver. + + All objects are available after the first device unlock and are not backed up. + */ +@interface PFKeychainStore : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithService:(NSString *)service NS_DESIGNATED_INITIALIZER; + +- (nullable id)objectForKey:(NSString *)key; +- (nullable id)objectForKeyedSubscript:(NSString *)key; + +- (BOOL)setObject:(nullable id)object forKey:(NSString *)key; +- (BOOL)setObject:(nullable id)object forKeyedSubscript:(NSString *)key; +- (BOOL)removeObjectForKey:(NSString *)key; +- (BOOL)removeAllObjects; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFKeychainStore.m b/Parse/Internal/PFKeychainStore.m new file mode 100644 index 000000000..747ff105b --- /dev/null +++ b/Parse/Internal/PFKeychainStore.m @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFKeychainStore.h" + +#import "PFAssert.h" +#import "PFLogging.h" +#import "PFMacros.h" +#import "Parse.h" + +NSString *const PFKeychainStoreDefaultService = @"com.parse.sdk"; + +@interface PFKeychainStore () { + dispatch_queue_t _synchronizationQueue; +} + +@property (nonatomic, copy, readonly) NSString *service; +@property (nonatomic, copy, readonly) NSDictionary *keychainQueryTemplate; + +@end + +@implementation PFKeychainStore + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (NSDictionary *)_keychainQueryTemplateForService:(NSString *)service { + NSMutableDictionary *query = [NSMutableDictionary dictionary]; + if ([service length]) { + query[(__bridge NSString *)kSecAttrService] = service; + } + query[(__bridge NSString *)kSecClass] = (__bridge id)kSecClassGenericPassword; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wtautological-pointer-compare" + if (&kSecAttrAccessible != nil) { + query[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly; + } +#pragma clang diagnostic pop + + return [query copy]; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithService:(NSString *)service { + self = [super init]; + if (!self) return nil; + + _service = service; + _keychainQueryTemplate = [[self class] _keychainQueryTemplateForService:service]; + + NSString *queueLabel = [NSString stringWithFormat:@"com.parse.keychain.%@", service]; + _synchronizationQueue = dispatch_queue_create([queueLabel UTF8String], DISPATCH_QUEUE_CONCURRENT); + PFMarkDispatchQueue(_synchronizationQueue); + + return self; +} + +///-------------------------------------- +#pragma mark - Read +///-------------------------------------- + +- (id)objectForKey:(NSString *)key { + __block NSData *data = nil; + dispatch_sync(_synchronizationQueue, ^{ + data = [self _dataForKey:key]; + }); + + if (data) { + id object = nil; + @try { + object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + } + @catch (NSException *exception) {} + + return object; + } + return nil; +} + +- (id)objectForKeyedSubscript:(NSString *)key { + return [self objectForKey:key]; +} + +- (NSData *)_dataForKey:(NSString *)key { + NSMutableDictionary *query = [self.keychainQueryTemplate mutableCopy]; + + query[(__bridge NSString *)kSecAttrAccount] = key; + query[(__bridge NSString *)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; + query[(__bridge NSString *)kSecReturnData] = (__bridge id)kCFBooleanTrue; + + //recover data + CFDataRef data = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&data); + if (status != errSecSuccess && status != errSecItemNotFound) { + PFLogError(PFLoggingTagCommon, + @"PFKeychainStore failed to get object for key '%@', with error: %ld", key, (long)status); + } + return CFBridgingRelease(data); +} + +///-------------------------------------- +#pragma mark - Write +///-------------------------------------- + +- (BOOL)setObject:(id)object forKey:(NSString *)key { + NSParameterAssert(key != nil); + + if (!object) { + return [self removeObjectForKey:key]; + } + + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object]; + if (!data) { + return NO; + } + + NSMutableDictionary *query = [self.keychainQueryTemplate mutableCopy]; + query[(__bridge NSString *)kSecAttrAccount] = key; + + NSDictionary *update = @{ (__bridge NSString *)kSecValueData : data }; + + __block OSStatus status = errSecSuccess; + dispatch_barrier_sync(_synchronizationQueue,^{ + if ([self _dataForKey:key]) { + status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update); + } else { + [query addEntriesFromDictionary:update]; + status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); + } + }); + + if (status != errSecSuccess) { + PFLogError(PFLoggingTagCommon, + @"PFKeychainStore failed to set object for key '%@', with error: %ld", key, (long)status); + } + + return (status == errSecSuccess); +} + +- (BOOL)setObject:(id)object forKeyedSubscript:(NSString *)key { + return [self setObject:object forKey:key]; +} + +- (BOOL)removeObjectForKey:(NSString *)key { + __block BOOL value = NO; + dispatch_barrier_sync(_synchronizationQueue, ^{ + value = [self _removeObjectForKey:key]; + }); + return value; +} + +- (BOOL)_removeObjectForKey:(NSString *)key { + PFAssertIsOnDispatchQueue(_synchronizationQueue); + NSMutableDictionary *query = [self.keychainQueryTemplate mutableCopy]; + query[(__bridge NSString *)kSecAttrAccount] = key; + + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); + return (status == errSecSuccess); +} + +- (BOOL)removeAllObjects { + NSMutableDictionary *query = [self.keychainQueryTemplate mutableCopy]; + query[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue; + query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll; + + __block BOOL value = YES; + dispatch_barrier_sync(_synchronizationQueue, ^{ + CFArrayRef result = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); + if (status != errSecSuccess) { + return; + } + + for (NSDictionary *item in CFBridgingRelease(result)) { + NSString *key = item[(__bridge id)kSecAttrAccount]; + value = [self _removeObjectForKey:key]; + if (!value) { + return; + } + } + }); + return value; +} + +@end diff --git a/Parse/Internal/PFLocationManager.h b/Parse/Internal/PFLocationManager.h new file mode 100644 index 000000000..b1f115c65 --- /dev/null +++ b/Parse/Internal/PFLocationManager.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class CLLocation; +@class CLLocationManager; + +#if TARGET_OS_IPHONE + +@class UIApplication; + +#endif + +typedef void(^PFLocationManagerLocationUpdateBlock)(CLLocation *location, NSError *error); + +/*! + PFLocationManager is an internal class which wraps a CLLocationManager and + returns an updated CLLocation via the provided block. + + When -addBlockForCurrentLocation is called, the CLLocationManager's + -startUpdatingLocations is called, and upon CLLocationManagerDelegate callback + (either success or failure), any handlers that were passed to this class will + be called _once_ with the updated location, then removed. The CLLocationManager + stopsUpdatingLocation upon a single failure or success case, so that the next + location request is guaranteed a speedily returned CLLocation. + */ +@interface PFLocationManager : NSObject + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithSystemLocationManager:(CLLocationManager *)manager; + +#if TARGET_OS_IPHONE + +- (instancetype)initWithSystemLocationManager:(CLLocationManager *)manager + application:(UIApplication *)application + bundle:(NSBundle *)bundle NS_DESIGNATED_INITIALIZER; + +#endif + +///-------------------------------------- +#pragma mark - Current Location +///-------------------------------------- + +- (void)addBlockForCurrentLocation:(PFLocationManagerLocationUpdateBlock)handler; + +@end diff --git a/Parse/Internal/PFLocationManager.m b/Parse/Internal/PFLocationManager.m new file mode 100644 index 000000000..c8c11c251 --- /dev/null +++ b/Parse/Internal/PFLocationManager.m @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFLocationManager.h" + +#import + +#import "PFConstants.h" +#import "PFGeoPoint.h" + +#if !TARGET_OS_IPHONE + +// To let us compile for OSX. +@compatibility_alias UIApplication NSApplication; + +#endif + +@interface PFLocationManager () + +@property (nonatomic, strong) CLLocationManager *locationManager; +@property (nonatomic, strong) NSBundle *bundle; +@property (nonatomic, strong) UIApplication *application; + +// We use blocks and not BFTasks because Tasks don't gain us much - we still +// have to manually hold onto them so that they can be resolved in the +// CLLocationManager callback. +@property (nonatomic, strong) NSMutableSet *blockSet; + +@end + +@implementation PFLocationManager + +///-------------------------------------- +#pragma mark - CLLocationManager +///-------------------------------------- + ++ (CLLocationManager *)_newSystemLocationManager { + __block CLLocationManager *manager = nil; + + // CLLocationManager should be created only on main thread, as it needs a run loop to serve delegate callbacks + dispatch_block_t block = ^{ + manager = [[CLLocationManager alloc] init]; + }; + if ([[NSThread currentThread] isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } + return manager; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + CLLocationManager *manager = [[self class] _newSystemLocationManager]; + return [self initWithSystemLocationManager:manager]; +} + +- (instancetype)initWithSystemLocationManager:(CLLocationManager *)manager { + return [self initWithSystemLocationManager:manager + application:[UIApplication sharedApplication] + bundle:[NSBundle mainBundle]]; +} + +- (instancetype)initWithSystemLocationManager:(CLLocationManager *)manager + application:(UIApplication *)application + bundle:(NSBundle *)bundle { + self = [super init]; + if (!self) return nil; + + _blockSet = [NSMutableSet setWithCapacity:1]; + _locationManager = manager; + _locationManager.delegate = self; + _bundle = bundle; + _application = application; + + return self; +} + +///-------------------------------------- +#pragma mark - Dealloc +///-------------------------------------- + +- (void)dealloc { + _locationManager.delegate = nil; +} + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + +- (void)addBlockForCurrentLocation:(PFLocationManagerLocationUpdateBlock)handler { + @synchronized (self.blockSet) { + [self.blockSet addObject:[handler copy]]; + } + +#if TARGET_OS_IPHONE + if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) { + + if (self.application.applicationState != UIApplicationStateBackground && + [self.bundle objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] != nil) { + [self.locationManager requestWhenInUseAuthorization]; + } else { + [self.locationManager requestAlwaysAuthorization]; + } + } +#endif + + [self.locationManager startUpdatingLocation]; +} + +///-------------------------------------- +#pragma mark - CLLocationManagerDelegate +///-------------------------------------- + +// TODO: (nlutsenko) Remove usage of this method, when we drop support for OSX 10.8 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)locationManager:(CLLocationManager *)manager + didUpdateToLocation:(CLLocation *)newLocation + fromLocation:(CLLocation *)oldLocation { + [manager stopUpdatingLocation]; + + NSMutableSet *callbacks = [NSMutableSet setWithCapacity:1]; + @synchronized (self.blockSet) { + [callbacks setSet:self.blockSet]; + [self.blockSet removeAllObjects]; + } + for (void(^block)(CLLocation *, NSError *) in callbacks) { + block(newLocation, nil); + } +} +#pragma clang diagnostic pop + +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + CLLocation *location = [locations lastObject]; + CLLocation *oldLocation = [locations count] > 1 ? [locations objectAtIndex:[locations count] - 2] : nil; + + // TODO: (nlutsenko) Remove usage of this method, when we drop support for OSX 10.8 (didUpdateLocations is 10.9+) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self locationManager:manager didUpdateToLocation:location fromLocation:oldLocation]; +#pragma clang diagnostic pop +} + +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { + [manager stopUpdatingLocation]; + + NSMutableSet *callbacks = nil; + @synchronized (self.blockSet) { + callbacks = [self.blockSet copy]; + [self.blockSet removeAllObjects]; + } + for (PFLocationManagerLocationUpdateBlock block in callbacks) { + block(nil, error); + } +} + +@end diff --git a/Parse/Internal/PFLogger.h b/Parse/Internal/PFLogger.h new file mode 100644 index 000000000..a69495db9 --- /dev/null +++ b/Parse/Internal/PFLogger.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +typedef uint8_t PFLoggingTag; + +@interface PFLogger : NSObject + +@property (atomic, assign) PFLogLevel logLevel; + +///-------------------------------------- +/// @name Shared Logger +///-------------------------------------- + +/*! +A shared instance of `PFLogger` that should be used for all logging. + +@returns An shared singleton instance of `PFLogger`. +*/ ++ (instancetype)sharedLogger; + +///-------------------------------------- +/// @name Logging Messages +///-------------------------------------- + +/*! + Logs a message at a specific level for a tag. + If current logging level doesn't include this level - this method does nothing. + + @param level Logging Level + @param tag Logging Tag + @param format Format to use for the log message. + */ +- (void)logMessageWithLevel:(PFLogLevel)level + tag:(PFLoggingTag)tag + format:(NSString *)format, ... NS_FORMAT_FUNCTION(3, 4); + +@end diff --git a/Parse/Internal/PFLogger.m b/Parse/Internal/PFLogger.m new file mode 100644 index 000000000..d309096a8 --- /dev/null +++ b/Parse/Internal/PFLogger.m @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFLogger.h" + +#import "PFApplication.h" +#import "PFLogging.h" + +@implementation PFLogger + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (NSString *)_descriptionForLoggingTag:(PFLoggingTag)tag { + NSString *description = nil; + switch (tag) { + case PFLoggingTagCommon: + break; + case PFLoggingTagCrashReporting: + description = @"Crash Reporting"; + break; + default: + break; + } + return description; +} + ++ (NSString *)_descriptionForLogLevel:(PFLogLevel)logLevel { + NSString *description = nil; + switch (logLevel) { + case PFLogLevelNone: + break; + case PFLogLevelError: + description = @"Error"; + break; + case PFLogLevelWarning: + description = @"Warning"; + break; + case PFLogLevelInfo: + description = @"Info"; + break; + default: + break; + } + return description; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)sharedLogger { + static PFLogger *logger; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + logger = [[PFLogger alloc] init]; + }); + return logger; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _logLevel = ([PFApplication currentApplication].appStoreEnvironment ? PFLogLevelNone : PFLogLevelWarning); + + return self; +} + +///-------------------------------------- +#pragma mark - Logging Messages +///-------------------------------------- + +- (void)logMessageWithLevel:(PFLogLevel)level + tag:(PFLoggingTag)tag + format:(NSString *)format, ... NS_FORMAT_FUNCTION(3, 4) { + if (level > self.logLevel || level == PFLogLevelNone || !format) { + return; + } + + va_list args; + va_start(args, format); + + NSMutableString *message = [NSMutableString stringWithFormat:@"[%@]", [[self class] _descriptionForLogLevel:level]]; + + NSString *tagDescription = [[self class] _descriptionForLoggingTag:tag]; + if (tagDescription) { + [message appendFormat:@"[%@]", tagDescription]; + } + + [message appendFormat:@": %@", format]; + + NSLogv(message, args); + + va_end(args); +} + +@end diff --git a/Parse/Internal/PFLogging.h b/Parse/Internal/PFLogging.h new file mode 100644 index 000000000..1d1f3da19 --- /dev/null +++ b/Parse/Internal/PFLogging.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef Parse_PFLogging_h +#define Parse_PFLogging_h + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +#import "PFLogger.h" + +static const PFLoggingTag PFLoggingTagCommon = 0; +static const PFLoggingTag PFLoggingTagCrashReporting = 100; + +#define PFLog(level, loggingTag, frmt, ...) \ +[[PFLogger sharedLogger] logMessageWithLevel:level tag:loggingTag format:(frmt), ##__VA_ARGS__] + +#define PFLogError(tag, frmt, ...) \ +PFLog(PFLogLevelError, (tag), (frmt), ##__VA_ARGS__) + +#define PFLogWarning(tag, frmt, ...) \ +PFLog(PFLogLevelWarning, (tag), (frmt), ##__VA_ARGS__) + +#define PFLogInfo(tag, frmt, ...) \ +PFLog(PFLogLevelInfo, (tag), (frmt), ##__VA_ARGS__) + +#define PFLogDebug(tag, frmt, ...) \ +PFLog(PFLogLevelDebug, (tag), (frmt), ##__VA_ARGS__) + +#define PFLogException(exception) \ +PFLogError(PFLoggingTagCommon, @"Caught \"%@\" with reason \"%@\"%@", \ +exception.name, exception, \ +[exception callStackSymbols] ? [NSString stringWithFormat:@":\n%@.", [exception callStackSymbols]] : @"") + +#endif diff --git a/Parse/Internal/PFMacros.h b/Parse/Internal/PFMacros.h new file mode 100644 index 000000000..68725ed24 --- /dev/null +++ b/Parse/Internal/PFMacros.h @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#ifndef Parse_PFMacros_h +#define Parse_PFMacros_h + +/*! + This macro allows to create NSSet via subscript. + */ +#define PF_SET(...) [NSSet setWithObjects:__VA_ARGS__, nil] + +/*! + This macro is a handy thing for converting libSystem objects to (void *) pointers. + If you are targeting OSX 10.8+ and iOS 6.0+ - this is no longer required. + */ +#if OS_OBJECT_USE_OBJC + #define PFOSObjectPointer(object) \ + (__bridge void *)(object) +#else + #define PFOSObjectPointer(object) \ + (void *)(object) +#endif + +/*! + Mark a queue in order to be able to check PFAssertIsOnMarkedQueue. + */ +#define PFMarkDispatchQueue(queue) \ +dispatch_queue_set_specific((queue), \ + PFOSObjectPointer(queue), \ + PFOSObjectPointer(queue), \ + NULL) + +///-------------------------------------- +/// @name Memory Management +/// +/// The following macros are influenced and include portions of libextobjc. +///-------------------------------------- + +/*! + Creates a __weak version of the variable provided, + which can later be safely used or converted into strong variable via @strongify. + */ +#define weakify(var) \ +try {} @catch (...) {} \ +__weak __typeof__(var) var ## _weak = var; + +/*! + Creates a strong shadow reference of the variable provided. + Variable must have previously been passed to @weakify. + */ +#define strongify(var) \ +try {} @catch (...) {} \ +__strong __typeof__(var) var = var ## _weak; + +///-------------------------------------- +/// @name KVC +///-------------------------------------- + +/*! + This macro ensures that object.key exists at compile time. + It can accept a chained key path. + */ +#define keypath(TYPE, PATH) \ +(((void)(NO && ((void)((TYPE *)(nil)).PATH, NO)), # PATH)) + +///-------------------------------------- +/// @name Runtime +///-------------------------------------- + +/*! + Using objc_msgSend directly is bad, very bad. Doing so without casting could result in stack-smashing on architectures + (*cough* x86 *cough*) that use strange methods of returning values of different types. + + The objc_msgSend_safe macro ensures that we properly cast the function call to use the right conventions when passing + parameters and getting return values. This also fixes some issues with ARC and objc_msgSend directly, though strange + things can happen when receiving values from NS_RETURNS_RETAINED methods. + */ +#define objc_msgSend(...) _Pragma("GCC error \"Use objc_msgSend_safe() instead!\"") +#define objc_msgSend_safe(returnType, argTypes...) ((returnType (*)(id, SEL, ##argTypes))(objc_msgSend)) + +/*! + This exists because if we throw an exception from dispatch_sync, it doesn't 'bubble up' to the calling thread. + This simply wraps dispatch_sync and properly throws the exception back to the calling thread, not the thread that + the exception was originally raised on. + + @param queue The queue to execute on + @param block The block to execute + + @see dispatch_sync + */ +#define pf_sync_with_throw(queue, block) \ + do { \ + __block NSException *caught = nil; \ + dispatch_sync(queue, ^{ \ + @try { block(); } \ + @catch (NSException *ex) { \ + caught = ex; \ + } \ + }); \ + if (caught) @throw caught; \ + } while (0) + +/*! + To prevent retain cycles by OCMock, this macro allows us to capture a weak reference to return from a stubbed method. + */ +#define andReturnWeak(variable) _andDo( \ + ({ \ + __weak typeof(variable) variable ## _weak = (variable); \ + ^(NSInvocation *invocation) { \ + __autoreleasing typeof(variable) variable ## _block = variable ## _weak; \ + [invocation setReturnValue:&(variable ## _block)]; \ + }; \ + }) \ +) + +#endif diff --git a/Parse/Internal/PFMulticastDelegate.h b/Parse/Internal/PFMulticastDelegate.h new file mode 100644 index 000000000..7a54dc3d9 --- /dev/null +++ b/Parse/Internal/PFMulticastDelegate.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/*! + Represents an event that can be subscribed to by multiple observers. + */ +@interface PFMulticastDelegate : NSObject { +@private + NSMutableArray *callbacks; +} + +/*! + Subscribes a block for callback. + + Important: if you ever plan to be able to unsubscribe the block, you must copy the block + before passing it to subscribe, and use the same instance for unsubscribe. + */ +- (void)subscribe:(void(^)(id result, NSError *error))block; +- (void)unsubscribe:(void(^)(id result, NSError *error))block; +- (void)invoke:(id)result error:(NSError *)error; +- (void)clear; + +@end diff --git a/Parse/Internal/PFMulticastDelegate.m b/Parse/Internal/PFMulticastDelegate.m new file mode 100644 index 000000000..9d49fa8ff --- /dev/null +++ b/Parse/Internal/PFMulticastDelegate.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMulticastDelegate.h" + +@implementation PFMulticastDelegate + +- (instancetype)init { + if (self = [super init]) { + callbacks = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)subscribe:(void(^)(id result, NSError *error))block { + [callbacks addObject:block]; +} + +- (void)unsubscribe:(void(^)(id result, NSError *error))block { + [callbacks removeObject:block]; +} + +- (void)invoke:(id)result error:(NSError *)error { + NSArray *callbackCopy = [callbacks copy]; + for (void (^block)(id result, NSError *error) in callbackCopy) { + block(result, error); + } +} +- (void)clear { + [callbacks removeAllObjects]; +} + +@end diff --git a/Parse/Internal/PFNetworkCommand.h b/Parse/Internal/PFNetworkCommand.h new file mode 100644 index 000000000..650292338 --- /dev/null +++ b/Parse/Internal/PFNetworkCommand.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@protocol PFNetworkCommand + +///-------------------------------------- +/// @name Properties +///-------------------------------------- + +@property (nonatomic, copy, readonly) NSString *sessionToken; +@property (nonatomic, copy, readonly) NSString *operationSetUUID; + +// If this command creates an object that is referenced by some other command, +// then this localId will be updated with the new objectId that is returned. +@property (nonatomic, copy) NSString *localId; + +///-------------------------------------- +/// @name Encoding/Decoding +///-------------------------------------- + ++ (instancetype)commandFromDictionaryRepresentation:(NSDictionary *)dictionary; +- (NSDictionary *)dictionaryRepresentation; + ++ (BOOL)isValidDictionaryRepresentation:(NSDictionary *)dictionary; + +///-------------------------------------- +/// @name Local Identifiers +///-------------------------------------- + +/*! + Replaces all local ids in this command with the correct objectId for that object. + This should be called before sending the command over the network, so that there + are no local ids sent to the Parse Cloud. If any local id refers to an object that + has not yet been saved, and thus has no objectId, then this method raises an + exception. + */ +- (void)resolveLocalIds; + +@end diff --git a/Parse/Internal/PFPinningEventuallyQueue.h b/Parse/Internal/PFPinningEventuallyQueue.h new file mode 100644 index 000000000..17a04998c --- /dev/null +++ b/Parse/Internal/PFPinningEventuallyQueue.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFEventuallyQueue.h" + +@interface PFPinningEventuallyQueue : PFEventuallyQueue + +///-------------------------------------- +/// @name Init +///-------------------------------------- + ++ (instancetype)newDefaultPinningEventuallyQueueWithCommandRunner:(id)commandRunner; + +@end diff --git a/Parse/Internal/PFPinningEventuallyQueue.m b/Parse/Internal/PFPinningEventuallyQueue.m new file mode 100644 index 000000000..0d039d2e1 --- /dev/null +++ b/Parse/Internal/PFPinningEventuallyQueue.m @@ -0,0 +1,327 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPinningEventuallyQueue.h" + +#import +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFErrorUtilities.h" +#import "PFEventuallyPin.h" +#import "PFEventuallyQueue_Private.h" +#import "PFMacros.h" +#import "PFObjectPrivate.h" +#import "PFOperationSet.h" +#import "PFRESTCommand.h" +#import "PFTaskQueue.h" + +@interface PFPinningEventuallyQueue () { + /*! + Queue for reading/writing eventually operations from LDS. Makes all reads/writes atomic + operations. + */ + PFTaskQueue *_taskQueue; + + /*! + List of `PFEventuallyPin.uuid` that are currently queued in `_processingQueue`. This contains + uuid of PFEventuallyPin that's enqueued. + */ + NSMutableArray *_eventuallyPinUUIDQueue; + + /*! + Map of eventually operation UUID to matching PFEventuallyPin. This contains PFEventuallyPin + that's enqueued. + */ + NSMutableDictionary *_uuidToEventuallyPin; + + /*! + Map OperationSetUUID to PFOperationSet + */ + NSMutableDictionary *_operationSetUUIDToOperationSet; + + /*! + Map OperationSetUUID to PFEventuallyPin + */ + NSMutableDictionary *_operationSetUUIDToEventuallyPin; +} + +@end + +@implementation PFPinningEventuallyQueue + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)newDefaultPinningEventuallyQueueWithCommandRunner:(id)commandRunner { + PFPinningEventuallyQueue *queue = [[self alloc] initWithCommandRunner:commandRunner + maxAttemptsCount:PFEventuallyQueueDefaultMaxAttemptsCount + retryInterval:PFEventuallyQueueDefaultTimeoutRetryInterval]; + [queue start]; + return queue; +} + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommandRunner:(id)commandRunner + maxAttemptsCount:(NSUInteger)attemptsCount + retryInterval:(NSTimeInterval)retryInterval { + self = [super initWithCommandRunner:commandRunner maxAttemptsCount:attemptsCount retryInterval:retryInterval]; + if (!self) return nil; + + _taskQueue = [[PFTaskQueue alloc] init]; + + dispatch_sync(_synchronizationQueue, ^{ + _eventuallyPinUUIDQueue = [NSMutableArray array]; + _uuidToEventuallyPin = [NSMutableDictionary dictionary]; + _operationSetUUIDToOperationSet = [NSMutableDictionary dictionary]; + _operationSetUUIDToEventuallyPin = [NSMutableDictionary dictionary]; + }); + + // Populate Eventually Pin to make sure we pre-loaded any existing data. + [self _populateEventuallyPinAsync]; + + return self; +} + +///-------------------------------------- +#pragma mark - Controlling Queue +///-------------------------------------- + +- (void)removeAllCommands { + [super removeAllCommands]; + + BFTask *removeTask = [_taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueWithBlock:^id(BFTask *task) { + return [[PFEventuallyPin findAllEventuallyPin] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *eventuallyPins = task.result; + NSMutableArray *unpinTasks = [NSMutableArray array]; + + for (PFEventuallyPin *eventuallyPin in eventuallyPins) { + [unpinTasks addObject:[eventuallyPin unpinInBackgroundWithName:PFEventuallyPinPinName]]; + } + + return [BFTask taskForCompletionOfAllTasks:unpinTasks]; + }]; + }]; + }]; + + [removeTask waitForResult:nil]; + // Clear in-memory data + dispatch_sync(_synchronizationQueue, ^{ + [_eventuallyPinUUIDQueue removeAllObjects]; + [_uuidToEventuallyPin removeAllObjects]; + [_operationSetUUIDToEventuallyPin removeAllObjects]; + [_operationSetUUIDToOperationSet removeAllObjects]; + }); +} + +- (void)_simulateReboot { + [super _simulateReboot]; + + [_eventuallyPinUUIDQueue removeAllObjects]; + [_uuidToEventuallyPin removeAllObjects]; + [_operationSetUUIDToEventuallyPin removeAllObjects]; + [_operationSetUUIDToOperationSet removeAllObjects]; + + [self _populateEventuallyPinAsync]; +} + +///-------------------------------------- +#pragma mark - PFEventuallyQueueSubclass +///-------------------------------------- + +- (NSString *)_newIdentifierForCommand:(id)command { + return [[NSUUID UUID] UUIDString]; +} + +- (NSArray *)_pendingCommandIdentifiers { + [[self _populateEventuallyPinAsync] waitForResult:nil]; + + NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + PFEventuallyPin *pin = _uuidToEventuallyPin[evaluatedObject]; + // Filter out all pins that don't have `operationSet` data ready yet + // to make sure we send the command with all the changes. + if (pin.operationSetUUID) { + return (_operationSetUUIDToEventuallyPin[pin.operationSetUUID] != nil); + } + return YES; + }]; + return [_eventuallyPinUUIDQueue filteredArrayUsingPredicate:predicate]; +} + +- (id)_commandWithIdentifier:(NSString *)identifier error:(NSError **)error { + // Should be populated by `_pendingCommandIdentifiers` + PFEventuallyPin *eventuallyPin = _uuidToEventuallyPin[identifier]; + + // TODO (hallucinogen): this is a temporary hack. We need to change this to match the Android one. + // We need to construct the command just right when we want to execute it. Or else it will ask for localId + // when there's unsaved child. + switch (eventuallyPin.type) { + case PFEventuallyPinTypeSave: { + PFOperationSet *operationSet = _operationSetUUIDToOperationSet[eventuallyPin.operationSetUUID]; + return [eventuallyPin.object _constructSaveCommandForChanges:operationSet + sessionToken:eventuallyPin.sessionToken + objectEncoder:[PFPointerObjectEncoder objectEncoder]]; + } + case PFEventuallyPinTypeDelete: + return [eventuallyPin.object _currentDeleteCommandWithSessionToken:eventuallyPin.sessionToken]; + default: + break; + } + + id command = eventuallyPin.command; + if (!command && error) { + *error = [PFErrorUtilities errorWithCode:kPFErrorInternalServer + message:@"Failed to construct eventually command from cache." + shouldLog:NO]; + } + return command; +} + +- (BFTask *)_enqueueCommandInBackground:(id)command + object:(PFObject *)object + identifier:(NSString *)identifier { + return [_taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueAsyncWithBlock:^id(BFTask *task){ + return [PFEventuallyPin pinEventually:object forCommand:command withUUID:identifier]; + }]; + }]; +} + +- (BFTask *)_didFinishRunningCommand:(id)command + withIdentifier:(NSString *)identifier + resultTask:(BFTask *)resultTask { + // Delete the commands regardless, even if it failed. Otherwise we'll just keep trying it forever. + // We don't need to wait for taskQueue since it will not be queued again since this + // PFEventuallyPin is still in `_eventuallyPinUUIDQueue` + PFEventuallyPin *eventuallyPin = _uuidToEventuallyPin[identifier]; + BFTask *unpinTask = [eventuallyPin unpinInBackgroundWithName:PFEventuallyPinPinName]; + unpinTask = [unpinTask continueWithBlock:^id(BFTask *task) { + // Remove data from memory. + dispatch_sync(_synchronizationQueue, ^{ + [_uuidToEventuallyPin removeObjectForKey:identifier]; + [_eventuallyPinUUIDQueue removeObject:identifier]; + }); + + if (resultTask.cancelled || resultTask.exception || resultTask.error) { + return resultTask; + } + + if (eventuallyPin.operationSetUUID) { + // Remove only if the operation succeeded + dispatch_sync(_synchronizationQueue, ^{ + [_operationSetUUIDToOperationSet removeObjectForKey:eventuallyPin.operationSetUUID]; + [_operationSetUUIDToEventuallyPin removeObjectForKey:eventuallyPin.operationSetUUID]; + }); + } + + PFCommandResult *commandResult = resultTask.result; + switch (eventuallyPin.type) { + case PFEventuallyPinTypeSave: { + + task = [task continueWithBlock:^id(BFTask *task) { + return [eventuallyPin.object handleSaveResultAsync:commandResult.result]; + }]; + break; + } + + case PFEventuallyPinTypeDelete: { + task = [task continueWithBlock:^id(BFTask *task) { + PFObject *object = eventuallyPin.object; + id controller = [[object class] objectController]; + return [controller processDeleteResultAsync:commandResult.result forObject:object]; + }]; + break; + } + + default:break; + } + + return task; + }]; + + // Notify event listener that we finished running. + return [[super _didFinishRunningCommand:command + withIdentifier:identifier + resultTask:resultTask] continueWithBlock:^id(BFTask *task) { + return unpinTask; + }]; +} + +/*! + Synchronizes PFObject taskQueue (Many) and PFPinningEventuallyQueue taskQueue (None). Each queue will be held + until both are ready, matched on operationSetUUID. Once both are ready, the eventually task will run. + */ +- (BFTask *)_waitForOperationSet:(PFOperationSet *)operationSet eventuallyPin:(PFEventuallyPin *)eventuallyPin { + if (eventuallyPin != nil && eventuallyPin.type != PFEventuallyPinTypeSave) { + // If not save, then we don't have to do anything special. + return [BFTask taskWithResult:nil]; + } + + // TODO (hallucinogen): actually wait for PFObject taskQueue and PFPinningEventually taskQueue + + __block NSString *uuid = nil; + dispatch_sync(_synchronizationQueue, ^{ + if (operationSet != nil) { + uuid = operationSet.uuid; + _operationSetUUIDToOperationSet[uuid] = operationSet; + } + if (eventuallyPin != nil) { + uuid = eventuallyPin.operationSetUUID; + _operationSetUUIDToEventuallyPin[uuid] = eventuallyPin; + } + }); + if (uuid == nil) { + NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Either operationSet or eventuallyPin must be set" + userInfo:nil]; + return [BFTask taskWithException:exception]; + } + return [BFTask taskWithResult:nil]; +} + +///-------------------------------------- +#pragma mark - Eventually Pin +///-------------------------------------- + +- (BFTask *)_populateEventuallyPinAsync { + return [_taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [[toAwait continueWithBlock:^id(BFTask *task) { + return [PFEventuallyPin findAllEventuallyPinWithExcludeUUIDs:_eventuallyPinUUIDQueue]; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *eventuallyPins = task.result; + + for (PFEventuallyPin *eventuallyPin in eventuallyPins) { + // If it's enqueued already, we don't need to run it again. + if ([_eventuallyPinUUIDQueue containsObject:eventuallyPin.operationSetUUID]) { + continue; + } + + // Make sure the data is in memory. + dispatch_sync(_synchronizationQueue, ^{ + [_eventuallyPinUUIDQueue addObject:eventuallyPin.uuid]; + _uuidToEventuallyPin[eventuallyPin.uuid] = eventuallyPin; + }); + + // For now we don't care whether this will fail or not. + [[self _waitForOperationSet:nil eventuallyPin:eventuallyPin] waitForResult:nil]; + } + + return task; + }]; + }]; +} + +@end diff --git a/Parse/Internal/PFReachability.h b/Parse/Internal/PFReachability.h new file mode 100644 index 000000000..85c622ebb --- /dev/null +++ b/Parse/Internal/PFReachability.h @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@class PFReachability; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(uint8_t, PFReachabilityState) { + PFReachabilityStateNotReachable, + PFReachabilityStateReachableViaWiFi, + PFReachabilityStateReachableViaCell, +}; + +@protocol PFReachabilityListener + +- (void)reachability:(PFReachability *)reachability didChangeReachabilityState:(PFReachabilityState)state; + +@end + +@interface PFReachability : NSObject + +@property (nonatomic, assign, readonly) PFReachabilityState currentState; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithURL:(NSURL *)url NS_DESIGNATED_INITIALIZER; + +/* + Returns a shared singleton instance, + that could be used to check if Parse is reachable + */ ++ (instancetype)sharedParseReachability; + +/* + Adds a weak reference to the listener, + callbacks are executed on the main thread when status or flags change. + */ +- (void)addListener:(id)listener; + +/* + Removes weak reference to the listener. + */ +- (void)removeListener:(id)listener; + +/* + Removes all references to all listener objects. + */ +- (void)removeAllListeners; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PFReachability.m b/Parse/Internal/PFReachability.m new file mode 100644 index 000000000..a9a0f8669 --- /dev/null +++ b/Parse/Internal/PFReachability.m @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFReachability.h" + +#import "PFAssert.h" +#import "PFConstants.h" +#import "PFLogging.h" +#import "PFMacros.h" +#import "PFWeakValue.h" + +@interface PFReachability () { + dispatch_queue_t _synchronizationQueue; + NSMutableArray *_listenersArray; + + SCNetworkReachabilityRef _networkReachability; +} + +@property (nonatomic, assign, readwrite) SCNetworkReachabilityFlags flags; + +@end + +static void _reachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { + NSCAssert(info != NULL, @"info was NULL in ReachabilityCallback"); + PFReachability *reachability = (__bridge PFReachability *)info; + reachability.flags = flags; +} + +@implementation PFReachability + +@synthesize flags = _flags; + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (BOOL)_reachabilityStateForFlags:(SCNetworkConnectionFlags)flags { + PFReachabilityState reachabilityState = PFReachabilityStateNotReachable; + + if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) { + // if target host is not reachable + return reachabilityState; + } + + if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) { + // if target host is reachable and no connection is required + // then we'll assume (for now) that your on Wi-Fi + reachabilityState = PFReachabilityStateReachableViaWiFi; + } + if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) { + // ... and the connection is on-demand (or on-traffic) if the + // calling application is using the CFSocketStream or higher APIs + if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { + // ... and no [user] intervention is needed + reachabilityState = PFReachabilityStateReachableViaWiFi; + } + } + +#if TARGET_OS_IPHONE + if (((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) && + ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0)) { + // ... but WWAN connections are OK if the calling application + // is using the CFNetwork (CFSocketStream?) APIs. + // ... and a network connection is not required (kSCNetworkReachabilityFlagsConnectionRequired) + // which could be et w/connection flag (e.g. IsWWAN) indicating type of connection required. + reachabilityState = PFReachabilityStateReachableViaCell; + } +#endif + + return reachabilityState; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithURL:(NSURL *)url { + self = [super init]; + if (!self) return nil; + + _synchronizationQueue = dispatch_queue_create("com.parse.reachability", DISPATCH_QUEUE_CONCURRENT); + _listenersArray = [NSMutableArray array]; + [self _startMonitoringReachabilityWithURL:url]; + + return self; +} + ++ (instancetype)sharedParseReachability { + static PFReachability *reachability; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *serverUrlAsString = [NSString stringWithFormat:@"%@/%ld", kPFParseServer, (long)PARSE_API_VERSION]; + NSURL *serverUrl = [NSURL URLWithString:serverUrlAsString]; + reachability = [[self alloc] initWithURL:serverUrl]; + }); + return reachability; +} + +///-------------------------------------- +#pragma mark - Dealloc +///-------------------------------------- + +- (void)dealloc { + if (_networkReachability != NULL) { + SCNetworkReachabilitySetCallback(_networkReachability, NULL, NULL); + SCNetworkReachabilitySetDispatchQueue(_networkReachability, NULL); + CFRelease(_networkReachability); + _networkReachability = NULL; + } +} + +///-------------------------------------- +#pragma mark - Listeners +///-------------------------------------- + +- (void)addListener:(id)listener { + PFWeakValue *value = [PFWeakValue valueWithWeakObject:listener]; + dispatch_barrier_sync(_synchronizationQueue, ^{ + [_listenersArray addObject:value]; + }); +} + +- (void)removeListener:(id)listener { + dispatch_barrier_sync(_synchronizationQueue, ^{ + [_listenersArray filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + id weakObject = [evaluatedObject weakObject]; + return (weakObject == nil || weakObject == listener); + }]]; + }); +} + +- (void)removeAllListeners { + dispatch_barrier_sync(_synchronizationQueue, ^{ + [_listenersArray removeAllObjects]; + }); +} + +- (void)_notifyAllListeners { + @weakify(self); + dispatch_async(_synchronizationQueue, ^{ + @strongify(self); + PFReachabilityState state = [[self class] _reachabilityStateForFlags:_flags]; + for (PFWeakValue *value in _listenersArray) { + [value.weakObject reachability:self didChangeReachabilityState:state]; + } + + dispatch_barrier_async(_synchronizationQueue, ^{ + [_listenersArray filterUsingPredicate:[NSPredicate predicateWithFormat:@"SELf.weakObject != nil"]]; + }); + }); +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)setFlags:(SCNetworkReachabilityFlags)flags { + dispatch_barrier_async(_synchronizationQueue, ^{ + _flags = flags; + [self _notifyAllListeners]; + }); +} + +- (SCNetworkReachabilityFlags)flags { + __block SCNetworkReachabilityFlags flags; + dispatch_sync(_synchronizationQueue, ^{ + flags = _flags; + }); + return flags; +} + +- (PFReachabilityState)currentState { + return [[self class] _reachabilityStateForFlags:self.flags]; +} + +///-------------------------------------- +#pragma mark - Reachability +///-------------------------------------- + +- (void)_startMonitoringReachabilityWithURL:(NSURL *)url { + dispatch_barrier_async(_synchronizationQueue, ^{ + _networkReachability = SCNetworkReachabilityCreateWithName(NULL, [[url host] UTF8String]); + if (_networkReachability != NULL) { + // Set the initial flags + SCNetworkReachabilityFlags flags; + SCNetworkReachabilityGetFlags(_networkReachability, &flags); + self.flags = flags; + + // Set up notification for changes in reachability. + SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; + if (SCNetworkReachabilitySetCallback(_networkReachability, _reachabilityCallback, &context)) { + if (!SCNetworkReachabilitySetDispatchQueue(_networkReachability, _synchronizationQueue)) { + PFLogError(PFLoggingTagCommon, @"Unable to start listening for network connectivity status."); + } + } + } + }); +} + +@end diff --git a/Parse/Internal/PFTaskQueue.h b/Parse/Internal/PFTaskQueue.h new file mode 100644 index 000000000..88618858b --- /dev/null +++ b/Parse/Internal/PFTaskQueue.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; + +@interface PFTaskQueue : NSObject + +// The lock for this task queue. +@property (nonatomic, strong, readonly) NSObject *mutex; + +/*! + Enqueues a task created by the given block. Then block is given a task to + await once state is snapshotted (e.g. after capturing session tokens at the + time of the save call. Awaiting this task will wait for the created task's + turn in the queue. + */ +- (BFTask *)enqueue:(BFTask *(^)(BFTask *toAwait))taskStart; + +@end diff --git a/Parse/Internal/PFTaskQueue.m b/Parse/Internal/PFTaskQueue.m new file mode 100644 index 000000000..fb6d11114 --- /dev/null +++ b/Parse/Internal/PFTaskQueue.m @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTaskQueue.h" + +#import + +@interface PFTaskQueue () + +@property (nonatomic, strong, readwrite) BFTask *tail; +@property (nonatomic, strong, readwrite) NSObject *mutex; + +@end + +@implementation PFTaskQueue + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + self.mutex = [[NSObject alloc] init]; + + return self; +} + +- (BFTask *)enqueue:(BFTask *(^)(BFTask *toAwait))taskStart { + @synchronized (self.mutex) { + BFTask *oldTail = self.tail ?: [BFTask taskWithResult:nil]; + + // The task created by taskStart is responsible for waiting on the + // task passed to it before doing its work. This gives it an opportunity + // to do startup work or save state before waiting for its turn in the queue. + BFTask *task = taskStart(oldTail); + + // The tail task should be dependent on the old tail as well as the newly-created + // task. This prevents cancellation of the new task from causing the queue to run + // out of order. + self.tail = [BFTask taskForCompletionOfAllTasks:@[oldTail, task]]; + + return task; + } +} + +@end diff --git a/Parse/Internal/PFWeakValue.h b/Parse/Internal/PFWeakValue.h new file mode 100644 index 000000000..2ff1e4714 --- /dev/null +++ b/Parse/Internal/PFWeakValue.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFWeakValue : NSObject + +@property (nonatomic, weak, readonly) id weakObject; + ++ (instancetype)valueWithWeakObject:(id)object; + +@end diff --git a/Parse/Internal/PFWeakValue.m b/Parse/Internal/PFWeakValue.m new file mode 100644 index 000000000..a12920250 --- /dev/null +++ b/Parse/Internal/PFWeakValue.m @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFWeakValue.h" + +@interface PFWeakValue () + +@property (nonatomic, weak, readwrite) id weakObject; + +@end + +@implementation PFWeakValue + ++ (instancetype)valueWithWeakObject:(id)object { + PFWeakValue *value = [[self alloc] init]; + value.weakObject = object; + return value; +} + +@end diff --git a/Parse/Internal/ParseInternal.h b/Parse/Internal/ParseInternal.h new file mode 100644 index 000000000..2a0a585cd --- /dev/null +++ b/Parse/Internal/ParseInternal.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +#import "PFAssert.h" +#import "PFAuthenticationProvider.h" +#import "PFBlockRetryer.h" +#import "PFCommandCache.h" +#import "PFEventuallyQueue.h" +#import "PFFieldOperation.h" +#import "PFGeoPointPrivate.h" +#import "PFInternalUtils.h" +#import "PFKeyValueCache.h" +#import "PFObjectPrivate.h" +#import "PFUserPrivate.h" +#import "ParseModule.h" + +@interface Parse (ParseModules) + ++ (void)enableParseModule:(id)module; ++ (void)disableParseModule:(id)module; ++ (BOOL)isModuleEnabled:(id)module; + +@end diff --git a/Parse/Internal/ParseManager.h b/Parse/Internal/ParseManager.h new file mode 100644 index 000000000..9be423e77 --- /dev/null +++ b/Parse/Internal/ParseManager.h @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" +#import "PFOfflineStore.h" + +@class PFAnalyticsController; +@class PFCoreManager; +@class PFInstallationIdentifierStore; +@class PFKeychainStore; +@class PFPurchaseController; +@class PFPushManager; + +@interface ParseManager : NSObject + +@property (nonatomic, copy, readonly) NSString *applicationId; +@property (nonatomic, copy, readonly) NSString *clientKey; + +@property (nonatomic, copy, readonly) NSString *applicationGroupIdentifier; +@property (nonatomic, copy, readonly) NSString *containingApplicationIdentifier; + +@property (nonatomic, strong, readonly) PFCoreManager *coreManager; +@property (nonatomic, strong) PFPushManager *pushManager; + +@property (nonatomic, strong) PFAnalyticsController *analyticsController; + +#if TARGET_OS_IPHONE +@property (nonatomic, strong) PFPurchaseController *purchaseController; +#endif + +///-------------------------------------- +/// @name Initialization +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; + +/*! + Initializes an instance of ParseManager class. + + @param applicationId ApplicationId of Parse app. + @param clientKey ClientKey of Parse app. + + @returns `ParseManager` instance. + */ +- (instancetype)initWithApplicationId:(NSString *)applicationId + clientKey:(NSString *)clientKey NS_DESIGNATED_INITIALIZER; + +/*! + Configures ParseManager with specified properties. + + @param applicationGroupIdentifier Shared AppGroup container identifier. + @param containingApplicationIdentifier Containg application bundle identifier (for extensions). + @param localDataStoreEnabled `BOOL` flag to enable local datastore or not. + */ +- (void)configureWithApplicationGroupIdentifier:(NSString *)applicationGroupIdentifier + containingApplicationIdentifier:(NSString *)containingApplicationIdentifier + enabledLocalDataStore:(BOOL)localDataStoreEnabled; + +///-------------------------------------- +/// @name Offline Store +///-------------------------------------- + +- (void)loadOfflineStoreWithOptions:(PFOfflineStoreOptions)options; + +///-------------------------------------- +/// @name Eventually Queue +///-------------------------------------- + +- (void)clearEventuallyQueue; + +///-------------------------------------- +/// @name Core Manager +///-------------------------------------- + +- (void)unloadCoreManager; + +///-------------------------------------- +/// @name Preloading +///-------------------------------------- + +- (BFTask *)preloadDiskObjectsToMemoryAsync; + +@end diff --git a/Parse/Internal/ParseManager.m b/Parse/Internal/ParseManager.m new file mode 100644 index 000000000..bfac5848f --- /dev/null +++ b/Parse/Internal/ParseManager.m @@ -0,0 +1,456 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ParseManager.h" + +#import + +#import "BFTask+Private.h" +#import "PFAnalyticsController.h" +#import "PFAssert.h" +#import "PFCommandCache.h" +#import "PFConfig.h" +#import "PFCoreManager.h" +#import "PFFileManager.h" +#import "PFInstallation.h" +#import "PFInstallationIdentifierStore.h" +#import "PFKeyValueCache.h" +#import "PFKeychainStore.h" +#import "PFLogging.h" +#import "PFMultiProcessFileLockController.h" +#import "PFPinningEventuallyQueue.h" +#import "PFPushManager.h" +#import "PFUser.h" +#import "PFURLSessionCommandRunner.h" + +#if TARGET_OS_IPHONE +#import "PFPurchaseController.h" +#import "PFProduct.h" +#endif + +static NSString *const _ParseApplicationIdFileName = @"applicationId"; + +@interface ParseManager () +{ + dispatch_queue_t _offlineStoreAccessQueue; + dispatch_queue_t _eventuallyQueueAccessQueue; + dispatch_queue_t _keychainStoreAccessQueue; + dispatch_queue_t _fileManagerAccessQueue; + dispatch_queue_t _installationIdentifierStoreAccessQueue; + dispatch_queue_t _commandRunnerAccessQueue; + dispatch_queue_t _keyValueCacheAccessQueue; + dispatch_queue_t _coreManagerAccessQueue; + dispatch_queue_t _pushManagerAccessQueue; + dispatch_queue_t _controllerAccessQueue; + + dispatch_queue_t _preloadQueue; +} + +@end + +@implementation ParseManager + +@synthesize keychainStore = _keychainStore; +@synthesize fileManager = _fileManager; +@synthesize offlineStore = _offlineStore; +@synthesize eventuallyQueue = _eventuallyQueue; +@synthesize installationIdentifierStore = _installationIdentifierStore; +@synthesize commandRunner = _commandRunner; +@synthesize keyValueCache = _keyValueCache; +@synthesize coreManager = _coreManager; +@synthesize analyticsController = _analyticsController; +@synthesize pushManager = _pushManager; +#if TARGET_OS_IPHONE +@synthesize purchaseController = _purchaseController; +#endif + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithApplicationId:(NSString *)applicationId clientKey:(NSString *)clientKey { + self = [super init]; + if (!self) return nil; + + _offlineStoreAccessQueue = dispatch_queue_create("com.parse.offlinestore.access", DISPATCH_QUEUE_CONCURRENT); + _eventuallyQueueAccessQueue = dispatch_queue_create("com.parse.eventuallyqueue.access", DISPATCH_QUEUE_SERIAL); + _keychainStoreAccessQueue = dispatch_queue_create("com.parse.keychainstore.access", DISPATCH_QUEUE_SERIAL); + _fileManagerAccessQueue = dispatch_queue_create("com.parse.filemanager.access", DISPATCH_QUEUE_SERIAL); + _installationIdentifierStoreAccessQueue = dispatch_queue_create("com.parse.installationidentifierstore.access", + DISPATCH_QUEUE_SERIAL); + _commandRunnerAccessQueue = dispatch_queue_create("com.parse.commandrunner.access", DISPATCH_QUEUE_SERIAL); + _keyValueCacheAccessQueue = dispatch_queue_create("com.parse.keyvaluecache.access", DISPATCH_QUEUE_SERIAL); + _coreManagerAccessQueue = dispatch_queue_create("com.parse.coreManager.access", DISPATCH_QUEUE_SERIAL); + _pushManagerAccessQueue = dispatch_queue_create("com.parse.pushManager.access", DISPATCH_QUEUE_SERIAL); + _controllerAccessQueue = dispatch_queue_create("com.parse.controller.access", DISPATCH_QUEUE_SERIAL); + _preloadQueue = dispatch_queue_create("com.parse.preload", DISPATCH_QUEUE_SERIAL); + + _applicationId = [applicationId copy]; + _clientKey = [clientKey copy]; + + return self; +} + +- (void)configureWithApplicationGroupIdentifier:(NSString *)applicationGroupIdentifier + containingApplicationIdentifier:(NSString *)containingApplicationIdentifier + enabledLocalDataStore:(BOOL)localDataStoreEnabled { + _applicationGroupIdentifier = [applicationGroupIdentifier copy]; + _containingApplicationIdentifier = [containingApplicationIdentifier copy]; + + // Migrate any data if it's required. + [self _migrateSandboxDataToApplicationGroupContainerIfNeeded]; + + // Make sure the data on disk for Parse is for the current application. + [self _checkApplicationId]; + + if (localDataStoreEnabled) { + PFOfflineStoreOptions options = (_applicationGroupIdentifier ? + PFOfflineStoreOptionAlwaysFetchFromSQLite : 0); + [self loadOfflineStoreWithOptions:options]; + } +} + +///-------------------------------------- +#pragma mark - Offline Store +///-------------------------------------- + +- (void)loadOfflineStoreWithOptions:(PFOfflineStoreOptions)options { + PFConsistencyAssert(!_offlineStore, @"Can't load offline store more than once."); + dispatch_barrier_sync(_offlineStoreAccessQueue, ^{ + _offlineStore = [[PFOfflineStore alloc] initWithFileManager:self.fileManager options:options]; + }); +} + +- (void)setOfflineStore:(PFOfflineStore *)offlineStore { + dispatch_barrier_sync(_offlineStoreAccessQueue, ^{ + _offlineStore = offlineStore; + }); +} + +- (PFOfflineStore *)offlineStore { + __block PFOfflineStore *offlineStore = nil; + dispatch_sync(_offlineStoreAccessQueue, ^{ + offlineStore = _offlineStore; + }); + return offlineStore; +} + +- (BOOL)isOfflineStoreLoaded { + return (self.offlineStore != nil); +} + +///-------------------------------------- +#pragma mark - Eventually Queue +///-------------------------------------- + +- (PFEventuallyQueue *)eventuallyQueue { + __block PFEventuallyQueue *queue = nil; + dispatch_sync(_eventuallyQueueAccessQueue, ^{ + if (!_eventuallyQueue || + (self.offlineStoreLoaded && [_eventuallyQueue isKindOfClass:[PFCommandCache class]]) || + (!self.offlineStoreLoaded && [_eventuallyQueue isKindOfClass:[PFPinningEventuallyQueue class]])) { + + id commandRunner = self.commandRunner; + + PFCommandCache *commandCache = [self _newCommandCache]; + _eventuallyQueue = (self.offlineStoreLoaded ? + [PFPinningEventuallyQueue newDefaultPinningEventuallyQueueWithCommandRunner:commandRunner] + : + commandCache); + + // We still need to clear out the old command cache even if we're using Pinning in case + // anything is left over when the user upgraded. Checking number of pending and then + // clearing should be enough. + if (self.offlineStoreLoaded && commandCache.commandCount > 0) { + [commandCache removeAllCommands]; + } + } + queue = _eventuallyQueue; + }); + return queue; +} + +- (PFCommandCache *)_newCommandCache { + // Construct the path to the cache directory in /Library/Private Documents/Parse/Command Cache + // This isn't in the "Library/Caches" directory because we don't want the OS clearing it for us. + // It falls under the category of "offline data". + // See https://developer.apple.com/library/ios/#qa/qa1699/_index.html + NSString *folderPath = [self.fileManager parseDefaultDataDirectoryPath]; + return [PFCommandCache newDefaultCommandCacheWithCommandRunner:self.commandRunner cacheFolderPath:folderPath]; +} + +- (void)clearEventuallyQueue { + dispatch_sync(_preloadQueue, ^{ + dispatch_sync(_eventuallyQueueAccessQueue, ^{ + [_eventuallyQueue removeAllCommands]; + [_eventuallyQueue pause]; + _eventuallyQueue = nil; + }); + }); +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +#pragma mark KeychainStore + +- (PFKeychainStore *)keychainStore { + __block PFKeychainStore *store = nil; + dispatch_sync(_keychainStoreAccessQueue, ^{ + if (!_keychainStore) { + NSString *bundleIdentifier = (_containingApplicationIdentifier ?: [[NSBundle mainBundle] bundleIdentifier]); + NSString *service = [NSString stringWithFormat:@"%@.%@", bundleIdentifier, PFKeychainStoreDefaultService]; + _keychainStore = [[PFKeychainStore alloc] initWithService:service]; + } + store = _keychainStore; + }); + return store; +} + +#pragma mark FileManager + +- (PFFileManager *)fileManager { + __block PFFileManager *fileManager = nil; + dispatch_sync(_fileManagerAccessQueue, ^{ + if (!_fileManager) { + _fileManager = [[PFFileManager alloc] initWithApplicationIdentifier:self.applicationId + applicationGroupIdentifier:self.applicationGroupIdentifier]; + } + fileManager = _fileManager; + }); + return fileManager; +} + +#pragma mark InstallationIdentifierStore + +- (PFInstallationIdentifierStore *)installationIdentifierStore { + __block PFInstallationIdentifierStore *store = nil; + dispatch_sync(_installationIdentifierStoreAccessQueue, ^{ + if (!_installationIdentifierStore) { + _installationIdentifierStore = [[PFInstallationIdentifierStore alloc] initWithFileManager:self.fileManager]; + } + store = _installationIdentifierStore; + }); + return store; +} + +#pragma mark CommandRunner + +- (id)commandRunner { + __block id runner = nil; + dispatch_sync(_commandRunnerAccessQueue, ^{ + if (!_commandRunner) { + _commandRunner = [PFURLSessionCommandRunner commandRunnerWithDataSource:self + applicationId:self.applicationId + clientKey:self.clientKey]; + } + runner = _commandRunner; + }); + return runner; +} + +#pragma mark KeyValueCache + +- (PFKeyValueCache *)keyValueCache { + __block PFKeyValueCache *cache = nil; + dispatch_sync(_keyValueCacheAccessQueue, ^{ + if (!_keyValueCache) { + NSString *path = [self.fileManager parseCacheItemPathForPathComponent:@"../ParseKeyValueCache/"]; + _keyValueCache = [[PFKeyValueCache alloc] initWithCacheDirectoryPath:path]; + } + cache = _keyValueCache; + }); + return cache; +} + +#pragma mark CoreManager + +- (PFCoreManager *)coreManager { + __block PFCoreManager *manager = nil; + dispatch_sync(_coreManagerAccessQueue, ^{ + if (!_coreManager) { + _coreManager = [PFCoreManager managerWithDataSource:self]; + } + manager = _coreManager; + }); + return manager; +} + +- (void)unloadCoreManager { + dispatch_sync(_coreManagerAccessQueue, ^{ + _coreManager = nil; + }); +} + +#pragma mark PushManager + +- (PFPushManager *)pushManager { + __block PFPushManager *manager = nil; + dispatch_sync(_pushManagerAccessQueue, ^{ + if (!_pushManager) { + _pushManager = [PFPushManager managerWithCommonDataSource:self coreDataSource:self.coreManager]; + } + manager = _pushManager; + }); + return manager; +} + +- (void)setPushManager:(PFPushManager *)pushManager { + dispatch_sync(_pushManagerAccessQueue, ^{ + _pushManager = pushManager; + }); +} + +#pragma mark AnalyticsController + +- (PFAnalyticsController *)analyticsController { + __block PFAnalyticsController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_analyticsController) { + _analyticsController = [[PFAnalyticsController alloc] initWithEventuallyQueue:self.eventuallyQueue]; + } + controller = _analyticsController; + }); + return controller; +} + +- (void)setAnalyticsController:(PFAnalyticsController *)analyticsController { + dispatch_sync(_controllerAccessQueue, ^{ + if (_analyticsController != analyticsController) { + _analyticsController = analyticsController; + } + }); +} + +#if TARGET_OS_IPHONE + +#pragma mark PurchaseController + +- (PFPurchaseController *)purchaseController { + __block PFPurchaseController *controller = nil; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_purchaseController) { + _purchaseController = [PFPurchaseController controllerWithCommandRunner:self.commandRunner + fileManager:self.fileManager]; + } + controller = _purchaseController; + }); + return controller; +} + +- (void)setPurchaseController:(PFPurchaseController *)purchaseController { + dispatch_sync(_controllerAccessQueue, ^{ + _purchaseController = purchaseController; + }); +} + +#endif + +///-------------------------------------- +#pragma mark - Preloading +///-------------------------------------- + +- (BFTask *)preloadDiskObjectsToMemoryAsync { + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor executorWithDispatchQueue:_preloadQueue] withBlock:^id{ + @strongify(self); + [PFUser currentUser]; + [PFConfig currentConfig]; + [PFInstallation currentInstallation]; + + [self eventuallyQueue]; + + return nil; + }]; +} + +///-------------------------------------- +#pragma mark - ApplicationId +///-------------------------------------- + +/*! + Verifies that the data stored on disk for Parse was generated using the same application that is running now. + */ +- (void)_checkApplicationId { + NSFileManager *systemFileManager = [NSFileManager defaultManager]; + + // Make sure the current version of the cache is for this application id. + NSString *applicationIdFile = [self.fileManager parseDataItemPathForPathComponent:_ParseApplicationIdFileName]; + [[PFMultiProcessFileLockController sharedController] beginLockedContentAccessForFileAtPath:applicationIdFile]; + + if ([systemFileManager fileExistsAtPath:applicationIdFile]) { + NSError *error = nil; + NSString *applicationId = [NSString stringWithContentsOfFile:applicationIdFile + encoding:NSUTF8StringEncoding + error:&error]; + if (!error && ![applicationId isEqualToString:self.applicationId]) { + // The application id has changed, so everything on disk is invalid. + [self.keychainStore removeAllObjects]; + [self.keyValueCache removeAllObjects]; + + NSArray *tasks = @[ + // Remove the contents only, but don't delete the folder. + [PFFileManager removeDirectoryContentsAsyncAtPath:[self.fileManager parseDefaultDataDirectoryPath]], + // Completely remove everything in deprecated folder. + [PFFileManager removeItemAtPathAsync:[self.fileManager parseDataDirectoryPath_DEPRECATED]] + ]; + [[BFTask taskForCompletionOfAllTasks:tasks] waitForResult:nil withMainThreadWarning:NO]; + } + } + + if (![systemFileManager fileExistsAtPath:applicationIdFile]) { + NSError *error = nil; + BFTask *writeTask = [PFFileManager writeStringAsync:self.applicationId toFile:applicationIdFile]; + [writeTask waitForResult:&error withMainThreadWarning:NO]; + if (error) { + PFLogError(PFLoggingTagCommon, @"Unable to create applicationId file with error: %@", error); + } + } + + [[PFMultiProcessFileLockController sharedController] endLockedContentAccessForFileAtPath:applicationIdFile]; +} + +///-------------------------------------- +#pragma mark - Data Sharing +///-------------------------------------- + +- (void)_migrateSandboxDataToApplicationGroupContainerIfNeeded { + // There is no need to migrate anything on OSX, since we are using globally available folder. +#if TARGET_OS_IPHONE + // Do nothing if there is no application group container or containing application is specified. + if (!self.applicationGroupIdentifier || self.containingApplicationIdentifier) { + return; + } + + NSString *localSandboxDataPath = [self.fileManager parseLocalSandboxDataDirectoryPath]; + NSString *dataPath = [self.fileManager parseDefaultDataDirectoryPath]; + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:localSandboxDataPath error:nil]; + if ([contents count] != 0) { + // If moving files fails - just log the error, but don't fail. + NSError *error = nil; + [[PFFileManager moveContentsOfDirectoryAsyncAtPath:localSandboxDataPath + toDirectoryAtPath:dataPath + executor:[BFExecutor immediateExecutor]] waitForResult:&error]; + if (error) { + PFLogError(PFLoggingTagCommon, + @"Failed to migrate local sandbox data to shared container with error %@", + [error localizedDescription]); + } else { + [[PFFileManager removeItemAtPathAsync:localSandboxDataPath withFileLock:NO] waitForResult:nil]; + } + } +#endif +} + +@end diff --git a/Parse/Internal/ParseModule.h b/Parse/Internal/ParseModule.h new file mode 100644 index 000000000..a9a1a499f --- /dev/null +++ b/Parse/Internal/ParseModule.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@protocol ParseModule + +- (void)parseDidInitializeWithApplicationId:(NSString *)applicationId clientKey:(NSString *)clientKey; + +@end + +@interface ParseModuleCollection : NSObject + +- (void)addParseModule:(id)module; +- (void)removeParseModule:(id)module; + +- (BOOL)containsModule:(id)module; +- (NSUInteger)modulesCount; + +@end diff --git a/Parse/Internal/ParseModule.m b/Parse/Internal/ParseModule.m new file mode 100644 index 000000000..5167762c0 --- /dev/null +++ b/Parse/Internal/ParseModule.m @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ParseModule.h" + +///-------------------------------------- +#pragma mark - ParseModuleCollection +///-------------------------------------- + +typedef void (^ParseModuleEnumerationBlock)(id module, BOOL *stop, BOOL *remove); + +@interface ParseModuleCollection () + +@property (atomic, strong) dispatch_queue_t collectionQueue; +@property (atomic, strong) NSPointerArray *modules; + +@end + +@implementation ParseModuleCollection + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (self) { + _collectionQueue = dispatch_queue_create("com.parse.ParseModuleCollection", DISPATCH_QUEUE_SERIAL); + _modules = [NSPointerArray weakObjectsPointerArray]; + } + return self; +} + +///-------------------------------------- +#pragma mark - Collection +///-------------------------------------- + +- (void)addParseModule:(id)module { + if (module == nil) { + return; + } + + [self performCollectionAccessBlock:^{ + [self.modules addPointer:(__bridge void *)module]; + }]; +} + +- (void)removeParseModule:(id)module { + if (module == nil) { + return; + } + + [self enumerateModulesWithBlock:^(id enumeratedModule, BOOL *stop, BOOL *remove) { + *remove = (module == enumeratedModule); + }]; +} + +- (BOOL)containsModule:(id)module { + __block BOOL retValue = NO; + [self enumerateModulesWithBlock:^(id enumeratedModule, BOOL *stop, BOOL *remove) { + if (module == enumeratedModule) { + retValue = YES; + *stop = YES; + } + }]; + return retValue; +} + +- (NSUInteger)modulesCount { + return [self.modules count]; +} + +///-------------------------------------- +#pragma mark - ParseModule +///-------------------------------------- + +- (void)parseDidInitializeWithApplicationId:(NSString *)applicationId clientKey:(NSString *)clientKey { + [self enumerateModulesWithBlock:^(id module, BOOL *stop, BOOL *remove) { + dispatch_async(dispatch_get_main_queue(), ^{ + [module parseDidInitializeWithApplicationId:applicationId clientKey:clientKey]; + }); + }]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (void)performCollectionAccessBlock:(dispatch_block_t)block { + dispatch_sync(self.collectionQueue, block); +} + +/*! + Enumerates all existing modules in this collection. + + NOTE: This **will modify the contents of the collection** if any of the modules were deallocated since last loop. + + @param block the block to enumerate with. + */ +- (void)enumerateModulesWithBlock:(ParseModuleEnumerationBlock)block { + [self performCollectionAccessBlock:^{ + NSMutableIndexSet *toRemove = [[NSMutableIndexSet alloc] init]; + + NSUInteger index = 0; + BOOL stop = NO; + + for (__strong id module in self.modules) { + BOOL remove = module == nil; + if (!remove) { + block(module, &stop, &remove); + } + + if (remove) { + [toRemove addIndex:index]; + } + + if (stop) break; + index++; + } + + // NSPointerArray doesn't have a -removeObjectsAtIndexes:... WHY!?!? + for (index = toRemove.firstIndex; index != NSNotFound; index = [toRemove indexGreaterThanIndex:index]) { + [self.modules removePointerAtIndex:index]; + } + }]; +} + +@end diff --git a/Parse/Internal/Parse_Private.h b/Parse/Internal/Parse_Private.h new file mode 100644 index 000000000..f95654f34 --- /dev/null +++ b/Parse/Internal/Parse_Private.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +#import "ParseManager.h" + +@class PFEventuallyQueue; + +@interface Parse () + ++ (void)_resetDataSharingIdentifiers; + ++ (ParseManager *)_currentManager; ++ (void)_clearCurrentManager; + +@end diff --git a/Parse/Internal/Product/PFProduct+Private.h b/Parse/Internal/Product/PFProduct+Private.h new file mode 100644 index 000000000..af69e41b5 --- /dev/null +++ b/Parse/Internal/Product/PFProduct+Private.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFProduct.h" + +typedef enum { + PFProductDownloadStateStart, + PFProductDownloadStateDownloading, + PFProductDownloadStateDownloaded +} PFProductDownloadState; + +@interface PFProduct () { + NSDecimalNumber *price; + NSLocale *priceLocale; + NSInteger progress; + NSString *contentPath; +} + +/// The properties below are transient properties, not stored on Parse's server. +/*! + The price of the product, discovered via iTunes Connect. + */ +@property (nonatomic, strong) NSDecimalNumber *price; + +/*! + The price locale of the product. + */ +@property (nonatomic, strong) NSLocale *priceLocale; + +/*! + The progress of the download, if one is in progress. It's an integer between 0 and 100. + */ +@property (nonatomic, assign) NSInteger progress; + +/*! + The content path of the download. + */ +@property (nonatomic, strong) NSString *contentPath; + +@end diff --git a/Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.h b/Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.h new file mode 100644 index 000000000..97d57c6fc --- /dev/null +++ b/Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@class BFTask; + +@interface PFProductsRequestResult : NSObject + +@property (nonatomic, copy, readonly) NSSet *validProducts; +@property (nonatomic, copy, readonly) NSSet *invalidProductIdentifiers; + +- (instancetype)initWithProductsResponse:(SKProductsResponse *)response; + +@end + +/*! + * This class is responsible for handling the first part of an IAP handshake. + * It sends a request to iTunes Connect with a set of product identifiers, and iTunes returns + * with a list of valid and invalid products. The class then proceeds to call the completion block passed in. + */ +@interface PFProductsRequestHandler : NSObject + +- (instancetype)initWithProductsRequest:(SKProductsRequest *)request; + +- (BFTask *)findProductsAsync; + +@end diff --git a/Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.m b/Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.m new file mode 100644 index 000000000..4dbce940a --- /dev/null +++ b/Parse/Internal/Product/ProductsRequestHandler/PFProductsRequestHandler.m @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFProductsRequestHandler.h" + +#import +#import + +@implementation PFProductsRequestResult + +- (instancetype)initWithProductsResponse:(SKProductsResponse *)response { + self = [super init]; + if (!self) return nil; + + _validProducts = [NSSet setWithArray:response.products]; + _invalidProductIdentifiers = [NSSet setWithArray:response.invalidProductIdentifiers]; + + return self; +} + +@end + +@interface PFProductsRequestHandler () + +@property (nonatomic, strong) BFTaskCompletionSource *taskCompletionSource; +@property (nonatomic, strong) SKProductsRequest *productsRequest; + +@end + +@implementation PFProductsRequestHandler + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithProductsRequest:(SKProductsRequest *)request { + self = [super init]; + if (!self) return nil; + + _productsRequest = request; + _productsRequest.delegate = self; + + return self; +} + +///-------------------------------------- +#pragma mark - Dealloc +///-------------------------------------- + +- (void)dealloc { + // Clear the delegate, as it's still an `assign`, instead of `weak` + _productsRequest.delegate = nil; +} + +///-------------------------------------- +#pragma mark - Find +///-------------------------------------- + +- (BFTask *)findProductsAsync { + _taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + [_productsRequest start]; + return _taskCompletionSource.task; +} + +///-------------------------------------- +#pragma mark - SKProductsRequestDelegate +///-------------------------------------- + +- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { + PFProductsRequestResult *result = [[PFProductsRequestResult alloc] initWithProductsResponse:response]; + [self.taskCompletionSource trySetResult:result]; +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { + [self.taskCompletionSource trySetError:error]; + + // according to documentation, this method does not call requestDidFinish + request.delegate = nil; +} + +- (void)requestDidFinish:(SKRequest *)request { + // the documentation assures that this is the point safe to get rid of the request + request.delegate = nil; +} + +@end diff --git a/Parse/Internal/PropertyInfo/PFPropertyInfo.h b/Parse/Internal/PropertyInfo/PFPropertyInfo.h new file mode 100644 index 000000000..efd4c1759 --- /dev/null +++ b/Parse/Internal/PropertyInfo/PFPropertyInfo.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFBaseState.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFPropertyInfo : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithClass:(Class)kls + name:(NSString *)propertyName; + +- (instancetype)initWithClass:(Class)kls + name:(NSString *)propertyName + associationType:(PFPropertyInfoAssociationType)associationType NS_DESIGNATED_INITIALIZER; + ++ (instancetype)propertyInfoWithClass:(Class)kls + name:(NSString *)propertyName; + ++ (instancetype)propertyInfoWithClass:(Class)kls + name:(NSString *)propertyName + associationType:(PFPropertyInfoAssociationType)associationType; + +@property (nonatomic, copy, readonly) NSString *name; +@property (nonatomic, readonly) PFPropertyInfoAssociationType associationType; + +/*! + Returns the value of this property, + properly wrapped from the target object. + When possible, just invokes the property. + When not, uses -valueForKey:. + */ +- (nullable id)getWrappedValueFrom:(id)object; +- (void)setWrappedValue:(nullable id)value forObject:(id)object; + +// Moves the value from one object to the other, based on the association type given. +- (void)takeValueFrom:(id)one toObject:(id)two; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/PropertyInfo/PFPropertyInfo.m b/Parse/Internal/PropertyInfo/PFPropertyInfo.m new file mode 100644 index 000000000..7fdcff85b --- /dev/null +++ b/Parse/Internal/PropertyInfo/PFPropertyInfo.m @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPropertyInfo_Private.h" + +#import + +#import "PFAssert.h" +#import "PFMacros.h" +#import "PFPropertyInfo_Runtime.h" + +static inline NSString *safeStringWithPropertyAttributeValue(objc_property_t property, const char *attribute) { + char *value = property_copyAttributeValue(property, attribute); + if (!value) + return nil; + + // NSString initWithBytesNoCopy doesn't seem to work, so fall back to the CF counterpart. + return (__bridge_transfer NSString *)CFStringCreateWithCStringNoCopy(NULL, + value, + kCFStringEncodingUTF8, + kCFAllocatorMalloc); +} + +static inline NSString *stringByCapitalizingFirstCharacter(NSString *string) { + return [NSString stringWithFormat:@"%C%@", + (unichar)toupper([string characterAtIndex:0]), + [string substringFromIndex:1]]; +} + +@implementation PFPropertyInfo + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithClass:(Class)kls name:(NSString *)propertyName { + return [self initWithClass:kls name:propertyName associationType:_associationType]; +} + +- (instancetype)initWithClass:(Class)kls name:(NSString *)propertyName + associationType:(PFPropertyInfoAssociationType)associationType { + self = [super init]; + if (!self) return nil; + + _sourceClass = kls; + _name = [propertyName copy]; + _associationType = associationType; + + objc_property_t objcProperty = class_getProperty(kls, [_name UTF8String]); + + do { + _ivar = class_getInstanceVariable(kls, [safeStringWithPropertyAttributeValue(objcProperty, "V") UTF8String]); + if (_ivar) break; + + // If we implement a property in a subclass with a different type then the parent, but rely upon the parent's + // implementation, we will have to attempt to infer the variable name... + // TODO: (richardross): Walk the superclass tree for the synthesized value? + _ivar = class_getInstanceVariable(kls, [[@"_" stringByAppendingString:_name] UTF8String]); + if (_ivar) break; + + _ivar = class_getInstanceVariable(kls, [_name UTF8String]); + } while (0); + + _typeEncoding = safeStringWithPropertyAttributeValue(objcProperty, "T"); + _object = [_typeEncoding hasPrefix:@"@"]; + + NSString *propertyGetter = safeStringWithPropertyAttributeValue(objcProperty, "G") ?: _name; + _getterSelector = NSSelectorFromString(propertyGetter); + + BOOL readonly = safeStringWithPropertyAttributeValue(objcProperty, "R") != nil; + NSString *propertySetter = safeStringWithPropertyAttributeValue(objcProperty, "S"); + if (propertySetter == nil && !readonly) { + propertySetter = [NSString stringWithFormat:@"set%@:", stringByCapitalizingFirstCharacter(_name)]; + } + + _setterSelector = NSSelectorFromString(propertySetter); + + if (_associationType == PFPropertyInfoAssociationTypeDefault) { + // TODO: (richardross) Check if the property is weak as well. + BOOL isCopy = safeStringWithPropertyAttributeValue(objcProperty, "C") != nil; + _associationType = (_object ? (isCopy ? PFPropertyInfoAssociationTypeCopy + : PFPropertyInfoAssociationTypeStrong) + : PFPropertyInfoAssociationTypeAssign); + } + + return self; +} + ++ (instancetype)propertyInfoWithClass:(Class)kls name:(NSString *)propertyName { + return [[self alloc] initWithClass:kls name:propertyName]; +} + ++ (instancetype)propertyInfoWithClass:(Class)kls name:(NSString *)propertyName + associationType:(PFPropertyInfoAssociationType)associationType { + return [[self alloc] initWithClass:kls name:propertyName associationType:associationType]; +} + +///-------------------------------------- +#pragma mark - Wrapping +///-------------------------------------- + +- (id)getWrappedValueFrom:(id)object { + if (self.object) { + return objc_msgSend_safe(id)(object, self.getterSelector); + } + + return [object valueForKey:self.name]; +} + +- (void)setWrappedValue:(id)value forObject:(id)object { + if (self.object && self.setterSelector) { + objc_msgSend_safe(void, id)(object, self.setterSelector, value); + return; + } + + [object setValue:value forKey:self.name]; +} + +///-------------------------------------- +#pragma mark - Taking +///-------------------------------------- + +- (void)takeValueFrom:(id)one toObject:(id)two { + if (!self.ivar) { + id wrappedValue = [self getWrappedValueFrom:one]; + [self setWrappedValue:wrappedValue forObject:two]; + + return; + } + + NSUInteger size = 0; + NSGetSizeAndAlignment(ivar_getTypeEncoding(self.ivar), &size, NULL); + + char valuePtr[size]; + bzero(valuePtr, size); + + NSInvocation *invocation = nil; + + // TODO: (richardross) Cache the method signatures, as those are fairly slow to calculate. + if (one && [one respondsToSelector:self.getterSelector]) { + NSMethodSignature *methodSignature = [one methodSignatureForSelector:self.getterSelector]; + invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + + [invocation setTarget:one]; + [invocation setSelector:self.getterSelector]; + } + + [invocation invoke]; + [invocation getReturnValue:valuePtr]; + + object_setIvarValue_safe(two, self.ivar, valuePtr, self.associationType); +} + +///-------------------------------------- +#pragma mark - Equality +///-------------------------------------- + +- (NSUInteger)hash { + return 0; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[self class]]) { + return NO; + } + + PFPropertyInfo *other = object; + + // If they're the same property and one of them subclasses the other, do no further checking. + return [self.name isEqual:other.name] && + ([self.sourceClass isSubclassOfClass:other.sourceClass] || + [other.sourceClass isSubclassOfClass:self.sourceClass]); +} + +@end diff --git a/Parse/Internal/PropertyInfo/PFPropertyInfo_Private.h b/Parse/Internal/PropertyInfo/PFPropertyInfo_Private.h new file mode 100644 index 000000000..db9ebea5b --- /dev/null +++ b/Parse/Internal/PropertyInfo/PFPropertyInfo_Private.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFPropertyInfo.h" + +@interface PFPropertyInfo () + +@property (atomic, assign, readonly) Class sourceClass; +@property (atomic, assign, readonly, getter=isObject) BOOL object; + +@property (atomic, copy, readonly) NSString *typeEncoding; +@property (atomic, assign, readonly) Ivar ivar; + +@property (atomic, assign, readonly) SEL getterSelector; +@property (atomic, assign, readonly) SEL setterSelector; + +@end diff --git a/Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.h b/Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.h new file mode 100644 index 000000000..bc1b59e58 --- /dev/null +++ b/Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPropertyInfo.h" + +#import + +/*! + @abstract Safely sets an object's instance variable to the variable in the specified address. + @discussion The Objective-C runtime's built-in methods for setting instance variables (`object_setIvar`) and + (`object_setInstanceVariable`), are both terrible. They never read any more than a single pointer, so they + fail for structs, as well as 64 bit numbers on 32 bit platforms. Because of this, we need a solution to allow us to + safely set instance variable values whose sizes may be significantly more than a pointer. + + @note Like most Objective-C runtime methods, this WILL fail if you try and set a bitfield, so please don't do that. + + @param obj The object to operate on. + @param ivar The ivar to set the new value for. + @param fromMemory The **address** of the new value to set. + @param associationType The association type of the new value. One of PFPropertyInfoAssociationType. + */ +extern void object_setIvarValue_safe(__unsafe_unretained id obj, Ivar ivar, void *fromMemory, uint8_t associationType); + +/*! + @abstract Safely gets an object's instance variable and puts it into the specified address. + @discussion The Objective-C runtime's built-in methods for getting instance variables (`object_getIvar`) and + (`object_getInstanceVariable`), are both terrible. They never read any more than a single pointer, so they + fail for structs, as well as 64 bit numbers on 32 bit platforms. Because of this, we need a solution to allow us to + safely get instance variable values whose sizes may be significantly more than a pointer. + + @note Like most Objective-C runtime methods, this WILL fail if you try and set a bitfield, so please don't do that. + + @param obj The object to operate on. + @param ivar The ivar to get the value from. + @param toMemory The address to copy the value into. + @param associationType The assocation type of the new value. One of PFPrropertyInfoAssocationType. + */ +extern void object_getIvarValue_safe(__unsafe_unretained id obj, Ivar ivar, void *toMemory, uint8_t associationType); diff --git a/Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.m b/Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.m new file mode 100644 index 000000000..a2071f11a --- /dev/null +++ b/Parse/Internal/PropertyInfo/PFPropertyInfo_Runtime.m @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPropertyInfo_Runtime.h" + +#import +#import + +/*! + This macro is really interesting. Because ARC will insert implicit retains, releases and other memory managment code + that we don't want here, we have to basically trick ARC into treating the functions we want as functions with type + `void *`. The way we do that is actually via the linker - instead of coming up with some crazy macro to forward all + arguments along to the correct function, especially when some of these functions aren't in any public headers. + + They are, however, well defined, according to the clang official ARC runtime support document: + http://clang.llvm.org/docs/AutomaticReferenceCounting.html#id55 + + That means this is unlikely to ever break. + + The weakref attribute is documented here: + https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes + + And we use this to make sure our type-invariant functions actually call the proper underlying ones. + */ +#define NO_TYPECHECK_SYMBOL(ret, fn, args...) static ret fn ## _noTypeCheck (args) __attribute__((weakref(#fn))); +#define OBJECT_GETOFFSET_PTR(obj, offset) (void *) ((uintptr_t)obj + offset) + +NO_TYPECHECK_SYMBOL(void *, objc_loadWeak, void **); + +NO_TYPECHECK_SYMBOL(void *, objc_storeWeak, void **, void *); +NO_TYPECHECK_SYMBOL(void *, objc_storeStrong, void **, void *); + +NO_TYPECHECK_SYMBOL(void *, objc_autorelease, void *); +NO_TYPECHECK_SYMBOL(void *, objc_retainAutorelease, void *); + +void object_getIvarValue_safe(__unsafe_unretained id obj, Ivar ivar, void *toMemory, uint8_t associationType) { + ptrdiff_t offset = ivar_getOffset(ivar); + void *location = OBJECT_GETOFFSET_PTR(obj, offset); + + switch (associationType) { + case PFPropertyInfoAssociationTypeDefault: + [NSException raise:NSInvalidArgumentException format:@"Invalid association type Default!"]; + break; + + case PFPropertyInfoAssociationTypeAssign: { + NSUInteger size = 0; + NSGetSizeAndAlignment(ivar_getTypeEncoding(ivar), &size, NULL); + + memcpy(toMemory, location, size); + break; + } + + case PFPropertyInfoAssociationTypeWeak: { + void *results = objc_loadWeak_noTypeCheck(location); + + memcpy(toMemory, &results, sizeof(id)); + break; + } + + case PFPropertyInfoAssociationTypeStrong: + case PFPropertyInfoAssociationTypeCopy: + case PFPropertyInfoAssociationTypeMutableCopy: { + void *objectValue = *(void **)location; + objectValue = objc_retainAutorelease_noTypeCheck(objectValue); + + memcpy(toMemory, &objectValue, sizeof(id)); + break; + } + } +} + +void object_setIvarValue_safe(__unsafe_unretained id obj, Ivar ivar, void *fromMemory, uint8_t associationType) { + ptrdiff_t offset = ivar_getOffset(ivar); + void *location = OBJECT_GETOFFSET_PTR(obj, offset); + + NSUInteger size = 0; + NSGetSizeAndAlignment(ivar_getTypeEncoding(ivar), &size, NULL); + + void *newValue = NULL; + + switch (associationType) { + case PFPropertyInfoAssociationTypeDefault: + [NSException raise:NSInvalidArgumentException format:@"Invalid association type Default!"]; + return; + + case PFPropertyInfoAssociationTypeAssign: { + memcpy(location, fromMemory, size); + return; + } + + case PFPropertyInfoAssociationTypeWeak: { + objc_storeWeak_noTypeCheck(location, *(void **)fromMemory); + return; + } + + case PFPropertyInfoAssociationTypeStrong: + newValue = *(void **)fromMemory; + break; + + case PFPropertyInfoAssociationTypeCopy: + case PFPropertyInfoAssociationTypeMutableCopy: { + SEL command = (associationType == PFPropertyInfoAssociationTypeCopy) ? @selector(copy) + : @selector(mutableCopy); + + + void *(*objc_msgSend_casted)(void *, SEL) = (void *)objc_msgSend; + void *oldValue = *(void **)fromMemory; + + newValue = objc_msgSend_casted(oldValue, command); + newValue = objc_autorelease_noTypeCheck(newValue); + break; + } + } + + objc_storeStrong_noTypeCheck(location, newValue); +} diff --git a/Parse/Internal/Purchase/Controller/PFPurchaseController.h b/Parse/Internal/Purchase/Controller/PFPurchaseController.h new file mode 100644 index 000000000..e3b1aee3c --- /dev/null +++ b/Parse/Internal/Purchase/Controller/PFPurchaseController.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@class BFTask; +@class PFFileManager; +@class PFPaymentTransactionObserver; +@protocol PFCommandRunning; +@class SKPaymentQueue; +@class SKPaymentTransaction; + +@interface PFPurchaseController : NSObject + +@property (nonatomic, strong, readonly) id commandRunner; +@property (nonatomic, strong, readonly) PFFileManager *fileManager; + +@property (nonatomic, strong) SKPaymentQueue *paymentQueue; +@property (nonatomic, strong, readonly) PFPaymentTransactionObserver *transactionObserver; + +@property (nonatomic, assign) Class productsRequestClass; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCommandRunner:(id)commandRunner + fileManager:(PFFileManager *)fileManager NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithCommandRunner:(id)commandRunner + fileManager:(PFFileManager *)fileManager; + +///-------------------------------------- +/// @name Products +///-------------------------------------- + +- (BFTask *)findProductsAsyncWithIdentifiers:(NSSet *)productIdentifiers; +- (BFTask *)buyProductAsyncWithIdentifier:(NSString *)productIdentifier; +- (BFTask *)downloadAssetAsyncForTransaction:(SKPaymentTransaction *)transaction + withProgressBlock:(PFProgressBlock)progressBlock + sessionToken:(NSString *)sessionToken; + +- (NSString *)assetContentPathForProductWithIdentifier:(NSString *)identifier fileName:(NSString *)fileName; +- (BOOL)canPurchase; + +@end diff --git a/Parse/Internal/Purchase/Controller/PFPurchaseController.m b/Parse/Internal/Purchase/Controller/PFPurchaseController.m new file mode 100644 index 000000000..7f5bf6dd6 --- /dev/null +++ b/Parse/Internal/Purchase/Controller/PFPurchaseController.m @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPurchaseController.h" + +#import + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFConstants.h" +#import "PFDecoder.h" +#import "PFFileManager.h" +#import "PFFile_Private.h" +#import "PFHTTPRequest.h" +#import "PFMacros.h" +#import "PFPaymentTransactionObserver.h" +#import "PFProductsRequestHandler.h" +#import "PFRESTCommand.h" + +@interface PFPurchaseController () { + PFProductsRequestHandler *_currentProductsRequestHandler; +} + +@end + +@implementation PFPurchaseController + +@synthesize paymentQueue = _paymentQueue; +@synthesize transactionObserver = _transactionObserver; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommandRunner:(id)commandRunner fileManager:(PFFileManager *)fileManager { + self = [super init]; + if (!self) return nil; + + _commandRunner = commandRunner; + _fileManager = fileManager; + + return self; +} + ++ (instancetype)controllerWithCommandRunner:(id)commandRunner + fileManager:(PFFileManager *)fileManager { + return [[self alloc] initWithCommandRunner:commandRunner fileManager:fileManager]; +} + +///-------------------------------------- +#pragma mark - Dealloc +///-------------------------------------- + +- (void)dealloc { + if (_paymentQueue && _transactionObserver) { + [_paymentQueue removeTransactionObserver:_transactionObserver]; + } +} + +///-------------------------------------- +#pragma mark - Products +///-------------------------------------- + +- (BFTask *)findProductsAsyncWithIdentifiers:(NSSet *)productIdentifiers { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id { + @strongify(self); + Class requestClass = self.productsRequestClass ?: [SKProductsRequest class]; + SKProductsRequest *request = [[requestClass alloc] initWithProductIdentifiers:productIdentifiers]; + _currentProductsRequestHandler = [[PFProductsRequestHandler alloc] initWithProductsRequest:request]; + return [_currentProductsRequestHandler findProductsAsync]; + }] continueWithSuccessBlock:^id(BFTask *task) { + _currentProductsRequestHandler = nil; + return task; + }]; +} + +- (BFTask *)buyProductAsyncWithIdentifier:(NSString *)productIdentifier { + PFParameterAssert(productIdentifier, @"You must pass in a valid product identifier."); + + if (![self canPurchase]) { + NSError *error = [NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorPaymentDisabled + userInfo:nil]; + return [BFTask taskWithError:error]; + } + NSSet *identifiers = PF_SET(productIdentifier); + @weakify(self); + return [[self findProductsAsyncWithIdentifiers:identifiers] continueWithSuccessBlock:^id(BFTask *task) { + PFProductsRequestResult *result = task.result; + @strongify(self); + + for (NSString *invalidIdentifier in result.invalidProductIdentifiers) { + if ([invalidIdentifier isEqualToString:productIdentifier]) { + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorInvalidProductIdentifier + userInfo:nil]]; + } + } + + for (SKProduct *product in result.validProducts) { + if ([product.productIdentifier isEqualToString:productIdentifier]) { + BFTaskCompletionSource *source = [BFTaskCompletionSource taskCompletionSource]; + [self.transactionObserver handle:productIdentifier runOnceBlock:^(NSError *error) { + if (error) { + [source trySetError:error]; + } else { + [source trySetResult:nil]; + } + }]; + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [self.paymentQueue addPayment:payment]; + return source.task; + } + } + + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorProductNotFoundInAppStore + userInfo:nil]]; + }]; +} + +- (BFTask *)downloadAssetAsyncForTransaction:(SKPaymentTransaction *)transaction + withProgressBlock:(PFProgressBlock)progressBlock + sessionToken:(NSString *)sessionToken { + // Ignore the deprecation, as it works until iOS 9. + // TODO: (nlutsenko) Update for iOS 9 receipt verification. This will require server-side change, most likely. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSData *transactionReceipt = transaction.transactionReceipt; +#pragma clang diagnostic pop + if (!transactionReceipt) { + NSError *error = [NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorReceiptMissing + userInfo:nil]; + return [BFTask taskWithError:error]; + } + + NSDictionary *params = [[PFEncoder objectEncoder] encodeObject:@{ @"receipt" : transactionReceipt }]; + PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"validate_purchase" + httpMethod:PFHTTPRequestMethodPOST + parameters:params + sessionToken:sessionToken]; + BFTask *task = [self.commandRunner runCommandAsync:command withOptions:PFCommandRunningOptionRetryIfFailed]; + @weakify(self); + return [task continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + + PFCommandResult *result = task.result; + PFFile *file = [[PFDecoder objectDecoder] decodeObject:result.result]; + if (![file isKindOfClass:[PFFile class]]) { + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorInvalidPurchaseReceipt + userInfo:result.result]]; + } + + NSString *finalFilePath = [self assetContentPathForProductWithIdentifier:transaction.payment.productIdentifier + fileName:file.name]; + NSString *directoryPath = [finalFilePath stringByDeletingLastPathComponent]; + return [[[[[PFFileManager createDirectoryIfNeededAsyncAtPath:directoryPath] continueWithBlock:^id(BFTask *task) { + if (task.faulted) { + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorProductDownloadFileSystemFailure + userInfo:nil]]; + } + return file; + }] continueWithSuccessBlock:^id(BFTask *task) { + return [file getDataStreamInBackgroundWithProgressBlock:progressBlock]; + }] continueWithSuccessBlock:^id(BFTask *task) { + NSString *cachedFilePath = [file _cachedFilePath]; + return [[PFFileManager copyItemAsyncAtPath:cachedFilePath + toPath:finalFilePath] continueWithBlock:^id(BFTask *task) { + // No-op file exists error. + if (task.error.code == NSFileWriteFileExistsError) { + return nil; + } + return task; + }]; + }] continueWithSuccessResult:finalFilePath]; + }]; +} + +- (NSString *)assetContentPathForProductWithIdentifier:(NSString *)identifier fileName:(NSString *)fileName { + // We store files locally at (ParsePrivateDir)/(ProductIdentifier)/filename + NSString *filePath = [self.fileManager parseDataItemPathForPathComponent:identifier]; + filePath = [filePath stringByAppendingPathComponent:fileName]; + return filePath; +} + +- (BOOL)canPurchase { + return [[self.paymentQueue class] canMakePayments]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (SKPaymentQueue *)paymentQueue { + if (!_paymentQueue) { + _paymentQueue = [SKPaymentQueue defaultQueue]; + } + return _paymentQueue; +} + +- (PFPaymentTransactionObserver *)transactionObserver { + if (!_transactionObserver) { + _transactionObserver = [[PFPaymentTransactionObserver alloc] init]; + [self.paymentQueue addTransactionObserver:_transactionObserver]; + } + return _transactionObserver; +} + +@end diff --git a/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.h b/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.h new file mode 100644 index 000000000..8509c43eb --- /dev/null +++ b/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +/*! + * The PFPaymentTransactionObserver listens to the payment queue, processes a payment by running business logic, + * and completes the transaction. It's a complex interaction and best explained as follows: + * 1) an observer object is created and added to the payment queue, typically before IAP happens (but not necessarily), + * 2) PFPurchase creates a payment and adds it to the payment queue, + * 3) when the observer sees this payment, it runs the business logic associated with this payment, + * 4) when the business logic finishes, the observer completes the transaction. If the business logic does not finish, the transaction is not completed, which means the user does not get charged, + * 5) after the transaction finishes, custom UI logic is run. + */ +@interface PFPaymentTransactionObserver : NSObject + +- (void)handle:(NSString *)productIdentifier block:(void (^)(SKPaymentTransaction *))block; +- (void)handle:(NSString *)productIdentifier runOnceBlock:(void (^)(NSError *))block; + +@end diff --git a/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.m b/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.m new file mode 100644 index 000000000..100750de9 --- /dev/null +++ b/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver.m @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPaymentTransactionObserver_Private.h" + +#import "PFAssert.h" + +@implementation PFPaymentTransactionObserver + +@synthesize blocks; +@synthesize runOnceBlocks; +@synthesize lockObj; +@synthesize runOnceLockObj; + +///-------------------------------------- +#pragma mark - NSObject +///-------------------------------------- + +- (instancetype)init { + if (self = [super init]) { + blocks = [[NSMutableDictionary alloc] init]; + runOnceBlocks = [[NSMutableDictionary alloc] init]; + lockObj = [[NSObject alloc] init]; + runOnceLockObj = [[NSObject alloc] init]; + } + return self; +} + +///-------------------------------------- +#pragma mark - SKPaymentTransactionObserver +///-------------------------------------- + +- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { + for (SKPaymentTransaction *transaction in transactions) { + switch (transaction.transactionState) { + case SKPaymentTransactionStatePurchased: + case SKPaymentTransactionStateFailed: + case SKPaymentTransactionStateRestored: + [self completeTransaction:transaction fromPaymentQueue:queue]; + break; + default: + break; + } + } +} + +///-------------------------------------- +#pragma mark - PFPaymentTransactionObserver +///-------------------------------------- + +- (void)completeTransaction:(SKPaymentTransaction *)transaction fromPaymentQueue:(SKPaymentQueue *)queue { + NSString *productIdentifier = transaction.payment.productIdentifier; + + @synchronized(lockObj) { + void(^completion)(SKPaymentTransaction *) = self.blocks[productIdentifier]; + if (!transaction.error && completion) { + completion(transaction); + } + } + + @synchronized(runOnceLockObj) { + void(^runOnceBlock)(NSError *) = (void(^)(NSError *))[self.runOnceBlocks objectForKey:productIdentifier]; + if (runOnceBlock) { + runOnceBlock(transaction.error); + [self.runOnceBlocks removeObjectForKey:productIdentifier]; + } + } + + // Calling finish:transaction here prevents the user from registering another observer to handle this transaction. + [queue finishTransaction:transaction]; +} + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + +- (void)handle:(NSString *)productIdentifier block:(void(^)(SKPaymentTransaction *))block { + @synchronized(lockObj) { + self.blocks[productIdentifier] = block; + } +} + +- (void)handle:(NSString *)productIdentifier runOnceBlock:(void(^)(NSError *))block { + @synchronized(runOnceLockObj) { + PFConsistencyAssert(self.runOnceBlocks[productIdentifier] == nil, + @"You cannot purchase a product that is in the process of being purchased."); + + if (!block) { + // Create a no-op action so that we can store it in the dictionary, + // this is useful because we use the existence of this block to test if + // the same product is being purchased at the time. + block = ^(NSError *error) { + }; + } + self.runOnceBlocks[productIdentifier] = block; + } +} + +@end diff --git a/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver_Private.h b/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver_Private.h new file mode 100644 index 000000000..268af07fc --- /dev/null +++ b/Parse/Internal/Purchase/PaymentTransactionObserver/PFPaymentTransactionObserver_Private.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPaymentTransactionObserver.h" + +@interface PFPaymentTransactionObserver () + +@property (nonatomic, strong) NSMutableDictionary *blocks; +@property (nonatomic, strong) NSMutableDictionary *runOnceBlocks; +@property (nonatomic, strong) NSObject *lockObj; +@property (nonatomic, strong) NSObject *runOnceLockObj; + +@end diff --git a/Parse/Internal/Push/ChannelsController/PFPushChannelsController.h b/Parse/Internal/Push/ChannelsController/PFPushChannelsController.h new file mode 100644 index 000000000..ec2e429dc --- /dev/null +++ b/Parse/Internal/Push/ChannelsController/PFPushChannelsController.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreDataProvider.h" + +@class BFTask; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFPushChannelsController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Get +///-------------------------------------- + +- (BFTask *)getSubscribedChannelsAsync; + +///-------------------------------------- +/// @name Subscribe +///-------------------------------------- + +- (BFTask *)subscribeToChannelAsyncWithName:(NSString *)name; +- (BFTask *)unsubscribeFromChannelAsyncWithName:(NSString *)name; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/ChannelsController/PFPushChannelsController.m b/Parse/Internal/Push/ChannelsController/PFPushChannelsController.m new file mode 100644 index 000000000..22cb8adf3 --- /dev/null +++ b/Parse/Internal/Push/ChannelsController/PFPushChannelsController.m @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPushChannelsController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCurrentInstallationController.h" +#import "PFErrorUtilities.h" +#import "PFInstallation.h" + +@interface PFPushChannelsController () + +@property (nonatomic, strong, readonly) PFCurrentInstallationController *currentInstallationController; + +@end + +@implementation PFPushChannelsController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(nonnull id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)controllerWithDataSource:(nonnull id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Get +///-------------------------------------- + +- (BFTask *)getSubscribedChannelsAsync { + return [[self _getCurrentObjectAsync] continueWithSuccessBlock:^id(BFTask *task) { + PFInstallation *installation = task.result; + + BFTask *installationTask = (installation.objectId + ? [installation fetchInBackground] + : [installation saveInBackground]); + + return [installationTask continueWithSuccessBlock:^id(BFTask *task) { + return [NSSet setWithArray:installation.channels]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Subscribe +///-------------------------------------- + +- (BFTask *)subscribeToChannelAsyncWithName:(nonnull NSString *)name { + return [[self _getCurrentObjectAsync] continueWithSuccessBlock:^id(BFTask *task) { + PFInstallation *installation = task.result; + if ([installation.channels containsObject:name] && + ![installation isDirtyForKey:@"channels"]) { + return @YES; + } + + [installation addUniqueObject:name forKey:@"channels"]; + return [installation saveInBackground]; + }]; +} + +- (BFTask *)unsubscribeFromChannelAsyncWithName:(nonnull NSString *)name { + return [[self _getCurrentObjectAsync] continueWithSuccessBlock:^id(BFTask *task) { + PFInstallation *installation = task.result; + if (name.length != 0 && + ![installation.channels containsObject:name] && + ![installation isDirtyForKey:@"channels"]) { + return @YES; + } + [installation removeObject:name forKey:@"channels"]; + return [installation saveInBackground]; + }]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +- (BFTask *)_getCurrentObjectAsync { + return [[self.currentInstallationController getCurrentObjectAsync] continueWithSuccessBlock:^id(BFTask *task) { + PFInstallation *installation = task.result; + if (!installation.deviceToken) { + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorPushMisconfigured + message:@"There is no device token stored yet."]; + return [BFTask taskWithError:error]; + } + + return task; + }]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (PFCurrentInstallationController *)currentInstallationController { + return self.dataSource.currentInstallationController; +} + +@end diff --git a/Parse/Internal/Push/Controller/PFPushController.h b/Parse/Internal/Push/Controller/PFPushController.h new file mode 100644 index 000000000..37741dd50 --- /dev/null +++ b/Parse/Internal/Push/Controller/PFPushController.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; +@class PFPushState; +@protocol PFCommandRunning; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFPushController : NSObject + +@property (nonatomic, strong, readonly) id commandRunner; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCommandRunner:(id)commandRunner NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithCommandRunner:(id)commandRunner; + +///-------------------------------------- +/// @name Sending Push +///-------------------------------------- + +/*! + Requests push notification to be sent for a given state. + + @param state State to use to send notifications. + @param sessionToken Current user session token. + + @returns `BFTask` with result set to `NSNumber` with `BOOL` identifying whether the request succeeded. + */ +- (BFTask *)sendPushNotificationAsyncWithState:(PFPushState *)state sessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/Controller/PFPushController.m b/Parse/Internal/Push/Controller/PFPushController.m new file mode 100644 index 000000000..36472de7c --- /dev/null +++ b/Parse/Internal/Push/Controller/PFPushController.m @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPushController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandRunning.h" +#import "PFMacros.h" +#import "PFRESTPushCommand.h" + +@implementation PFPushController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommandRunner:(id)commandRunner { + self = [super init]; + if (!self) return nil; + + _commandRunner = commandRunner; + + return self; +} + ++ (instancetype)controllerWithCommandRunner:(id)commandRunner { + return [[self alloc] initWithCommandRunner:commandRunner]; +} + +///-------------------------------------- +#pragma mark - Sending Push +///-------------------------------------- + +- (BFTask *)sendPushNotificationAsyncWithState:(PFPushState *)state + sessionToken:(NSString *)sessionToken { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [PFRESTPushCommand sendPushCommandWithPushState:state sessionToken:sessionToken]; + return [self.commandRunner runCommandAsync:command withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + return @(task.result != nil); + }]; +} + +@end diff --git a/Parse/Internal/Push/Manager/PFPushManager.h b/Parse/Internal/Push/Manager/PFPushManager.h new file mode 100644 index 000000000..6abb94a09 --- /dev/null +++ b/Parse/Internal/Push/Manager/PFPushManager.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreDataProvider.h" +#import "PFDataProvider.h" + +@class PFPushChannelsController; +@class PFPushController; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFPushManager : NSObject + +@property (nonatomic, weak, readonly) id commonDataSource; +@property (nonatomic, weak, readonly) id coreDataSource; + +@property (nonatomic, strong) PFPushController *pushController; +@property (nonatomic, strong) PFPushChannelsController *channelsController; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource NS_DESIGNATED_INITIALIZER; + ++ (instancetype)managerWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/Manager/PFPushManager.m b/Parse/Internal/Push/Manager/PFPushManager.m new file mode 100644 index 000000000..d02f68117 --- /dev/null +++ b/Parse/Internal/Push/Manager/PFPushManager.m @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPushManager.h" + +#import "PFAssert.h" +#import "PFMacros.h" +#import "PFPushChannelsController.h" +#import "PFPushController.h" + +@interface PFPushManager () { + dispatch_queue_t _controllerAccessQueue; +} + +@end + +@implementation PFPushManager + +@synthesize pushController = _pushController; +@synthesize channelsController = _channelsController; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + self = [super init]; + if (!self) return nil; + + _commonDataSource = commonDataSource; + _coreDataSource = coreDataSource; + _controllerAccessQueue = dispatch_queue_create("com.parse.push.controller.accessQueue", DISPATCH_QUEUE_SERIAL); + + return self; +} + ++ (instancetype)managerWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + return [[self alloc] initWithCommonDataSource:commonDataSource coreDataSource:coreDataSource]; +} + +///-------------------------------------- +#pragma mark - PushController +///-------------------------------------- + +- (PFPushController *)pushController { + __block PFPushController *controller; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_pushController) { + _pushController = [PFPushController controllerWithCommandRunner:self.commonDataSource.commandRunner]; + } + controller = _pushController; + }); + return controller; +} + +- (void)setPushController:(PFPushController *)pushController { + dispatch_sync(_controllerAccessQueue, ^{ + _pushController = pushController; + }); +} + +///-------------------------------------- +#pragma mark - Channels Controller +///-------------------------------------- + +- (PFPushChannelsController *)channelsController { + __block PFPushChannelsController *controller; + dispatch_sync(_controllerAccessQueue, ^{ + if (!_channelsController) { + _channelsController = [PFPushChannelsController controllerWithDataSource:self.coreDataSource]; + } + controller = _channelsController; + }); + return controller; +} + +- (void)setChannelsController:(PFPushChannelsController *)channelsController { + dispatch_sync(_controllerAccessQueue, ^{ + _channelsController = channelsController; + }); +} + +@end diff --git a/Parse/Internal/Push/PFPushPrivate.h b/Parse/Internal/Push/PFPushPrivate.h new file mode 100644 index 000000000..4ad0efd11 --- /dev/null +++ b/Parse/Internal/Push/PFPushPrivate.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +#import "PFMacros.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol PFPushInternalUtils + +@optional ++ (NSString *)convertDeviceTokenToString:(id)deviceToken; ++ (nullable NSString *)getDeviceTokenFromKeychain; ++ (void)clearDeviceToken; + +#if TARGET_OS_IPHONE + ++ (void)showAlertViewWithTitle:(nullable NSString *)title message:(nullable NSString *)message; ++ (void)playVibrate; ++ (void)playAudioWithName:(nullable NSString *)audioName; + +#endif + +@end + +@interface PFPush (Private) + +// For unit testability ++ (Class)pushInternalUtilClass; ++ (void)setPushInternalUtilClass:(nullable Class)utilClass; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/State/PFMutablePushState.h b/Parse/Internal/Push/State/PFMutablePushState.h new file mode 100644 index 000000000..1720a8032 --- /dev/null +++ b/Parse/Internal/Push/State/PFMutablePushState.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPushState.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFMutablePushState : PFPushState + +@property (nullable, nonatomic, copy, readwrite) NSSet *channels; +@property (nullable, nonatomic, copy, readwrite) PFQueryState *queryState; + +@property (nullable, nonatomic, strong, readwrite) NSDate *expirationDate; +@property (nullable, nonatomic, copy, readwrite) NSNumber *expirationTimeInterval; + +@property (nullable, nonatomic, copy, readwrite) NSDictionary *payload; + +///-------------------------------------- +/// @name Payload +///-------------------------------------- + +- (void)setPayloadWithMessage:(nullable NSString *)message; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/State/PFMutablePushState.m b/Parse/Internal/Push/State/PFMutablePushState.m new file mode 100644 index 000000000..998f75b27 --- /dev/null +++ b/Parse/Internal/Push/State/PFMutablePushState.m @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutablePushState.h" + +#import "PFPushState_Private.h" + +@implementation PFMutablePushState + +@dynamic channels; +@dynamic queryState; +@dynamic expirationDate; +@dynamic expirationTimeInterval; +@dynamic payload; + +///-------------------------------------- +#pragma mark - Payload +///-------------------------------------- + +- (void)setPayloadWithMessage:(NSString *)message { + if (!message) { + self.payload = nil; + } else { + self.payload = @{ @"alert" : [message copy] }; + } +} + +@end diff --git a/Parse/Internal/Push/State/PFPushState.h b/Parse/Internal/Push/State/PFPushState.h new file mode 100644 index 000000000..a94224967 --- /dev/null +++ b/Parse/Internal/Push/State/PFPushState.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFBaseState.h" + +@class PFQueryState; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFPushState : PFBaseState + +@property (nullable, nonatomic, copy, readonly) NSSet *channels; +@property (nullable, nonatomic, copy, readonly) PFQueryState *queryState; + +@property (nullable, nonatomic, strong, readonly) NSDate *expirationDate; +@property (nullable, nonatomic, copy, readonly) NSNumber *expirationTimeInterval; + +@property (nullable, nonatomic, copy, readonly) NSDictionary *payload; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithState:(nullable PFPushState *)state; ++ (instancetype)stateWithState:(nullable PFPushState *)state; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/State/PFPushState.m b/Parse/Internal/Push/State/PFPushState.m new file mode 100644 index 000000000..6eaa24543 --- /dev/null +++ b/Parse/Internal/Push/State/PFPushState.m @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPushState.h" +#import "PFPushState_Private.h" + +#import "PFMutablePushState.h" +#import "PFQueryState.h" + +@implementation PFPushState + +///-------------------------------------- +#pragma mark - PFBaseStateSubclass +///-------------------------------------- + ++ (NSDictionary *)propertyAttributes { + return @{ + @"channels": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"queryState": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"expirationDate": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeStrong], + @"expirationTimeInterval": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeStrong], + @"payload": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy] + }; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithState:(PFPushState *)state { + return [super initWithState:state]; +} + ++ (instancetype)stateWithState:(PFPushState *)state { + return [super stateWithState:state]; +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (id)copyWithZone:(NSZone *)zone { + return [[PFPushState allocWithZone:zone] initWithState:self]; +} + +///-------------------------------------- +#pragma mark - NSMutableCopying +///-------------------------------------- + +- (id)mutableCopyWithZone:(NSZone *)zone { + return [[PFMutablePushState allocWithZone:zone] initWithState:self]; +} + +@end diff --git a/Parse/Internal/Push/State/PFPushState_Private.h b/Parse/Internal/Push/State/PFPushState_Private.h new file mode 100644 index 000000000..33e1570ed --- /dev/null +++ b/Parse/Internal/Push/State/PFPushState_Private.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPushState.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFPushState () + +@property (nullable, nonatomic, copy, readwrite) NSSet *channels; +@property (nullable, nonatomic, copy, readwrite) PFQueryState *queryState; + +@property (nullable, nonatomic, strong, readwrite) NSDate *expirationDate; +@property (nullable, nonatomic, copy, readwrite) NSNumber *expirationTimeInterval; + +@property (nullable, nonatomic, copy, readwrite) NSDictionary *payload; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/Utilites/PFPushUtilities.h b/Parse/Internal/Push/Utilites/PFPushUtilities.h new file mode 100644 index 000000000..f312a7a75 --- /dev/null +++ b/Parse/Internal/Push/Utilites/PFPushUtilities.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFPushPrivate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFPushUtilities : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Push/Utilites/PFPushUtilities.m b/Parse/Internal/Push/Utilites/PFPushUtilities.m new file mode 100644 index 000000000..6d561cacd --- /dev/null +++ b/Parse/Internal/Push/Utilites/PFPushUtilities.m @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPushUtilities.h" + +#import + +#if TARGET_OS_IPHONE + +#import + +#endif + +#import "PFInstallationPrivate.h" +#import "PFKeychainStore.h" +#import "PFLogging.h" +#import "PFMacros.h" + +@implementation PFPushUtilities + +///-------------------------------------- +#pragma mark - PFPushInternalUtils +///-------------------------------------- + ++ (NSString *)convertDeviceTokenToString:(id)deviceToken { + if ([deviceToken isKindOfClass:[NSString class]]) { + return deviceToken; + } else { + NSMutableString *hexString = [NSMutableString string]; + const unsigned char *bytes = [deviceToken bytes]; + for (int i = 0; i < [deviceToken length]; i++) { + [hexString appendFormat:@"%02x", bytes[i]]; + } + return [NSString stringWithString:hexString]; + } +} + ++ (NSString *)getDeviceTokenFromKeychain { + // Used the first time we construct the currentInstallation, + // for backward compability with older SDKs. + PFKeychainStore *store = [[PFKeychainStore alloc] initWithService:@"ParsePush"]; + return store[@"ParsePush"]; +} + ++ (void)clearDeviceToken { + // Used in test case setup. + [[PFInstallation currentInstallation] _clearDeviceToken]; + [[[PFKeychainStore alloc] initWithService:@"ParsePush"] removeObjectForKey:@"ParsePush"]; +} + +#if TARGET_OS_IPHONE + ++ (void)showAlertViewWithTitle:(NSString *)title + message:(NSString *)message { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title + message:message + delegate:nil + cancelButtonTitle:NSLocalizedString(@"OK", + @"Default alert cancel button title.") + otherButtonTitles:nil]; + [alert show]; +} + ++ (void)playAudioWithName:(NSString *)audioFileName { + SystemSoundID soundId = -1; + + if (audioFileName) { + NSURL *bundlePath = [[NSBundle mainBundle] URLForResource:[audioFileName stringByDeletingPathExtension] + withExtension:[audioFileName pathExtension]]; + + AudioServicesCreateSystemSoundID((__bridge CFURLRef)bundlePath, &soundId); + } + + if (soundId != -1) { + AudioServicesPlaySystemSound(soundId); + } +} + ++ (void)playVibrate { + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); +} + +#endif + +@end diff --git a/Parse/Internal/Query/Controller/PFCachedQueryController.h b/Parse/Internal/Query/Controller/PFCachedQueryController.h new file mode 100644 index 000000000..db0c0571a --- /dev/null +++ b/Parse/Internal/Query/Controller/PFCachedQueryController.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFQueryController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFCachedQueryController : PFQueryController + +@property (nonatomic, weak, readonly) id commonDataSource; + +- (instancetype)initWithCommonDataSource:(id)dataSource; ++ (instancetype)controllerWithCommonDataSource:(id)dataSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Query/Controller/PFCachedQueryController.m b/Parse/Internal/Query/Controller/PFCachedQueryController.m new file mode 100644 index 000000000..b9529e23d --- /dev/null +++ b/Parse/Internal/Query/Controller/PFCachedQueryController.m @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCachedQueryController.h" + +#import + +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFDecoder.h" +#import "PFErrorUtilities.h" +#import "PFJSONSerialization.h" +#import "PFKeyValueCache.h" +#import "PFMacros.h" +#import "PFQueryState.h" +#import "PFRESTCommand.h" +#import "PFRESTQueryCommand.h" +#import "PFUser.h" + +@implementation PFCachedQueryController + +@dynamic commonDataSource; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithCommonDataSource:(id)dataSource { + return [super initWithCommonDataSource:dataSource]; +} + ++ (instancetype)controllerWithCommonDataSource:(id)dataSource { + return [super controllerWithCommonDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - PFQueryControllerSubclass +///-------------------------------------- + +- (BFTask *)runNetworkCommandAsync:(PFRESTCommand *)command + withCancellationToken:(BFCancellationToken *)cancellationToken + forQueryState:(PFQueryState *)queryState { + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + switch (queryState.cachePolicy) { + case kPFCachePolicyIgnoreCache: + { + return [self _runNetworkCommandAsync:command + withCancellationToken:cancellationToken + forQueryState:queryState]; + } + break; + case kPFCachePolicyNetworkOnly: + { + return [[self _runNetworkCommandAsync:command + withCancellationToken:cancellationToken + forQueryState:queryState] continueWithSuccessBlock:^id(BFTask *task) { + return [self _saveCommandResultAsync:task.result forCommandCacheKey:command.cacheKey]; + } cancellationToken:cancellationToken]; + } + break; + case kPFCachePolicyCacheOnly: + { + return [self _runNetworkCommandAsyncFromCache:command + withCancellationToken:cancellationToken + forQueryState:queryState]; + } + break; + case kPFCachePolicyNetworkElseCache: { + // Don't retry for network-else-cache, because it just slows things down. + BFTask *networkTask = [self _runNetworkCommandAsync:command + withCancellationToken:cancellationToken + forQueryState:queryState]; + @weakify(self); + return [networkTask continueWithBlock:^id(BFTask *task) { + @strongify(self); + if (task.cancelled || task.exception) { + return task; + } else if (task.error) { + return [self _runNetworkCommandAsyncFromCache:command + withCancellationToken:cancellationToken + forQueryState:queryState]; + } + return [self _saveCommandResultAsync:task.result forCommandCacheKey:command.cacheKey]; + } cancellationToken:cancellationToken]; + } + break; + case kPFCachePolicyCacheElseNetwork: + { + BFTask *cacheTask = [self _runNetworkCommandAsyncFromCache:command + withCancellationToken:cancellationToken + forQueryState:queryState]; + @weakify(self); + return [cacheTask continueWithBlock:^id(BFTask *task) { + @strongify(self); + if (task.error) { + return [self _runNetworkCommandAsync:command + withCancellationToken:cancellationToken + forQueryState:queryState]; + } + return task; + } cancellationToken:cancellationToken]; + } + break; + case kPFCachePolicyCacheThenNetwork: + PFConsistencyAssert(NO, @"kPFCachePolicyCacheThenNetwork is not implmented as a runner."); + break; + default: + PFConsistencyAssert(NO, @"Unrecognized cache policy: %d", queryState.cachePolicy); + break; + } + return nil; +} + +- (BFTask *)_runNetworkCommandAsync:(PFRESTCommand *)command + withCancellationToken:(BFCancellationToken *)cancellationToken + forQueryState:(PFQueryState *)queryState { + PFCommandRunningOptions options = 0; + // We don't want retries on NetworkElseCache, but rather instantly back-off to cache. + if (queryState.cachePolicy != kPFCachePolicyNetworkElseCache) { + options = PFCommandRunningOptionRetryIfFailed; + } + BFTask *networkTask = [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:options + cancellationToken:cancellationToken]; + return [networkTask continueWithSuccessBlock:^id(BFTask *task) { + if (queryState.cachePolicy == kPFCachePolicyNetworkOnly || + queryState.cachePolicy == kPFCachePolicyNetworkElseCache || + queryState.cachePolicy == kPFCachePolicyCacheElseNetwork) { + return [self _saveCommandResultAsync:task.result forCommandCacheKey:command.cacheKey]; + } + // Roll-forward the original result. + return task; + } cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - Cache +///-------------------------------------- + +- (NSString *)cacheKeyForQueryState:(PFQueryState *)queryState sessionToken:(NSString *)sessionToken { + return [PFRESTQueryCommand findCommandForQueryState:queryState withSessionToken:sessionToken].cacheKey; +} + +- (BOOL)hasCachedResultForQueryState:(PFQueryState *)queryState sessionToken:(NSString *)sessionToken { + // TODO: (nlutsenko) Once there is caching for `count`, the results for that command should also be checked. + // TODO: (nlutsenko) We should cache this result. + + NSString *cacheKey = [self cacheKeyForQueryState:queryState sessionToken:sessionToken]; + return ([self.commonDataSource.keyValueCache objectForKey:cacheKey maxAge:queryState.maxCacheAge] != nil); +} + +- (void)clearCachedResultForQueryState:(PFQueryState *)queryState sessionToken:(NSString *)sessionToken { + // TODO: (nlutsenko) Once there is caching for `count`, the results for that command should also be cleared. + NSString *cacheKey = [self cacheKeyForQueryState:queryState sessionToken:sessionToken]; + [self.commonDataSource.keyValueCache removeObjectForKey:cacheKey]; +} + +- (void)clearAllCachedResults { + [self.commonDataSource.keyValueCache removeAllObjects]; +} + +- (BFTask *)_runNetworkCommandAsyncFromCache:(PFRESTCommand *)command + withCancellationToken:(BFCancellationToken *)cancellationToken + forQueryState:(PFQueryState *)queryState { + NSString *jsonString = [self.commonDataSource.keyValueCache objectForKey:command.cacheKey + maxAge:queryState.maxCacheAge]; + if (!jsonString) { + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorCacheMiss + message:@"Cache miss." + shouldLog:NO]; + return [BFTask taskWithError:error]; + } + + NSDictionary *object = [PFJSONSerialization JSONObjectFromString:jsonString]; + if (!object) { + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorCacheMiss + message:@"Cache contains corrupted JSON."]; + return [BFTask taskWithError:error]; + } + + NSDictionary *decodedObject = [[PFDecoder objectDecoder] decodeObject:object]; + + PFCommandResult *result = [PFCommandResult commandResultWithResult:decodedObject + resultString:jsonString + httpResponse:nil]; + return [BFTask taskWithResult:result]; +} + +- (BFTask *)_saveCommandResultAsync:(PFCommandResult *)result forCommandCacheKey:(NSString *)cacheKey { + NSString *resultString = result.resultString; + if (resultString) { + [self.commonDataSource.keyValueCache setObject:resultString forKey:cacheKey]; + } + // Roll-forward the original result. + return [BFTask taskWithResult:result]; +} + +@end diff --git a/Parse/Internal/Query/Controller/PFOfflineQueryController.h b/Parse/Internal/Query/Controller/PFOfflineQueryController.h new file mode 100644 index 000000000..a7f0241d2 --- /dev/null +++ b/Parse/Internal/Query/Controller/PFOfflineQueryController.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFQueryController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFOfflineQueryController : PFQueryController + +@property (nonatomic, weak, readonly) id commonDataSource; +@property (nonatomic, weak, readonly) id coreDataSource; + +- (instancetype)initWithCommonDataSource:(id)dataSource NS_UNAVAILABLE; ++ (instancetype)controllerWithCommonDataSource:(id)dataSource NS_UNAVAILABLE; + +- (instancetype)initWithCommonDataSource:(id)dataSource + coreDataSource:(id)coreDataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithCommonDataSource:(id)dataSource + coreDataSource:(id)coreDataSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Query/Controller/PFOfflineQueryController.m b/Parse/Internal/Query/Controller/PFOfflineQueryController.m new file mode 100644 index 000000000..edca5d55e --- /dev/null +++ b/Parse/Internal/Query/Controller/PFOfflineQueryController.m @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFOfflineQueryController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandRunning.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFPin.h" +#import "PFPinningObjectStore.h" +#import "PFQueryState.h" +#import "PFRESTCommand.h" +#import "PFRelationPrivate.h" + +@interface PFOfflineQueryController () { + PFOfflineStore *_offlineStore; // TODO: (nlutsenko) Lazy-load this via self.dataSource. +} + +@end + +@implementation PFOfflineQueryController + +@dynamic commonDataSource; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithCommonDataSource:(id)dataSource { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommonDataSource:(id)dataSource + coreDataSource:(id)coreDataSource { + self = [super initWithCommonDataSource:dataSource]; + if (!self) return nil; + + _offlineStore = dataSource.offlineStore; + _coreDataSource = coreDataSource; + + return self; +} + ++ (instancetype)controllerWithCommonDataSource:(id)dataSource + coreDataSource:(id)coreDataSource { + return [[self alloc] initWithCommonDataSource:dataSource coreDataSource:coreDataSource]; +} + +///-------------------------------------- +#pragma mark - Find +///-------------------------------------- + +- (BFTask *)findObjectsAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(BFCancellationToken *)cancellationToken + user:(PFUser *)user { + if (queryState.queriesLocalDatastore) { + return [self _findObjectsFromLocalDatastoreAsyncForQueryState:queryState + withCancellationToken:cancellationToken + user:user]; + } + + NSDictionary *relationCondition = queryState.conditions[@"$relatedTo"]; + if (relationCondition) { + PFObject *object = relationCondition[@"object"]; + NSString *key = relationCondition[@"key"]; + if ([object isDataAvailableForKey:key]) { + PFRelation *relation = object[key]; + return [self _findObjectsAsyncInRelation:relation + ofObject:object + forQueryState:queryState + withCancellationToken:cancellationToken + user:user]; + } + } + + return [super findObjectsAsyncForQueryState:queryState withCancellationToken:cancellationToken user:user]; +} + +- (BFTask *)_findObjectsAsyncInRelation:(PFRelation *)relation + ofObject:(PFObject *)parentObject + forQueryState:(PFQueryState *)queryState + withCancellationToken:(BFCancellationToken *)cancellationToken + user:(PFUser *)user { + return [[super findObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationToken + user:user] continueWithSuccessBlock:^id(BFTask *fetchTask) { + + NSArray *objects = fetchTask.result; + for (PFObject *object in objects) { + [relation _addKnownObject:object]; + } + + return [[_offlineStore updateDataForObjectAsync:parentObject] continueWithBlock:^id(BFTask *task) { + // Roll-forward the result of find task instead of a result of update task. + return fetchTask; + } cancellationToken:cancellationToken]; + } cancellationToken:cancellationToken]; +} + + +- (BFTask *)_findObjectsFromLocalDatastoreAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(BFCancellationToken *)cancellationToken + user:(PFUser *)user { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + NSString *pinName = queryState.localDatastorePinName; + if (pinName) { + PFPinningObjectStore *objectStore = self.coreDataSource.pinningObjectStore; + return [objectStore fetchPinAsyncWithName:pinName]; + } + return nil; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFPin *pin = task.result; + return [_offlineStore findAsyncForQueryState:queryState user:user pin:pin]; + } cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - Count +///-------------------------------------- + +- (BFTask *)countObjectsAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(BFCancellationToken *)cancellationToken + user:(PFUser *)user { + if (queryState.queriesLocalDatastore) { + return [self _countObjectsFromLocalDatastoreAsyncForQueryState:queryState + withCancellationToken:cancellationToken + user:user]; + } + return [super countObjectsAsyncForQueryState:queryState withCancellationToken:cancellationToken user:user]; +} + +- (BFTask *)_countObjectsFromLocalDatastoreAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(BFCancellationToken *)cancellationToken + user:(PFUser *)user { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + NSString *pinName = queryState.localDatastorePinName; + if (pinName) { + PFPinningObjectStore *controller = self.coreDataSource.pinningObjectStore; + return [controller fetchPinAsyncWithName:pinName]; + } + return nil; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFPin *pin = task.result; + return [_offlineStore countAsyncForQueryState:queryState user:user pin:pin]; + } cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - PFQueryControllerSubclass +///-------------------------------------- + +- (BFTask *)runNetworkCommandAsync:(PFRESTCommand *)command + withCancellationToken:(BFCancellationToken *)cancellationToken + forQueryState:(PFQueryState *)queryState { + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed + cancellationToken:cancellationToken]; +} + +@end diff --git a/Parse/Internal/Query/Controller/PFQueryController.h b/Parse/Internal/Query/Controller/PFQueryController.h new file mode 100644 index 000000000..133b7332a --- /dev/null +++ b/Parse/Internal/Query/Controller/PFQueryController.h @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" + +@class BFCancellationToken; +@class BFTask; +@class PFQueryState; +@class PFRESTCommand; +@class PFUser; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFQueryController : NSObject + +@property (nonatomic, weak, readonly) id commonDataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCommonDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithCommonDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Find +///-------------------------------------- + +/*! + Finds objects from network or LDS for any given query state. + Supports cancellation and ACLed changes for a specific user. + + @param queryState Query state to use. + @param cancellationToken Cancellation token or `nil`. + @param user `user` to use for ACLs or `nil`. + + @returns Task that resolves to `NSArray` of `PFObject`s. + */ +- (BFTask *)findObjectsAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + user:(nullable PFUser *)user; // TODO: (nlutsenko) Pass `PFUserState` instead of user. + +///-------------------------------------- +/// @name Count +///-------------------------------------- + +/*! + Counts objects from network or LDS for any given query state. + Supports cancellation and ACLed changes for a specific user. + + @param queryState Query state to use. + @param cancellationToken Cancellation token or `nil`. + @param user `user` to use for ACLs or `nil`. + + @returns Task that resolves to `NSNumber` with a count of results. + */ +- (BFTask *)countObjectsAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + user:(nullable PFUser *)user; // TODO: (nlutsenko) Pass `PFUserState` instead of user. + +///-------------------------------------- +/// @name Caching +///-------------------------------------- + +- (NSString *)cacheKeyForQueryState:(PFQueryState *)queryState sessionToken:(nullable NSString *)sessionToken; +- (BOOL)hasCachedResultForQueryState:(PFQueryState *)queryState sessionToken:(nullable NSString *)sessionToken; + +- (void)clearCachedResultForQueryState:(PFQueryState *)queryState sessionToken:(nullable NSString *)sessionToken; +- (void)clearAllCachedResults; + +@end + +@protocol PFQueryControllerSubclass + +/*! + Implementation should run a command on a network runner. + + @param command Command to run. + @param cancellationToken Cancellation token. + @param queryState Query state to run command for. + + @returns `BFTask` instance with result of `PFCommandResult`. + */ +- (BFTask *)runNetworkCommandAsync:(PFRESTCommand *)command + withCancellationToken:(nullable BFCancellationToken *)cancellationToken + forQueryState:(PFQueryState *)queryState; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Query/Controller/PFQueryController.m b/Parse/Internal/Query/Controller/PFQueryController.m new file mode 100644 index 000000000..9b6c4940c --- /dev/null +++ b/Parse/Internal/Query/Controller/PFQueryController.m @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFQueryController.h" + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFPin.h" +#import "PFQueryState.h" +#import "PFRESTQueryCommand.h" +#import "PFUser.h" +#import "Parse_Private.h" + +@interface PFQueryController () + +@end + +@implementation PFQueryController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithCommonDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _commonDataSource = dataSource; + + return self; +} + ++ (instancetype)controllerWithCommonDataSource:(id)dataSource { + return [[self alloc] initWithCommonDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Find +///-------------------------------------- + +- (BFTask *)findObjectsAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(BFCancellationToken *)cancellationToken + user:(PFUser *)user { + NSDate *queryStart = (queryState.trace ? [NSDate date] : nil); + __block NSDate *querySent = nil; + + NSString *sessionToken = user.sessionToken; + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + PFRESTCommand *command = [PFRESTQueryCommand findCommandForQueryState:queryState withSessionToken:sessionToken]; + querySent = (queryState.trace ? [NSDate date] : nil); + return [self runNetworkCommandAsync:command + withCancellationToken:cancellationToken + forQueryState:queryState]; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + NSDate *queryReceived = (queryState.trace ? [NSDate date] : nil); + + NSArray *resultObjects = result.result[@"results"]; + NSMutableArray *foundObjects = [NSMutableArray arrayWithCapacity:resultObjects.count]; + if (resultObjects != nil) { + NSString *resultClassName = result.result[@"className"]; + if (!resultClassName) { + resultClassName = queryState.parseClassName; + } + NSArray *selectedKeys = queryState.selectedKeys.allObjects; + for (NSDictionary *resultObject in resultObjects) { + PFObject *object = [PFObject _objectFromDictionary:resultObject + defaultClassName:resultClassName + selectedKeys:selectedKeys]; + [foundObjects addObject:object]; + } + } + + NSString *traceLog = [result.result objectForKey:@"trace"]; + if (traceLog != nil) { + NSLog(@"Pre-processing took %f seconds\n%@Client side parsing took %f seconds", + [querySent timeIntervalSinceDate:queryStart], traceLog, + [queryReceived timeIntervalSinceNow]); + } + + return foundObjects; + } cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - Count +///-------------------------------------- + +- (BFTask *)countObjectsAsyncForQueryState:(PFQueryState *)queryState + withCancellationToken:(BFCancellationToken *)cancellationToken + user:(PFUser *)user { + NSString *sessionToken = user.sessionToken; + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + PFRESTQueryCommand *findCommand = [PFRESTQueryCommand findCommandForQueryState:queryState + withSessionToken:sessionToken]; + PFRESTCommand *countCommand = [PFRESTQueryCommand countCommandFromFindCommand:findCommand]; + return [self runNetworkCommandAsync:countCommand + withCancellationToken:cancellationToken + forQueryState:queryState]; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + return result.result[@"count"]; + } cancellationToken:cancellationToken]; +} + +///-------------------------------------- +#pragma mark - Caching +///-------------------------------------- + +- (NSString *)cacheKeyForQueryState:(PFQueryState *)queryState sessionToken:(NSString *)sessionToken { + return nil; +} + +- (BOOL)hasCachedResultForQueryState:(PFQueryState *)queryState sessionToken:(NSString *)sessionToken { + return NO; +} + +- (void)clearCachedResultForQueryState:(PFQueryState *)queryState sessionToken:(NSString *)sessionToken { +} + +- (void)clearAllCachedResults { +} + +///-------------------------------------- +#pragma mark - PFQueryControllerSubclass +///-------------------------------------- + +- (BFTask *)runNetworkCommandAsync:(PFRESTCommand *)command + withCancellationToken:(BFCancellationToken *)cancellationToken + forQueryState:(PFQueryState *)queryState { + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed + cancellationToken:cancellationToken]; +} + +@end diff --git a/Parse/Internal/Query/PFQueryPrivate.h b/Parse/Internal/Query/PFQueryPrivate.h new file mode 100644 index 000000000..b7cbdffe7 --- /dev/null +++ b/Parse/Internal/Query/PFQueryPrivate.h @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +#import "PFQueryState.h" + +extern NSString *const PFQueryKeyNotEqualTo; +extern NSString *const PFQueryKeyLessThan; +extern NSString *const PFQueryKeyLessThanEqualTo; +extern NSString *const PFQueryKeyGreaterThan; +extern NSString *const PFQueryKeyGreaterThanOrEqualTo; +extern NSString *const PFQueryKeyContainedIn; +extern NSString *const PFQueryKeyNotContainedIn; +extern NSString *const PFQueryKeyContainsAll; +extern NSString *const PFQueryKeyNearSphere; +extern NSString *const PFQueryKeyWithin; +extern NSString *const PFQueryKeyRegex; +extern NSString *const PFQueryKeyExists; +extern NSString *const PFQueryKeyInQuery; +extern NSString *const PFQueryKeyNotInQuery; +extern NSString *const PFQueryKeySelect; +extern NSString *const PFQueryKeyDontSelect; +extern NSString *const PFQueryKeyRelatedTo; +extern NSString *const PFQueryKeyOr; +extern NSString *const PFQueryKeyQuery; +extern NSString *const PFQueryKeyKey; +extern NSString *const PFQueryKeyObject; + +extern NSString *const PFQueryOptionKeyMaxDistance; +extern NSString *const PFQueryOptionKeyBox; +extern NSString *const PFQueryOptionKeyRegexOptions; + +@class BFTask; +@class PFObject; + +@interface PFQuery () + +@property (nonatomic, strong, readonly) PFQueryState *state; + +@end + +@interface PFQuery (Private) + +- (instancetype)whereRelatedToObject:(PFObject *)parent fromKey:(NSString *)key; +- (void)redirectClassNameForKey:(NSString *)key; + +@end diff --git a/Parse/Internal/Query/State/PFMutableQueryState.h b/Parse/Internal/Query/State/PFMutableQueryState.h new file mode 100644 index 000000000..e54a13c44 --- /dev/null +++ b/Parse/Internal/Query/State/PFMutableQueryState.h @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFQueryState.h" + +@interface PFMutableQueryState : PFQueryState + +@property (nonatomic, copy, readwrite) NSString *parseClassName; + +@property (nonatomic, assign, readwrite) NSInteger limit; +@property (nonatomic, assign, readwrite) NSInteger skip; + +///-------------------------------------- +/// @name Remote + Caching Options +///-------------------------------------- + +@property (nonatomic, assign, readwrite) PFCachePolicy cachePolicy; +@property (nonatomic, assign, readwrite) NSTimeInterval maxCacheAge; + +@property (nonatomic, assign, readwrite) BOOL trace; + +///-------------------------------------- +/// @name Local Datastore Options +///-------------------------------------- + +@property (nonatomic, assign, readwrite) BOOL shouldIgnoreACLs; +@property (nonatomic, assign, readwrite) BOOL shouldIncludeDeletingEventually; +@property (nonatomic, assign, readwrite) BOOL queriesLocalDatastore; +@property (nonatomic, copy, readwrite) NSString *localDatastorePinName; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithParseClassName:(NSString *)className; ++ (instancetype)stateWithParseClassName:(NSString *)className; + +///-------------------------------------- +/// @name Conditions +///-------------------------------------- + +- (void)setConditionType:(NSString *)type withObject:(id)object forKey:(NSString *)key; + +- (void)setEqualityConditionWithObject:(id)object forKey:(NSString *)key; +- (void)setRelationConditionWithObject:(id)object forKey:(NSString *)key; + +- (void)removeAllConditions; + +///-------------------------------------- +/// @name Sort +///-------------------------------------- + +- (void)sortByKey:(NSString *)key ascending:(BOOL)ascending; +- (void)addSortKey:(NSString *)key ascending:(BOOL)ascending; +- (void)addSortKeysFromSortDescriptors:(NSArray *)sortDescriptors; + +///-------------------------------------- +/// @name Includes +///-------------------------------------- + +- (void)includeKey:(NSString *)key; + +///-------------------------------------- +/// @name Selected Keys +///-------------------------------------- + +- (void)selectKeys:(NSArray *)keys; + +///-------------------------------------- +/// @name Redirect +///-------------------------------------- + +- (void)redirectClassNameForKey:(NSString *)key; + +@end diff --git a/Parse/Internal/Query/State/PFMutableQueryState.m b/Parse/Internal/Query/State/PFMutableQueryState.m new file mode 100644 index 000000000..3ac9be934 --- /dev/null +++ b/Parse/Internal/Query/State/PFMutableQueryState.m @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableQueryState.h" + +#import "PFQueryState_Private.h" + +@interface PFMutableQueryState () { + NSMutableDictionary *_conditions; + NSMutableArray *_sortKeys; + NSMutableSet *_includedKeys; + NSMutableDictionary *_extraOptions; +} + +@end + +@implementation PFMutableQueryState + +@synthesize conditions = _conditions; +@synthesize sortKeys = _sortKeys; +@synthesize includedKeys = _includedKeys; +@synthesize extraOptions = _extraOptions; + +@dynamic parseClassName; +@dynamic selectedKeys; +@dynamic limit; +@dynamic skip; +@dynamic cachePolicy; +@dynamic maxCacheAge; +@dynamic trace; +@dynamic shouldIgnoreACLs; +@dynamic shouldIncludeDeletingEventually; +@dynamic queriesLocalDatastore; +@dynamic localDatastorePinName; + +///-------------------------------------- +#pragma mark - Property Attributes +///-------------------------------------- + ++ (NSDictionary *)propertyAttributes { + NSMutableDictionary *attributes = [[super propertyAttributes] mutableCopy]; + + attributes[@"conditions"] = [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeMutableCopy]; + attributes[@"sortKeys"] = [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeMutableCopy]; + attributes[@"includedKeys"] = [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeMutableCopy]; + attributes[@"extraOptions"] = [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeMutableCopy]; + + return attributes; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithParseClassName:(NSString *)className { + self = [self init]; + if (!self) return nil; + + _parseClassName = [className copy]; + + return self; +} + ++ (instancetype)stateWithParseClassName:(NSString *)className { + return [[self alloc] initWithParseClassName:className]; +} + +///-------------------------------------- +#pragma mark - Conditions +///-------------------------------------- + +- (void)setConditionType:(NSString *)type withObject:(id)object forKey:(NSString *)key { + NSMutableDictionary *conditionObject = nil; + + // Check if we already have some sort of condition + id existingCondition = _conditions[key]; + if ([existingCondition isKindOfClass:[NSMutableDictionary class]]) { + conditionObject = existingCondition; + } + if (!conditionObject) { + conditionObject = [NSMutableDictionary dictionary]; + } + conditionObject[type] = object; + + [self setEqualityConditionWithObject:conditionObject forKey:key]; +} + +- (void)setEqualityConditionWithObject:(id)object forKey:(NSString *)key { + if (!_conditions) { + _conditions = [NSMutableDictionary dictionary]; + } + _conditions[key] = object; +} + +- (void)setRelationConditionWithObject:(id)object forKey:(NSString *)key { + // We need to force saved PFObject here. + NSMutableDictionary *condition = [NSMutableDictionary dictionaryWithCapacity:2]; + condition[@"object"] = object; + condition[@"key"] = key; + [self setEqualityConditionWithObject:condition forKey:@"$relatedTo"]; +} + +- (void)removeAllConditions { + [_conditions removeAllObjects]; +} + +///-------------------------------------- +#pragma mark - Sort +///-------------------------------------- + +- (void)sortByKey:(NSString *)key ascending:(BOOL)ascending { + [_sortKeys removeAllObjects]; + [self addSortKey:key ascending:ascending]; +} + +- (void)addSortKey:(NSString *)key ascending:(BOOL)ascending { + if (!key) { + return; + } + + NSString *sortKey = (ascending ? key : [NSString stringWithFormat:@"-%@", key]); + if (!_sortKeys) { + _sortKeys = [NSMutableArray arrayWithObject:sortKey]; + } else { + [_sortKeys addObject:sortKey]; + } +} + +- (void)addSortKeysFromSortDescriptors:(NSArray *)sortDescriptors { + [_sortKeys removeAllObjects]; + for (NSSortDescriptor *sortDescriptor in sortDescriptors) { + [self addSortKey:sortDescriptor.key ascending:sortDescriptor.ascending]; + } +} + +///-------------------------------------- +#pragma mark - Includes +///-------------------------------------- + +- (void)includeKey:(NSString *)key { + if (!_includedKeys) { + _includedKeys = [NSMutableSet setWithObject:key]; + } else { + [_includedKeys addObject:key]; + } +} + +///-------------------------------------- +#pragma mark - Selected Keys +///-------------------------------------- + +- (void)selectKeys:(NSArray *)keys { + if (keys) { + _selectedKeys = (_selectedKeys ? [_selectedKeys setByAddingObjectsFromArray:keys] : [NSSet setWithArray:keys]); + } else { + _selectedKeys = nil; + } +} + +///-------------------------------------- +#pragma mark - Redirect +///-------------------------------------- + +- (void)redirectClassNameForKey:(NSString *)key { + if (!_extraOptions) { + _extraOptions = [NSMutableDictionary dictionary]; + } + _extraOptions[@"redirectClassNameForKey"] = key; +} + +@end diff --git a/Parse/Internal/Query/State/PFQueryState.h b/Parse/Internal/Query/State/PFQueryState.h new file mode 100644 index 000000000..a66807bd2 --- /dev/null +++ b/Parse/Internal/Query/State/PFQueryState.h @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFBaseState.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@interface PFQueryState : PFBaseState + +@property (nonatomic, copy, readonly) NSString *parseClassName; + +@property (nonatomic, copy, readonly) NSDictionary *conditions; + +@property (nonatomic, copy, readonly) NSArray *sortKeys; +@property (nonatomic, copy, readonly) NSString *sortOrderString; + +@property (nonatomic, copy, readonly) NSSet *includedKeys; +@property (nonatomic, copy, readonly) NSSet *selectedKeys; +@property (nonatomic, copy, readonly) NSDictionary *extraOptions; + +@property (nonatomic, assign, readonly) NSInteger limit; +@property (nonatomic, assign, readonly) NSInteger skip; + +///-------------------------------------- +/// @name Remote + Caching Options +///-------------------------------------- + +@property (nonatomic, assign, readonly) PFCachePolicy cachePolicy; +@property (nonatomic, assign, readonly) NSTimeInterval maxCacheAge; + +@property (nonatomic, assign, readonly) BOOL trace; + +///-------------------------------------- +/// @name Local Datastore Options +///-------------------------------------- + +/*! + If ignoreACLs is enabled, we don't check ACLs when querying from LDS. We also don't grab + `PFUser currentUser` since it's unnecessary when ignoring ACLs. + */ +@property (nonatomic, assign, readonly) BOOL shouldIgnoreACLs; +/*! + This is currently unused, but is here to allow future querying across objects that are in the + process of being deleted eventually. + */ +@property (nonatomic, assign, readonly) BOOL shouldIncludeDeletingEventually; +@property (nonatomic, assign, readonly) BOOL queriesLocalDatastore; +@property (nonatomic, copy, readonly) NSString *localDatastorePinName; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithState:(PFQueryState *)state; ++ (instancetype)stateWithState:(PFQueryState *)state; + +@end diff --git a/Parse/Internal/Query/State/PFQueryState.m b/Parse/Internal/Query/State/PFQueryState.m new file mode 100644 index 000000000..d5da2582e --- /dev/null +++ b/Parse/Internal/Query/State/PFQueryState.m @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFQueryState.h" +#import "PFQueryState_Private.h" + +#import "PFMutableQueryState.h" +#import "PFPropertyInfo.h" + +@implementation PFQueryState + +///-------------------------------------- +#pragma mark - PFBaseStateSubclass +///-------------------------------------- + ++ (NSDictionary *)propertyAttributes { + return @{ + @"parseClassName": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"conditions": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"sortKeys": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"includedKeys": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"selectedKeys": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"extraOptions": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + + @"limit": [PFPropertyAttributes attributes], + @"skip": [PFPropertyAttributes attributes], + @"cachePolicy": [PFPropertyAttributes attributes], + @"maxCacheAge": [PFPropertyAttributes attributes], + + @"trace": [PFPropertyAttributes attributes], + @"shouldIgnoreACLs": [PFPropertyAttributes attributes], + @"shouldIncludeDeletingEventually": [PFPropertyAttributes attributes], + @"queriesLocalDatastore": [PFPropertyAttributes attributes], + + @"localDatastorePinName": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy] + }; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _cachePolicy = kPFCachePolicyIgnoreCache; + _maxCacheAge = INFINITY; + _limit = -1; + + return self; +} + +- (instancetype)initWithState:(PFQueryState *)state { + return [super initWithState:state]; +} + ++ (instancetype)stateWithState:(PFQueryState *)state { + return [super stateWithState:state]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (NSString *)sortOrderString { + return [self.sortKeys componentsJoinedByString:@","]; +} + +///-------------------------------------- +#pragma mark - Mutable Copying +///-------------------------------------- + +- (id)copyWithZone:(NSZone *)zone { + return [[PFQueryState allocWithZone:zone] initWithState:self]; +} + +- (instancetype)mutableCopyWithZone:(NSZone *)zone { + return [[PFMutableQueryState allocWithZone:zone] initWithState:self]; +} + +@end diff --git a/Parse/Internal/Query/State/PFQueryState_Private.h b/Parse/Internal/Query/State/PFQueryState_Private.h new file mode 100644 index 000000000..006d3c230 --- /dev/null +++ b/Parse/Internal/Query/State/PFQueryState_Private.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFQueryState.h" + +@interface PFQueryState () { +@protected + NSString *_parseClassName; + + NSDictionary *_conditions; + + NSArray *_sortKeys; + + NSSet *_includedKeys; + NSSet *_selectedKeys; + NSDictionary *_extraOptions; + + NSInteger _limit; + NSInteger _skip; + + PFCachePolicy _cachePolicy; + NSTimeInterval _maxCacheAge; + + BOOL _trace; + + BOOL _shouldIgnoreACLs; + BOOL _shouldIncludeDeletingEventually; + BOOL _queriesLocalDatastore; + NSString *_localDatastorePinName; +} + +@property (nonatomic, copy, readwrite) NSString *parseClassName; + +@property (nonatomic, assign, readwrite) NSInteger limit; +@property (nonatomic, assign, readwrite) NSInteger skip; + +///-------------------------------------- +/// @name Remote + Caching Options +///-------------------------------------- + +@property (nonatomic, assign, readwrite) PFCachePolicy cachePolicy; +@property (nonatomic, assign, readwrite) NSTimeInterval maxCacheAge; + +@property (nonatomic, assign, readwrite) BOOL trace; + +///-------------------------------------- +/// @name Local Datastore Options +///-------------------------------------- + +@property (nonatomic, assign, readwrite) BOOL shouldIgnoreACLs; +@property (nonatomic, assign, readwrite) BOOL shouldIncludeDeletingEventually; +@property (nonatomic, assign, readwrite) BOOL queriesLocalDatastore; +@property (nonatomic, copy, readwrite) NSString *localDatastorePinName; + +@end diff --git a/Parse/Internal/Query/Utilities/PFQueryUtilities.h b/Parse/Internal/Query/Utilities/PFQueryUtilities.h new file mode 100644 index 000000000..59276df94 --- /dev/null +++ b/Parse/Internal/Query/Utilities/PFQueryUtilities.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFQueryUtilities : NSObject + +///-------------------------------------- +/// @name Predicate +///-------------------------------------- + +/*! + Takes an arbitrary predicate and normalizes it to a form that can easily be converted to a `PFQuery`. + */ ++ (NSPredicate *)predicateByNormalizingPredicate:(NSPredicate *)predicate; + +///-------------------------------------- +/// @name Regex +///-------------------------------------- + +/*! + Converts a string into a regex that matches it. + + @param string String to convert from. + + @returns Query regex string from a string. + */ ++ (NSString *)regexStringForString:(NSString *)string; + +///-------------------------------------- +/// @name Errors +///-------------------------------------- + ++ (NSError *)objectNotFoundError; + +@end diff --git a/Parse/Internal/Query/Utilities/PFQueryUtilities.m b/Parse/Internal/Query/Utilities/PFQueryUtilities.m new file mode 100644 index 000000000..2189e171d --- /dev/null +++ b/Parse/Internal/Query/Utilities/PFQueryUtilities.m @@ -0,0 +1,536 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFQueryUtilities.h" + +#import "PFConstants.h" +#import "PFErrorUtilities.h" + +@implementation PFQueryUtilities + +///-------------------------------------- +#pragma mark - Predicate +///-------------------------------------- + ++ (NSPredicate *)predicateByNormalizingPredicate:(NSPredicate *)predicate { + return [self _hoistCommonPredicates:[self _normalizeToDNF:predicate]]; +} + +/*! + Traverses over all of the subpredicates in the given predicate, calling the given blocks to + transform any instances of NSPredicate. + */ ++ (NSPredicate *)_mapPredicate:(NSPredicate *)predicate + compoundBlock:(NSPredicate *(^)(NSCompoundPredicate *))compoundBlock + comparisonBlock:(NSPredicate *(^)(NSComparisonPredicate *predicate))comparisonBlock { + if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { + if (compoundBlock) { + return compoundBlock((NSCompoundPredicate *)predicate); + } else { + NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; + + NSMutableArray *newSubpredicates = [NSMutableArray arrayWithCapacity:compound.subpredicates.count]; + for (NSPredicate *subPredicate in compound.subpredicates) { + [newSubpredicates addObject:[self _mapPredicate:subPredicate + compoundBlock:compoundBlock + comparisonBlock:comparisonBlock]]; + } + + NSCompoundPredicateType type = compound.compoundPredicateType; + return [[NSCompoundPredicate alloc] initWithType:type subpredicates:newSubpredicates]; + } + } + + if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { + if (comparisonBlock) { + return comparisonBlock((NSComparisonPredicate *)predicate); + } else { + return predicate; + } + } + + [NSException raise:NSInternalInconsistencyException format:@"NSExpression predicates are not supported."]; + return nil; +} + +/*! + Returns a predicate that is the negation of the input predicate, or throws on error. + */ ++ (NSPredicate *)_negatePredicate:(NSPredicate *)predicate { + return [self _mapPredicate:predicate + compoundBlock:^NSPredicate *(NSCompoundPredicate *compound) { + switch (compound.compoundPredicateType) { + case NSNotPredicateType: { + return [compound.subpredicates objectAtIndex:0]; + } + case NSAndPredicateType: { + NSMutableArray *newSubpredicates = + [NSMutableArray arrayWithCapacity:compound.subpredicates.count]; + for (NSPredicate *subpredicate in compound.subpredicates) { + [newSubpredicates addObject:[self _negatePredicate:subpredicate]]; + } + return [NSCompoundPredicate orPredicateWithSubpredicates:newSubpredicates]; + } + case NSOrPredicateType: { + NSMutableArray *newSubpredicates = + [NSMutableArray arrayWithCapacity:compound.subpredicates.count]; + for (NSPredicate *subpredicate in compound.subpredicates) { + [newSubpredicates addObject:[self _negatePredicate:subpredicate]]; + } + return [NSCompoundPredicate andPredicateWithSubpredicates:newSubpredicates]; + } + default: { + [NSException raise:NSInternalInconsistencyException + format:@"This compound predicate cannot be negated. (%zd)", + compound.compoundPredicateType]; + return nil; + } + } + } comparisonBlock:^NSPredicate *(NSComparisonPredicate *comparison) { + NSPredicateOperatorType newType; + NSComparisonPredicateModifier newModifier = comparison.comparisonPredicateModifier; + SEL customSelector; + + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: { + newType = NSNotEqualToPredicateOperatorType; + break; + } + case NSNotEqualToPredicateOperatorType: { + newType = NSEqualToPredicateOperatorType; + break; + } + case NSInPredicateOperatorType: { + newType = NSCustomSelectorPredicateOperatorType; + customSelector = NSSelectorFromString(@"notContainedIn:"); + break; + } + case NSLessThanPredicateOperatorType: { + newType = NSGreaterThanOrEqualToPredicateOperatorType; + break; + } + case NSLessThanOrEqualToPredicateOperatorType: { + newType = NSGreaterThanPredicateOperatorType; + break; + } + case NSGreaterThanPredicateOperatorType: { + newType = NSLessThanOrEqualToPredicateOperatorType; + break; + } + case NSGreaterThanOrEqualToPredicateOperatorType: { + newType = NSLessThanPredicateOperatorType; + break; + } + case NSBetweenPredicateOperatorType: { + [NSException raise:NSInternalInconsistencyException + format:@"A BETWEEN predicate was found after they should have been removed."]; + } + case NSMatchesPredicateOperatorType: + case NSLikePredicateOperatorType: + case NSBeginsWithPredicateOperatorType: + case NSEndsWithPredicateOperatorType: + case NSContainsPredicateOperatorType: + case NSCustomSelectorPredicateOperatorType: + default: { + [NSException raise:NSInternalInconsistencyException + format:@"This comparison predicate cannot be negated. (%@)", comparison]; + return nil; + } + } + + if (newType == NSCustomSelectorPredicateOperatorType) { + return [NSComparisonPredicate predicateWithLeftExpression:comparison.leftExpression + rightExpression:comparison.rightExpression + customSelector:customSelector]; + } else { + return [NSComparisonPredicate predicateWithLeftExpression:comparison.leftExpression + rightExpression:comparison.rightExpression + modifier:newModifier + type:newType + options:comparison.options]; + } + }]; +} + +/*! + Returns a version of the given predicate that contains no NSNotPredicateType compound predicates. + This greatly simplifies the diversity of predicates we have to handle later in the pipeline. + */ ++ (NSPredicate *)removeNegation:(NSPredicate *)predicate { + return [self _mapPredicate:predicate + compoundBlock:^NSPredicate *(NSCompoundPredicate *compound) { + // Remove negation from any subpredicates. + NSMutableArray *newSubpredicates = + [NSMutableArray arrayWithCapacity:compound.subpredicates.count]; + for (NSPredicate *subPredicate in [compound subpredicates]) { + [newSubpredicates addObject:[self removeNegation:subPredicate]]; + } + + // If this is a NOT predicate, return the negation of the subpredicate. + // Otherwise, just pass it on. + if (compound.compoundPredicateType == NSNotPredicateType) { + return [self _negatePredicate:[newSubpredicates objectAtIndex:0]]; + } else { + return [[NSCompoundPredicate alloc] initWithType:compound.compoundPredicateType + subpredicates:newSubpredicates]; + } + } comparisonBlock:nil]; +} + +/*! + Returns a version of the given predicate that contains no NSBetweenPredicateOperatorType predicates. + (A BETWEEN {C, D}) gets converted to (A >= C AND A <= D). + */ ++ (NSPredicate *)removeBetween:(NSPredicate *)predicate { + return [self _mapPredicate:predicate + compoundBlock:nil + comparisonBlock:^NSPredicate *(NSComparisonPredicate *predicate) { + if ([predicate predicateOperatorType] == NSBetweenPredicateOperatorType) { + NSComparisonPredicate *between = (NSComparisonPredicate *)predicate; + NSExpression *rhs = between.rightExpression; + + if (rhs.expressionType != NSConstantValueExpressionType && + rhs.expressionType != NSAggregateExpressionType) { + [NSException raise:NSInternalInconsistencyException + format:@"The right-hand side of a BETWEEN operation must be a value or literal."]; + } + if (![rhs.constantValue isKindOfClass:[NSArray class]]) { + [NSException raise:NSInternalInconsistencyException + format:@"The right-hand side of a BETWEEN operation must be an array."]; + } + NSArray *array = rhs.constantValue; + if (array.count != 2) { + [NSException raise:NSInternalInconsistencyException + format:@"The right-hand side of a BETWEEN operation must have 2 items."]; + } + + id minValue = array[0]; + id maxValue = array[1]; + + NSExpression *minExpression = ([minValue isKindOfClass:[NSExpression class]] + ? minValue + : [NSExpression expressionForConstantValue:minValue]); + NSExpression *maxExpression = ([maxValue isKindOfClass:[NSExpression class]] + ? maxValue + : [NSExpression expressionForConstantValue:maxValue]); + + return [NSCompoundPredicate andPredicateWithSubpredicates: + @[ [NSComparisonPredicate predicateWithLeftExpression:between.leftExpression + rightExpression:minExpression + modifier:between.comparisonPredicateModifier + type:NSGreaterThanOrEqualToPredicateOperatorType + options:between.options], + [NSComparisonPredicate predicateWithLeftExpression:between.leftExpression + rightExpression:maxExpression + modifier:between.comparisonPredicateModifier + type:NSLessThanOrEqualToPredicateOperatorType + options:between.options] + ]]; + } + return predicate; + }]; +} + +/*! + Returns a version of the given predicate that contains no Yoda conditions. + A Yoda condition is one where there's a constant on the LHS, such as (3 <= X). + The predicate returned by this method will instead have (X >= 3). + */ ++ (NSPredicate *)reverseYodaConditions:(NSPredicate *)predicate { + return [self _mapPredicate:predicate + compoundBlock:nil + comparisonBlock:^NSPredicate *(NSComparisonPredicate *comparison) { + if (comparison.leftExpression.expressionType == NSConstantValueExpressionType && + comparison.rightExpression.expressionType == NSKeyPathExpressionType) { + // This is a Yoda condition. + NSPredicateOperatorType newType; + switch ([comparison predicateOperatorType]) { + case NSEqualToPredicateOperatorType: { + newType = NSEqualToPredicateOperatorType; + break; + } + case NSNotEqualToPredicateOperatorType: { + newType = NSNotEqualToPredicateOperatorType; + break; + } + case NSLessThanPredicateOperatorType: { + newType = NSGreaterThanPredicateOperatorType; + break; + } + case NSLessThanOrEqualToPredicateOperatorType: { + newType = NSGreaterThanOrEqualToPredicateOperatorType; + break; + } + case NSGreaterThanPredicateOperatorType: { + newType = NSLessThanPredicateOperatorType; + break; + } + case NSGreaterThanOrEqualToPredicateOperatorType: { + newType = NSLessThanOrEqualToPredicateOperatorType; + break; + } + case NSInPredicateOperatorType: { + // This is like "5 IN X" where X is an array. + // Mongo handles this with syntax like "X = 5". + newType = NSEqualToPredicateOperatorType; + break; + } + case NSContainsPredicateOperatorType: + case NSMatchesPredicateOperatorType: + case NSLikePredicateOperatorType: + case NSBeginsWithPredicateOperatorType: + case NSEndsWithPredicateOperatorType: + case NSCustomSelectorPredicateOperatorType: + case NSBetweenPredicateOperatorType: + default: { + // We don't know how to reverse this Yoda condition, but maybe that's okay. + return predicate; + } + } + return [NSComparisonPredicate predicateWithLeftExpression:comparison.rightExpression + rightExpression:comparison.leftExpression + modifier:comparison.comparisonPredicateModifier + type:newType + options:comparison.options]; + } + return comparison; + }]; +} + +/*! + Returns a version of the given predicate converted to disjunctive normal form (DNF). + Unlike normalizeToDNF:error:, this method only accepts compound predicates, and assumes that + removeNegation:error: has already been applied to the given predicate. + */ ++ (NSPredicate *)asOrOfAnds:(NSCompoundPredicate *)compound { + // Convert the sub-predicates to DNF. + NSMutableArray *dnfSubpredicates = [NSMutableArray arrayWithCapacity:compound.subpredicates.count]; + for (NSPredicate *subpredicate in compound.subpredicates) { + if ([subpredicate isKindOfClass:[NSCompoundPredicate class]]) { + [dnfSubpredicates addObject:[self asOrOfAnds:(NSCompoundPredicate *)subpredicate]]; + } else { + [dnfSubpredicates addObject:subpredicate]; + } + } + + if (compound.compoundPredicateType == NSOrPredicateType) { + // We just need to flatten any child ORs into this OR. + NSMutableArray *newSubpredicates = [NSMutableArray arrayWithCapacity:dnfSubpredicates.count]; + for (NSPredicate *subpredicate in dnfSubpredicates) { + if ([subpredicate isKindOfClass:[NSCompoundPredicate class]] && + ((NSCompoundPredicate *)subpredicate).compoundPredicateType == NSOrPredicateType) { + for (NSPredicate *grandchild in ((NSCompoundPredicate *)subpredicate).subpredicates) { + [newSubpredicates addObject:grandchild]; + } + } else { + [newSubpredicates addObject:subpredicate]; + } + } + // There's no reason to wrap a single predicate in an OR. + if (newSubpredicates.count == 1) { + return newSubpredicates.lastObject; + } + return [NSCompoundPredicate orPredicateWithSubpredicates:newSubpredicates]; + } + + if (compound.compoundPredicateType == NSAndPredicateType) { + // This is tough. We need to take the cross product of all the subpredicates. + NSMutableArray *disjunction = [NSMutableArray arrayWithObject:@[]]; + for (NSPredicate *subpredicate in dnfSubpredicates) { + NSMutableArray *newDisjunction = [NSMutableArray array]; + if ([subpredicate isKindOfClass:[NSCompoundPredicate class]]) { + NSCompoundPredicate *subcompound = (NSCompoundPredicate *)subpredicate; + if (subcompound.compoundPredicateType == NSOrPredicateType) { + // We have to add every item in the OR to every AND list we have. + for (NSArray *conjunction in disjunction) { + for (NSPredicate *grandchild in subcompound.subpredicates) { + [newDisjunction addObject:[conjunction arrayByAddingObject:grandchild]]; + } + } + + } else if (subcompound.compoundPredicateType == NSAndPredicateType) { + // Just add all these conditions to all the conjunctions in progress. + for (NSArray *conjunction in disjunction) { + NSArray *grandchildren = subcompound.subpredicates; + [newDisjunction addObject:[conjunction arrayByAddingObjectsFromArray:grandchildren]]; + } + + } else { + [NSException raise:NSInternalInconsistencyException + format:@"[PFQuery asOrOfAnds:] found a compound query that wasn't OR or AND."]; + } + } else { + // Just add this condition to all the conjunctions in progress. + for (NSArray *conjunction in disjunction) { + [newDisjunction addObject:[conjunction arrayByAddingObject:subpredicate]]; + } + } + disjunction = newDisjunction; + } + + // Now disjunction contains an OR of ANDs. We just need to convert it to NSPredicates. + NSMutableArray *andPredicates = [NSMutableArray arrayWithCapacity:disjunction.count]; + for (NSArray *conjunction in disjunction) { + if (conjunction.count > 0) { + if (conjunction.count == 1) { + [andPredicates addObject:conjunction.lastObject]; + } else { + [andPredicates addObject:[NSCompoundPredicate + andPredicateWithSubpredicates:conjunction]]; + } + } + } + if (andPredicates.count == 1) { + return andPredicates.lastObject; + } else { + return [NSCompoundPredicate orPredicateWithSubpredicates:andPredicates]; + } + } + + [NSException raise:NSInternalInconsistencyException + format:@"[PFQuery asOrOfAnds:] was passed a compound query that wasn't OR or AND."]; + + return nil; +} + +/*! + Throws an exception if any comparison predicate inside this predicate has any modifiers, such as ANY, EVERY, etc. + */ ++ (void)assertNoPredicateModifiers:(NSPredicate *)predicate { + [self _mapPredicate:predicate + compoundBlock:nil + comparisonBlock:^NSPredicate *(NSComparisonPredicate *comparison) { + if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) { + [NSException raise:NSInternalInconsistencyException + format:@"Unsupported comparison predicate modifier %zd.", + comparison.comparisonPredicateModifier]; + } + return comparison; + }]; +} + +/*! + Returns a version of the given predicate converted to disjunctive normal form (DNF), + known colloqially as an "or of ands", the only form of query that PFQuery accepts. + */ ++ (NSPredicate *)_normalizeToDNF:(NSPredicate *)predicate { + // Make sure they didn't use ANY, EVERY, etc. + [self assertNoPredicateModifiers:predicate]; + + // Change any BETWEEN operators to a conjunction. + predicate = [self removeBetween:predicate]; + + // Change any backwards (3 <= X) to the standardized (X >= 3). + predicate = [self reverseYodaConditions:predicate]; + + // Push any negation into the leaves. + predicate = [self removeNegation:predicate]; + + // Any comparison predicate is trivially DNF. + if (![predicate isKindOfClass:[NSCompoundPredicate class]]) { + return predicate; + } + + // It must be a compound predicate. Convert it to an OR of ANDs. + return [self asOrOfAnds:(NSCompoundPredicate *)predicate]; +} + +/*! + Takes a predicate like ((A AND B) OR (A AND C)) and rewrites it as the more efficient (A AND (B OR C)). + Assumes the input predicate is already in DNF. + // TODO: (nlutsenko): Move this logic into the server and remove it from here. + */ ++ (NSPredicate *)_hoistCommonPredicates:(NSPredicate *)predicate { + // This only makes sense for queries with a top-level OR. + if (!([predicate isKindOfClass:[NSCompoundPredicate class]] && + ((NSCompoundPredicate *)predicate).compoundPredicateType == NSOrPredicateType)) { + return predicate; + } + + // Find the set of predicates that are included in every branch of this OR. + NSArray *andPredicates = ((NSCompoundPredicate *)predicate).subpredicates; + NSMutableSet *common = nil; + for (NSPredicate *andPredicate in andPredicates) { + NSMutableSet *comparisonPredicates = nil; + if ([andPredicate isKindOfClass:[NSComparisonPredicate class]]) { + comparisonPredicates = [NSMutableSet setWithObject:andPredicate]; + } else { + comparisonPredicates = + [NSMutableSet setWithArray:((NSCompoundPredicate *)andPredicate).subpredicates]; + } + + if (!common) { + common = comparisonPredicates; + } else { + [common intersectSet:comparisonPredicates]; + } + } + + if (!common.count) { + return predicate; + } + + NSMutableArray *newAndPredicates = [NSMutableArray array]; + + // Okay, there were common sub-predicates. Hoist them up to this one. + for (NSPredicate *andPredicate in andPredicates) { + NSMutableSet *comparisonPredicates = nil; + if ([andPredicate isKindOfClass:[NSComparisonPredicate class]]) { + comparisonPredicates = [NSMutableSet setWithObject:andPredicate]; + } else { + comparisonPredicates = + [NSMutableSet setWithArray:((NSCompoundPredicate *)andPredicate).subpredicates]; + } + + for (NSPredicate *comparisonPredicate in common) { + [comparisonPredicates removeObject:comparisonPredicate]; + } + + if (comparisonPredicates.count == 0) { + // One of the OR predicates reduces to TRUE, so just return the hoisted part. + return [NSCompoundPredicate andPredicateWithSubpredicates:common.allObjects]; + } else if (comparisonPredicates.count == 1) { + [newAndPredicates addObject:comparisonPredicates.allObjects.lastObject]; + } else { + NSPredicate *newAndPredicate = + [NSCompoundPredicate andPredicateWithSubpredicates:comparisonPredicates.allObjects]; + [newAndPredicates addObject:newAndPredicate]; + } + } + + // Make an AND of the hoisted predicates and the OR of the modified subpredicates. + NSPredicate *newOrPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:newAndPredicates]; + NSArray *newPredicates = [@[ newOrPredicate ] arrayByAddingObjectsFromArray:common.allObjects]; + return [NSCompoundPredicate andPredicateWithSubpredicates:newPredicates]; +} + +///-------------------------------------- +#pragma mark - Regex +///-------------------------------------- + +/*! + This is used to create a regex string to match the input string. By using Q and E flags to match, we can do this + without requiring super expensive rewrites, but me must be careful to escape existing \E flags in the input string. + By replacing it with `\E\\E\Q`, the regex engine will end the old literal block, put in the user's `\E` string, and + Begin another literal block. + */ ++ (NSString *)regexStringForString:(NSString *)string { + return [NSString stringWithFormat:@"\\Q%@\\E", [string stringByReplacingOccurrencesOfString:@"\\E" + withString:@"\\E\\\\E\\Q"]]; +} + +///-------------------------------------- +#pragma mark - Errors +///-------------------------------------- + ++ (NSError *)objectNotFoundError { + return [PFErrorUtilities errorWithCode:kPFErrorObjectNotFound message:@"No results matched the query."]; +} + +@end diff --git a/Parse/Internal/Relation/PFRelationPrivate.h b/Parse/Internal/Relation/PFRelationPrivate.h new file mode 100644 index 000000000..1ce3f37f7 --- /dev/null +++ b/Parse/Internal/Relation/PFRelationPrivate.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@class PFDecoder; + +@interface PFRelation (Private) + ++ (PFRelation *)relationForObject:(PFObject *)parent forKey:(NSString *)key; ++ (PFRelation *)relationWithTargetClass:(NSString *)targetClass; ++ (PFRelation *)relationFromDictionary:(NSDictionary *)dictionary withDecoder:(PFDecoder *)decoder; +- (void)ensureParentIs:(PFObject *)someParent andKeyIs:(NSString *)someKey; +- (NSDictionary *)encodeIntoDictionary; +- (BOOL)_hasKnownObject:(PFObject *)object; +- (void)_addKnownObject:(PFObject *)object; +- (void)_removeKnownObject:(PFObject *)object; + +@end diff --git a/Parse/Internal/Relation/State/PFMutableRelationState.h b/Parse/Internal/Relation/State/PFMutableRelationState.h new file mode 100644 index 000000000..3fd99eddb --- /dev/null +++ b/Parse/Internal/Relation/State/PFMutableRelationState.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRelationState.h" + +@interface PFMutableRelationState : PFRelationState + +@property (nonatomic, weak, readwrite) PFObject *parent; +@property (nonatomic, copy, readwrite) NSString *targetClass; +@property (nonatomic, copy, readwrite) NSMutableSet *knownObjects; +@property (nonatomic, copy, readwrite) NSString *key; + +@end diff --git a/Parse/Internal/Relation/State/PFMutableRelationState.m b/Parse/Internal/Relation/State/PFMutableRelationState.m new file mode 100644 index 000000000..6a9227962 --- /dev/null +++ b/Parse/Internal/Relation/State/PFMutableRelationState.m @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableRelationState.h" + +#import "PFObject.h" +#import "PFRelationState_Private.h" + +@implementation PFMutableRelationState + +@dynamic parent; +@dynamic parentObjectId; +@dynamic parentClassName; +@dynamic targetClass; +@dynamic knownObjects; +@dynamic key; + +///-------------------------------------- +#pragma mark - PFBaseStateSubclass +///-------------------------------------- + ++ (NSDictionary *)propertyAttributes { + NSMutableDictionary *parentAttributes = [[super propertyAttributes] mutableCopy]; + + parentAttributes[@"knownObjects"] = [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeMutableCopy]; + + return parentAttributes; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _knownObjects = [[NSMutableSet alloc] init]; + + return self; +} + +///-------------------------------------- +#pragma mark - Properties +///-------------------------------------- + +- (void)setParent:(PFObject *)parent { + if (_parent != parent || ![self.parentClassName isEqualToString:parent.parseClassName] || + ![self.parentObjectId isEqualToString:parent.objectId]) { + _parent = parent; + _parentClassName = [[parent parseClassName] copy]; + _parentObjectId = [[parent objectId] copy]; + } +} + +@end diff --git a/Parse/Internal/Relation/State/PFRelationState.h b/Parse/Internal/Relation/State/PFRelationState.h new file mode 100644 index 000000000..3ce8e80c0 --- /dev/null +++ b/Parse/Internal/Relation/State/PFRelationState.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFBaseState.h" + +@class PFObject; + +@interface PFRelationState : PFBaseState + +@property (nonatomic, weak, readonly) PFObject *parent; +@property (nonatomic, copy, readonly) NSString *parentClassName; +@property (nonatomic, copy, readonly) NSString *parentObjectId; +@property (nonatomic, copy, readonly) NSString *targetClass; +@property (nonatomic, copy, readonly) NSSet *knownObjects; +@property (nonatomic, copy, readonly) NSString *key; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithState:(PFRelationState *)otherState; ++ (instancetype)stateWithState:(PFRelationState *)otherState; + +@end diff --git a/Parse/Internal/Relation/State/PFRelationState.m b/Parse/Internal/Relation/State/PFRelationState.m new file mode 100644 index 000000000..bdf36c65b --- /dev/null +++ b/Parse/Internal/Relation/State/PFRelationState.m @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRelationState.h" +#import "PFRelationState_Private.h" + +#import "PFMutableRelationState.h" + +@implementation PFRelationState + +///-------------------------------------- +#pragma mark - PFBaseStateSubclass +///-------------------------------------- + ++ (NSDictionary *)propertyAttributes { + return @{ + @"parent": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeWeak], + @"parentClassName": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"parentObjectId": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"targetClass": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"knownObjects": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"key": [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + }; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _knownObjects = [[NSSet alloc] init]; + + return self; +} + +- (instancetype)initWithState:(PFRelationState *)otherState { + return [super initWithState:otherState]; +} + ++ (instancetype)stateWithState:(PFRelationState *)otherState { + return [super stateWithState:otherState]; +} + +///-------------------------------------- +#pragma mark - Copying +///-------------------------------------- + +- (instancetype)copyWithZone:(NSZone *)zone { + return [[PFRelationState allocWithZone:zone] initWithState:self]; +} + +- (instancetype)mutableCopyWithZone:(NSZone *)zone { + return [[PFMutableRelationState allocWithZone:zone] initWithState:self]; +} + +@end diff --git a/Parse/Internal/Relation/State/PFRelationState_Private.h b/Parse/Internal/Relation/State/PFRelationState_Private.h new file mode 100644 index 000000000..bf13b9ed8 --- /dev/null +++ b/Parse/Internal/Relation/State/PFRelationState_Private.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRelationState.h" + +@interface PFRelationState() { +@protected + __weak PFObject *_parent; + NSString *_parentClassName; + NSString *_parentObjectId; + NSSet *_knownObjects; + NSString *_key; +} + +@property (nonatomic, weak, readwrite) PFObject *parent; +@property (nonatomic, copy, readwrite) NSString *parentClassName; +@property (nonatomic, copy, readwrite) NSString *parentObjectId; +@property (nonatomic, copy, readwrite) NSString *targetClass; +@property (nonatomic, copy, readwrite) NSSet *knownObjects; +@property (nonatomic, copy, readwrite) NSString *key; + +@end diff --git a/Parse/Internal/Session/Controller/PFSessionController.h b/Parse/Internal/Session/Controller/PFSessionController.h new file mode 100644 index 000000000..5b1477a25 --- /dev/null +++ b/Parse/Internal/Session/Controller/PFSessionController.h @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFDataProvider.h" + +@class BFTask; + +NS_ASSUME_NONNULL_BEGIN + +@interface PFSessionController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithDataSource:(id)dataSource; ++ (instancetype)controllerWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Current Session +///-------------------------------------- + +- (BFTask *)getCurrentSessionAsyncWithSessionToken:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Session/Controller/PFSessionController.m b/Parse/Internal/Session/Controller/PFSessionController.m new file mode 100644 index 000000000..7abe5354e --- /dev/null +++ b/Parse/Internal/Session/Controller/PFSessionController.m @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSessionController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFObjectPrivate.h" +#import "PFRESTSessionCommand.h" +#import "PFSession.h" + +@implementation PFSessionController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + + return self; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Current Session +///-------------------------------------- + +- (BFTask *)getCurrentSessionAsyncWithSessionToken:(NSString *)sessionToken { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [PFRESTSessionCommand getCurrentSessionCommandWithSessionToken:sessionToken]; + return [self.dataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + NSDictionary *dictionary = result.result; + PFSession *session = [PFSession _objectFromDictionary:dictionary + defaultClassName:[PFSession parseClassName] + completeData:YES]; + return session; + }]; +} + +@end diff --git a/Parse/Internal/Session/PFSession_Private.h b/Parse/Internal/Session/PFSession_Private.h new file mode 100644 index 000000000..40de5a92b --- /dev/null +++ b/Parse/Internal/Session/PFSession_Private.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@class PFSessionController; + +@interface PFSession () + +///-------------------------------------- +/// @name Session Controller +///-------------------------------------- + ++ (PFSessionController *)sessionController; + +@end diff --git a/Parse/Internal/Session/Utilities/PFSessionUtilities.h b/Parse/Internal/Session/Utilities/PFSessionUtilities.h new file mode 100644 index 000000000..806d7da48 --- /dev/null +++ b/Parse/Internal/Session/Utilities/PFSessionUtilities.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PFSessionUtilities : NSObject + +///-------------------------------------- +/// @name Session Token +///-------------------------------------- + ++ (BOOL)isSessionTokenRevocable:(nullable NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/Session/Utilities/PFSessionUtilities.m b/Parse/Internal/Session/Utilities/PFSessionUtilities.m new file mode 100644 index 000000000..4dff476f3 --- /dev/null +++ b/Parse/Internal/Session/Utilities/PFSessionUtilities.m @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSessionUtilities.h" + +@implementation PFSessionUtilities + +///-------------------------------------- +#pragma mark - Session Token +///-------------------------------------- + ++ (BOOL)isSessionTokenRevocable:(NSString *)sessionToken { + return (sessionToken && [sessionToken rangeOfString:@"r:"].location != NSNotFound); +} + +@end diff --git a/Parse/Internal/ThreadSafety/PFThreadsafety.h b/Parse/Internal/ThreadSafety/PFThreadsafety.h new file mode 100644 index 000000000..7ca2a64c0 --- /dev/null +++ b/Parse/Internal/ThreadSafety/PFThreadsafety.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +extern dispatch_queue_t PFThreadsafetyCreateQueueForObject(id object); +extern void PFThreadsafetySafeDispatchSync(dispatch_queue_t queue, dispatch_block_t block); diff --git a/Parse/Internal/ThreadSafety/PFThreadsafety.m b/Parse/Internal/ThreadSafety/PFThreadsafety.m new file mode 100644 index 000000000..e78f54744 --- /dev/null +++ b/Parse/Internal/ThreadSafety/PFThreadsafety.m @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFThreadsafety.h" + +static void *const PFThreadsafetyQueueIDKey = (void *)&PFThreadsafetyQueueIDKey; + +dispatch_queue_t PFThreadsafetyCreateQueueForObject(id object) { + NSString *label = [NSStringFromClass([object class]) stringByAppendingString:@".synchronizationQueue"]; + dispatch_queue_t queue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL); + + void *uuid = calloc(1, sizeof(uuid)); + dispatch_queue_set_specific(queue, PFThreadsafetyQueueIDKey, uuid, free); + + return queue; +} + +void PFThreadsafetySafeDispatchSync(dispatch_queue_t queue, dispatch_block_t block) { + void *uuidMine = dispatch_get_specific(PFThreadsafetyQueueIDKey); + void *uuidOther = dispatch_queue_get_specific(queue, PFThreadsafetyQueueIDKey); + + if (uuidMine == uuidOther) { + block(); + } else { + dispatch_sync(queue, block); + } +} diff --git a/Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.h b/Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.h new file mode 100644 index 000000000..136dd3d7c --- /dev/null +++ b/Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFAuthenticationProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@class BFTask; +@class PFUser; + +@interface PFUserAuthenticationController : NSObject + +///-------------------------------------- +/// @name Authentication Providers +///-------------------------------------- + +- (void)registerAuthenticationProvider:(id)provider; +- (void)unregisterAuthenticationProvider:(id)provider; + +- (id)authenticationProviderForAuthType:(NSString *)authType; + +///-------------------------------------- +/// @name Authentication +///-------------------------------------- + +- (BFTask *)authenticateAsyncWithProviderForAuthType:(NSString *)authType; +- (BFTask *)deauthenticateAsyncWithProviderForAuthType:(NSString *)authType; + +- (BOOL)restoreAuthenticationWithAuthData:(nullable NSDictionary *)authData + withProviderForAuthType:(NSString *)authType; + +///-------------------------------------- +/// @name Log In +///-------------------------------------- + +- (BFTask *)logInUserAsyncWithAuthType:(NSString *)authType; +- (BFTask *)logInUserAsyncWithAuthType:(NSString *)authType authData:(NSDictionary *)authData; + +///-------------------------------------- +/// @name Link +///-------------------------------------- + +- (BFTask *)linkUserAsync:(PFUser *)user withAuthType:(NSString *)authType; +- (BFTask *)linkUserAsync:(PFUser *)user withAuthType:(NSString *)authType authData:(NSDictionary *)authData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.m b/Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.m new file mode 100644 index 000000000..04db27f94 --- /dev/null +++ b/Parse/Internal/User/AuthenticationProviders/Controller/PFUserAuthenticationController.m @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUserAuthenticationController.h" + +#import + +#import "PFMacros.h" +#import "PFUserPrivate.h" + +@interface PFUserAuthenticationController () { + dispatch_queue_t _dataAccessQueue; + NSMutableDictionary *_authenticationProviders; +} + +@end + +@implementation PFUserAuthenticationController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _dataAccessQueue = dispatch_queue_create("com.parse.user.authenticationManager", DISPATCH_QUEUE_SERIAL); + _authenticationProviders = [NSMutableDictionary dictionary]; + + return self; +} + +///-------------------------------------- +#pragma mark - Authentication Providers +///-------------------------------------- + +- (void)registerAuthenticationProvider:(id)provider { + NSString *authType = [[provider class] authType]; + if (!authType) { + return; + } + dispatch_sync(_dataAccessQueue, ^{ + _authenticationProviders[authType] = provider; + }); + + // TODO: (nlutsenko) Decouple this further. + if (![authType isEqualToString:@"anonymous"]) { + [[PFUser currentUser] synchronizeAuthDataWithAuthType:authType]; + } +} + +- (void)unregisterAuthenticationProvider:(id)provider { + NSString *authType = [[provider class] authType]; + if (!authType) { + return; + } + dispatch_sync(_dataAccessQueue, ^{ + [_authenticationProviders removeObjectForKey:authType]; + }); +} + +- (id)authenticationProviderForAuthType:(NSString *)authType { + if (!authType) { + return nil; + } + + __block id provider = nil; + dispatch_sync(_dataAccessQueue, ^{ + provider = _authenticationProviders[authType]; + }); + return provider; +} + +///-------------------------------------- +#pragma mark - Authentication +///-------------------------------------- + +- (BFTask *)authenticateAsyncWithProviderForAuthType:(NSString *)authType { + id provider = [self authenticationProviderForAuthType:authType]; + return [provider authenticateAsync]; +} + +- (BFTask *)deauthenticateAsyncWithProviderForAuthType:(NSString *)authType { + id provider = [self authenticationProviderForAuthType:authType]; + if (provider) { + return [provider deauthenticateAsync]; + } + return [BFTask taskWithResult:nil]; +} + +- (BOOL)restoreAuthenticationWithAuthData:(NSDictionary *)authData withProviderForAuthType:(NSString *)authType { + id provider = [self authenticationProviderForAuthType:authType]; + if (!provider) { + return YES; + } + return [provider restoreAuthenticationWithAuthData:authData]; +} + +///-------------------------------------- +#pragma mark - Log In +///-------------------------------------- + +- (BFTask *)logInUserAsyncWithAuthType:(NSString *)authType { + @weakify(self); + return [[self authenticateAsyncWithProviderForAuthType:authType] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + return [self logInUserAsyncWithAuthType:authType authData:task.result]; + }]; +} + +- (BFTask *)logInUserAsyncWithAuthType:(NSString *)authType authData:(NSDictionary *)authData { + return [PFUser _logInWithAuthTypeInBackground:authType authData:authData]; +} + +///-------------------------------------- +#pragma mark - Link +///-------------------------------------- + +- (BFTask *)linkUserAsync:(PFUser *)user withAuthType:(NSString *)authType { + @weakify(self); + return [[self authenticateAsyncWithProviderForAuthType:authType] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + return [self linkUserAsync:user withAuthType:authType authData:task.result]; + }]; +} + +- (BFTask *)linkUserAsync:(PFUser *)user withAuthType:(NSString *)authType authData:(NSDictionary *)authData { + return [user _linkWithAuthTypeInBackground:authType authData:authData]; +} + +@end diff --git a/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.h b/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.h new file mode 100644 index 000000000..61a82efb2 --- /dev/null +++ b/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFAuthenticationProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFAnonymousAuthenticationProvider : NSObject + +/*! + Gets auth data with a fresh UUID. + */ +@property (nonatomic, copy, readonly) NSDictionary *authData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.m b/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.m new file mode 100644 index 000000000..a0c95d2ec --- /dev/null +++ b/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousAuthenticationProvider.m @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAnonymousAuthenticationProvider.h" + +#import + +@implementation PFAnonymousAuthenticationProvider + +///-------------------------------------- +#pragma mark - PFAuthenticationProvider +///-------------------------------------- + ++ (NSString *)authType { + return @"anonymous"; +} + +- (BFTask *)authenticateAsync { + return [BFTask taskWithResult:self.authData]; +} + +- (BFTask *)deauthenticateAsync { + return [BFTask taskWithResult:nil]; +} + +- (BOOL)restoreAuthenticationWithAuthData:(NSDictionary *)authData { + return YES; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (NSDictionary *)authData { + NSString *uuidString = [NSUUID UUID].UUIDString; + uuidString = [uuidString lowercaseString]; + return @{ @"id" : uuidString }; +} + +@end diff --git a/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousUtils_Private.h b/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousUtils_Private.h new file mode 100644 index 000000000..bf5ccdf0c --- /dev/null +++ b/Parse/Internal/User/AuthenticationProviders/Providers/Anonymous/PFAnonymousUtils_Private.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAnonymousUtils.h" + +@class PFAnonymousAuthenticationProvider; +@class PFUser; + +@interface PFAnonymousUtils (Private) + ++ (PFAnonymousAuthenticationProvider *)_authenticationProvider; + ++ (PFUser *)_lazyLogIn; + +@end diff --git a/Parse/Internal/User/AuthenticationProviders/Providers/PFAuthenticationProvider.h b/Parse/Internal/User/AuthenticationProviders/Providers/PFAuthenticationProvider.h new file mode 100644 index 000000000..95f9c2dbc --- /dev/null +++ b/Parse/Internal/User/AuthenticationProviders/Providers/PFAuthenticationProvider.h @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class BFTask; + +/*! + A common protocol for general Parse authentication providers. + This interface allows for additional providers (e.g. Facebook, Twitter, etc.) to be plugged in, + separating the service-specific authentication process from the common work + that needs to be done to actually link the service to a user. + */ +@protocol PFAuthenticationProvider + +/*! + Returns a unique identifier for this service. + This identifier must match the key in the authData hash for this provider's data on the server. + */ ++ (NSString *)authType; + +/*! + Invoked by a PFUser to authenticate with the service. This function should call back PFUser (using the supplied blocks) to notify it of success. + The NSDictionary passed to the success block should contain relevant authData (and should match the server's expectations of data to be used + for verifying identity on the server). + */ +- (BFTask *)authenticateAsync; + +/*! + Invoked by a PFUser upon logOut. Deauthenticate should be used to clear any state being kept by the provider that is associated with the logged-in user. + */ +- (BFTask *)deauthenticateAsync; + +/*! + Upon logging in (or restoring a PFUser from disk), authData is returned from the server, and the PFUser passes that data into this function, + allowin the authentication provider to set up its internal state appropriately (e.g. setting auth tokens and keys on a service's SDK so that the SDK + can be used immediately, without having to reauthorize). authData can be nil, in which case the user has been unlinked, and the service should clear its + internal state. Returning NO from this function indicates the authData was somehow invalid, and the user should be unlinked from the provider. + */ +- (BOOL)restoreAuthenticationWithAuthData:(NSDictionary *)authData; + +@end diff --git a/Parse/Internal/User/Coder/File/PFUserFileCodingLogic.h b/Parse/Internal/User/Coder/File/PFUserFileCodingLogic.h new file mode 100644 index 000000000..4ccbf772a --- /dev/null +++ b/Parse/Internal/User/Coder/File/PFUserFileCodingLogic.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectFileCodingLogic.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFUserFileCodingLogic : PFObjectFileCodingLogic + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/User/Coder/File/PFUserFileCodingLogic.m b/Parse/Internal/User/Coder/File/PFUserFileCodingLogic.m new file mode 100644 index 000000000..d459bc2ab --- /dev/null +++ b/Parse/Internal/User/Coder/File/PFUserFileCodingLogic.m @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUserFileCodingLogic.h" + +#import "PFDecoder.h" +#import "PFMutableUserState.h" +#import "PFObjectPrivate.h" +#import "PFUserConstants.h" +#import "PFUserPrivate.h" + +@interface PFUserFileCodingLogic () + +@end + +@implementation PFUserFileCodingLogic + +///-------------------------------------- +#pragma mark - Coding +///-------------------------------------- + +- (void)updateObject:(PFObject *)object fromDictionary:(NSDictionary *)dictionary usingDecoder:(PFDecoder *)decoder { + PFUser *user = (PFUser *)object; + + NSString *newSessionToken = dictionary[@"session_token"] ?: dictionary[PFUserSessionTokenRESTKey]; + if (newSessionToken) { + PFMutableUserState *state = [user._state mutableCopy]; + state.sessionToken = newSessionToken; + user._state = state; + } + + // Merge the linked service metadata + NSDictionary *newAuthData = dictionary[@"auth_data"] ?: dictionary[PFUserAuthDataRESTKey]; + newAuthData = [decoder decodeObject:newAuthData]; + if (newAuthData) { + [user.authData removeAllObjects]; + [user.linkedServiceNames removeAllObjects]; + [newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id linkData, BOOL *stop) { + if (linkData != [NSNull null]) { + user.authData[key] = linkData; + [user.linkedServiceNames addObject:key]; + [user synchronizeAuthDataWithAuthType:key]; + } else { + [user.authData removeObjectForKey:key]; + [user.linkedServiceNames removeObject:key]; + [user synchronizeAuthDataWithAuthType:key]; + } + }]; + } + + [super updateObject:user fromDictionary:dictionary usingDecoder:decoder]; +} + +@end diff --git a/Parse/Internal/User/Constants/PFUserConstants.h b/Parse/Internal/User/Constants/PFUserConstants.h new file mode 100644 index 000000000..f8da41e05 --- /dev/null +++ b/Parse/Internal/User/Constants/PFUserConstants.h @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +extern NSString *const PFUserUsernameRESTKey; +extern NSString *const PFUserPasswordRESTKey; +extern NSString *const PFUserSessionTokenRESTKey; +extern NSString *const PFUserAuthDataRESTKey; diff --git a/Parse/Internal/User/Constants/PFUserConstants.m b/Parse/Internal/User/Constants/PFUserConstants.m new file mode 100644 index 000000000..ecccaf14a --- /dev/null +++ b/Parse/Internal/User/Constants/PFUserConstants.m @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUserConstants.h" + +NSString *const PFUserUsernameRESTKey = @"username"; +NSString *const PFUserPasswordRESTKey = @"password"; +NSString *const PFUserSessionTokenRESTKey = @"sessionToken"; +NSString *const PFUserAuthDataRESTKey = @"authData"; diff --git a/Parse/Internal/User/Controller/PFUserController.h b/Parse/Internal/User/Controller/PFUserController.h new file mode 100644 index 000000000..4ff2208d5 --- /dev/null +++ b/Parse/Internal/User/Controller/PFUserController.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreDataProvider.h" +#import "PFDataProvider.h" +#import "PFObjectControlling.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFUserController : NSObject + +@property (nonatomic, weak, readonly) id commonDataSource; +@property (nonatomic, weak, readonly) id coreDataSource; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource; ++ (instancetype)controllerWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource; + +///-------------------------------------- +/// @name Log In +///-------------------------------------- + +- (BFTask *)logInCurrentUserAsyncWithSessionToken:(NSString *)sessionToken; +- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username + password:(NSString *)password + revocableSession:(BOOL)revocableSession; + +//TODO: (nlutsenko) Move this method into PFUserAuthenticationController after PFUser is decoupled further. +- (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType + authData:(NSDictionary *)authData + revocableSession:(BOOL)revocableSession; + +///-------------------------------------- +/// @name Reset Password +///-------------------------------------- + +- (BFTask *)requestPasswordResetAsyncForEmail:(NSString *)email; + +///-------------------------------------- +/// @name Log Out +///-------------------------------------- + +- (BFTask *)logOutUserAsyncWithSessionToken:(NSString *)sessionToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/User/Controller/PFUserController.m b/Parse/Internal/User/Controller/PFUserController.m new file mode 100644 index 000000000..cebeb52f4 --- /dev/null +++ b/Parse/Internal/User/Controller/PFUserController.m @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUserController.h" + +#import "BFTask+Private.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFCurrentUserController.h" +#import "PFErrorUtilities.h" +#import "PFMacros.h" +#import "PFObjectPrivate.h" +#import "PFRESTUserCommand.h" +#import "PFUserPrivate.h" + +@implementation PFUserController + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + self = [super init]; + if (!self) return nil; + + _commonDataSource = commonDataSource; + _coreDataSource = coreDataSource; + + return self; +} + ++ (instancetype)controllerWithCommonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + return [[self alloc] initWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; +} + +///-------------------------------------- +#pragma mark - Log In +///-------------------------------------- + +- (BFTask *)logInCurrentUserAsyncWithSessionToken:(NSString *)sessionToken { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [PFRESTUserCommand getCurrentUserCommandWithSessionToken:sessionToken]; + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFCommandResult *result = task.result; + NSDictionary *dictionary = result.result; + + // We test for a null object, if it isn't, we can use the response to create a PFUser. + if ([dictionary isKindOfClass:[NSNull class]] || !dictionary) { + return [BFTask taskWithError:[PFErrorUtilities errorWithCode:kPFErrorObjectNotFound + message:@"Invalid Session Token."]]; + } + + PFUser *user = [PFUser _objectFromDictionary:dictionary + defaultClassName:[PFUser parseClassName] + completeData:YES]; + // Serialize the object to disk so we can later access it via currentUser + PFCurrentUserController *controller = self.coreDataSource.currentUserController; + return [[controller saveCurrentObjectAsync:user] continueWithBlock:^id(BFTask *task) { + return user; + }]; + }]; +} + +- (BFTask *)logInCurrentUserAsyncWithUsername:(NSString *)username + password:(NSString *)password + revocableSession:(BOOL)revocableSession { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + PFRESTCommand *command = [PFRESTUserCommand logInUserCommandWithUsername:username + password:password + revocableSession:revocableSession]; + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + PFCommandResult *result = task.result; + NSDictionary *dictionary = result.result; + + // We test for a null object, if it isn't, we can use the response to create a PFUser. + if ([dictionary isKindOfClass:[NSNull class]] || !dictionary) { + return [BFTask taskWithError:[PFErrorUtilities errorWithCode:kPFErrorObjectNotFound + message:@"Invalid login credentials."]]; + } + + PFUser *user = [PFUser _objectFromDictionary:dictionary + defaultClassName:[PFUser parseClassName] + completeData:YES]; + + // Serialize the object to disk so we can later access it via currentUser + PFCurrentUserController *controller = self.coreDataSource.currentUserController; + return [[controller saveCurrentObjectAsync:user] continueWithBlock:^id(BFTask *task) { + return user; + }]; + }]; +} + +- (BFTask *)logInCurrentUserAsyncWithAuthType:(NSString *)authType + authData:(NSDictionary *)authData + revocableSession:(BOOL)revocableSession { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [PFRESTUserCommand serviceLoginUserCommandWithAuthenticationType:authType + authenticationData:authData + revocableSession:revocableSession]; + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + PFUser *user = [PFUser _objectFromDictionary:result.result + defaultClassName:[PFUser parseClassName] + completeData:YES]; + @synchronized ([user lock]) { + user.authData[authType] = authData; + [user.linkedServiceNames addObject:authType]; + [user startSave]; + return [user _handleServiceLoginCommandResult:result]; + } + }]; +} + +///-------------------------------------- +#pragma mark - Reset Password +///-------------------------------------- + +- (BFTask *)requestPasswordResetAsyncForEmail:(NSString *)email { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [PFRESTUserCommand resetPasswordCommandForUserWithEmail:email]; + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessResult:nil]; +} + +///-------------------------------------- +#pragma mark - Log Out +///-------------------------------------- + +- (BFTask *)logOutUserAsyncWithSessionToken:(NSString *)sessionToken { + @weakify(self); + return [[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + @strongify(self); + PFRESTCommand *command = [PFRESTUserCommand logOutUserCommandWithSessionToken:sessionToken]; + return [self.commonDataSource.commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithSuccessResult:nil]; +} + +@end diff --git a/Parse/Internal/User/CurrentUserController/PFCurrentUserController.h b/Parse/Internal/User/CurrentUserController/PFCurrentUserController.h new file mode 100644 index 000000000..ec6ae1fd1 --- /dev/null +++ b/Parse/Internal/User/CurrentUserController/PFCurrentUserController.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreDataProvider.h" +#import "PFCurrentObjectControlling.h" +#import "PFDataProvider.h" + +@class BFTask; +@class PFUser; + +typedef NS_OPTIONS(NSUInteger, PFCurrentUserLoadingOptions) { + PFCurrentUserLoadingOptionCreateLazyIfNotAvailable = 1 << 0, +}; + +@interface PFCurrentUserController : NSObject + +@property (nonatomic, weak, readonly) id commonDataSource; +@property (nonatomic, weak, readonly) id coreDataSource; + +@property (atomic, assign) BOOL automaticUsersEnabled; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithStorageType:(PFCurrentObjectStorageType)storageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource NS_DESIGNATED_INITIALIZER; ++ (instancetype)controllerWithStorageType:(PFCurrentObjectStorageType)storageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource; + +///-------------------------------------- +/// @name User +///-------------------------------------- + +- (BFTask *)getCurrentUserAsyncWithOptions:(PFCurrentUserLoadingOptions)options; + +- (BFTask *)logOutCurrentUserAsync; + +///-------------------------------------- +/// @name Session Token +///-------------------------------------- + +- (BFTask *)getCurrentUserSessionTokenAsync; + +@end diff --git a/Parse/Internal/User/CurrentUserController/PFCurrentUserController.m b/Parse/Internal/User/CurrentUserController/PFCurrentUserController.m new file mode 100644 index 000000000..25e8ff3c0 --- /dev/null +++ b/Parse/Internal/User/CurrentUserController/PFCurrentUserController.m @@ -0,0 +1,364 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCurrentUserController.h" + +#import + +#import "BFTask+Private.h" +#import "PFAnonymousUtils_Private.h" +#import "PFAssert.h" +#import "PFAsyncTaskQueue.h" +#import "PFFileManager.h" +#import "PFKeychainStore.h" +#import "PFMutableUserState.h" +#import "PFObjectFilePersistenceController.h" +#import "PFObjectPrivate.h" +#import "PFQuery.h" +#import "PFUserConstants.h" +#import "PFUserPrivate.h" + +@interface PFCurrentUserController () { + dispatch_queue_t _dataQueue; + PFAsyncTaskQueue *_dataTaskQueue; + + PFUser *_currentUser; + BOOL _currentUserMatchesDisk; +} + +@end + +@implementation PFCurrentUserController + +@synthesize storageType = _storageType; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithStorageType:(PFCurrentObjectStorageType)storageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + self = [super init]; + if (!self) return nil; + + _dataQueue = dispatch_queue_create("com.parse.currentUser.controller", DISPATCH_QUEUE_CONCURRENT); + _dataTaskQueue = [PFAsyncTaskQueue taskQueue]; + + _storageType = storageType; + _commonDataSource = commonDataSource; + _coreDataSource = coreDataSource; + + return self; +} + ++ (instancetype)controllerWithStorageType:(PFCurrentObjectStorageType)dataStorageType + commonDataSource:(id)commonDataSource + coreDataSource:(id)coreDataSource { + return [[self alloc] initWithStorageType:dataStorageType + commonDataSource:commonDataSource + coreDataSource:coreDataSource]; +} + +///-------------------------------------- +#pragma mark - PFCurrentObjectControlling +///-------------------------------------- + +- (BFTask *)getCurrentObjectAsync { + PFCurrentUserLoadingOptions options = 0; + if (self.automaticUsersEnabled) { + options |= PFCurrentUserLoadingOptionCreateLazyIfNotAvailable; + } + return [self getCurrentUserAsyncWithOptions:options]; +} + +- (BFTask *)saveCurrentObjectAsync:(PFUser *)object { + return [_dataTaskQueue enqueue:^id(BFTask *task) { + return [self _saveCurrentUserAsync:object]; + }]; +} + +///-------------------------------------- +#pragma mark - User +///-------------------------------------- + +- (BFTask *)getCurrentUserAsyncWithOptions:(PFCurrentUserLoadingOptions)options { + return [_dataTaskQueue enqueue:^id(BFTask *task) { + return [self _getCurrentUserAsyncWithOptions:options]; + }]; +} + +- (BFTask *)_getCurrentUserAsyncWithOptions:(PFCurrentUserLoadingOptions)options { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + __block BOOL matchesDisk = NO; + __block PFUser *currentUser = nil; + dispatch_sync(_dataQueue, ^{ + matchesDisk = _currentUserMatchesDisk; + currentUser = _currentUser; + }); + if (currentUser) { + return currentUser; + } + + if (matchesDisk) { + if (options & PFCurrentUserLoadingOptionCreateLazyIfNotAvailable) { + return [self _lazyLogInUser]; + } + return nil; + } + + return [[[[self _loadCurrentUserFromDiskAsync] continueWithSuccessBlock:^id(BFTask *task) { + PFUser *user = task.result; + // If the object was not yet saved, but is already linked with AnonymousUtils - it means it is lazy. + // So mark it's state as `isLazy` and make it `dirty` + if (!user.objectId && [PFAnonymousUtils isLinkedWithUser:user]) { + user.isLazy = YES; + [user _setDirty:YES]; + } + [user setIsCurrentUser:YES]; + return user; + }] continueWithBlock:^id(BFTask *task) { + dispatch_barrier_sync(_dataQueue, ^{ + _currentUser = task.result; + _currentUserMatchesDisk = !task.faulted; + }); + return task; + }] continueWithBlock:^id(BFTask *task) { + // If there's no user and automatic user is enabled, do lazy login. + if (!task.result && (options & PFCurrentUserLoadingOptionCreateLazyIfNotAvailable)) { + return [self _lazyLogInUser]; + } + return task; + }]; + }]; +} + +- (BFTask *)_saveCurrentUserAsync:(PFUser *)user { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + __block PFUser *currentUser = nil; + dispatch_sync(_dataQueue, ^{ + currentUser = _currentUser; + }); + + BFTask *task = [BFTask taskWithResult:nil]; + // Check for objectId equality to not logout in case we are saving another instance of the same user. + if (currentUser != nil && currentUser != user && ![user.objectId isEqualToString:currentUser.objectId]) { + task = [task continueWithBlock:^id(BFTask *task) { + return [currentUser _logOutAsync]; + }]; + } + return [[task continueWithBlock:^id(BFTask *task) { + @synchronized (user.lock) { + [user setIsCurrentUser:YES]; + [user synchronizeAllAuthData]; + } + return [self _saveCurrentUserToDiskAsync:user]; + }] continueWithBlock:^id(BFTask *task) { + dispatch_barrier_sync(_dataQueue, ^{ + _currentUser = user; + _currentUserMatchesDisk = !task.faulted && !task.cancelled; + }); + return user; + }]; + }]; +} + +- (BFTask *)logOutCurrentUserAsync { + return [_dataTaskQueue enqueue:^id(BFTask *task) { + return [[self _getCurrentUserAsyncWithOptions:0] continueWithBlock:^id(BFTask *task) { + BFTask *userLogoutTask = nil; + + PFUser *user = task.result; + if (user) { + userLogoutTask = [user _logOutAsync]; + } else { + userLogoutTask = [BFTask taskWithResult:nil]; + } + + NSString *filePath = [self.commonDataSource.fileManager parseDataItemPathForPathComponent:PFUserCurrentUserFileName]; + BFTask *fileTask = [PFFileManager removeItemAtPathAsync:filePath]; + BFTask *unpinTask = nil; + + if (self.storageType == PFCurrentObjectStorageTypeOfflineStore) { + unpinTask = [PFObject unpinAllObjectsInBackgroundWithName:PFUserCurrentUserPinName]; + } else { + unpinTask = [BFTask taskWithResult:nil]; + } + + [self _deleteSensitiveUserDataFromKeychainWithItemName:PFUserCurrentUserFileName]; + + BFTask *logoutTask = [[BFTask taskForCompletionOfAllTasks:@[ fileTask, unpinTask ]] continueWithBlock:^id(BFTask *task) { + dispatch_barrier_sync(_dataQueue, ^{ + _currentUser = nil; + _currentUserMatchesDisk = YES; + }); + return nil; + }]; + return [BFTask taskForCompletionOfAllTasks:@[ userLogoutTask, logoutTask ]]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Data Storage +///-------------------------------------- + +- (BFTask *)_loadCurrentUserFromDiskAsync { + BFTask *task = nil; + if (self.storageType == PFCurrentObjectStorageTypeOfflineStore) { + // Try loading from OfflineStore + PFQuery *query = [[[PFQuery queryWithClassName:[PFUser parseClassName]] + fromPinWithName:PFUserCurrentUserPinName] + // We need to ignoreACLs here because right now we don't have currentUser. + ignoreACLs]; + + // Silence the warning if we are loading from LDS + task = [[query findObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *results = task.result; + if ([results count] == 1) { + return [BFTask taskWithResult:results.firstObject]; + } else if ([results count] != 0) { + return [[PFObject unpinAllObjectsInBackgroundWithName:PFUserCurrentUserPinName] + continueWithSuccessResult:nil]; + } + + // Backward compatibility if we previously have non-LDS currentUser. + return [PFObject _migrateObjectInBackgroundFromFile:PFUserCurrentUserFileName toPin:PFUserCurrentUserPinName usingMigrationBlock:^id(BFTask *task) { + PFUser *user = task.result; + // Only migrate session token to Keychain if it was loaded from Data File. + if (user.sessionToken) { + return [self _saveSensitiveUserDataAsync:user + toKeychainItemWithName:PFUserCurrentUserKeychainItemName]; + } + return nil; + }]; + }]; + } else { + PFObjectFilePersistenceController *controller = self.coreDataSource.objectFilePersistenceController; + task = [controller loadPersistentObjectAsyncForKey:PFUserCurrentUserFileName]; + } + return [task continueWithSuccessBlock:^id(BFTask *task) { + PFUser *user = task.result; + return [[self _loadSensitiveUserDataAsync:user + fromKeychainItemWithName:PFUserCurrentUserKeychainItemName] continueWithSuccessResult:user]; + }]; +} + +- (BFTask *)_saveCurrentUserToDiskAsync:(PFUser *)user { + if (self.storageType == PFCurrentObjectStorageTypeOfflineStore) { + return [[[PFObject unpinAllObjectsInBackgroundWithName:PFUserCurrentUserPinName] continueWithSuccessBlock:^id(BFTask *task) { + return [self _saveSensitiveUserDataAsync:user toKeychainItemWithName:PFUserCurrentUserKeychainItemName]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // We don't want to include children of `currentUser` automatically. + return [user _pinInBackgroundWithName:PFUserCurrentUserPinName includeChildren:NO]; + }]; + } + + return [[self _saveSensitiveUserDataAsync:user + toKeychainItemWithName:PFUserCurrentUserKeychainItemName] continueWithBlock:^id(BFTask *task) { + PFObjectFilePersistenceController *controller = self.coreDataSource.objectFilePersistenceController; + return [controller persistObjectAsync:user forKey:PFUserCurrentUserFileName]; + }]; +} + +///-------------------------------------- +#pragma mark - Sensitive Data +///-------------------------------------- + +- (BFTask *)_loadSensitiveUserDataAsync:(PFUser *)user fromKeychainItemWithName:(NSString *)itemName { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSDictionary *userData = self.commonDataSource.keychainStore[itemName]; + @synchronized (user.lock) { + if (userData) { + PFMutableUserState *state = [user._state mutableCopy]; + + NSString *sessionToken = userData[PFUserSessionTokenRESTKey] ?: userData[@"session_token"]; + if (sessionToken) { + state.sessionToken = sessionToken; + } + + user._state = state; + + NSDictionary *newAuthData = userData[PFUserAuthDataRESTKey] ?: userData[@"auth_data"]; + [newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + user.authData[key] = obj; + if (obj != nil) { + [user.linkedServiceNames addObject:key]; + } + [user synchronizeAuthDataWithAuthType:key]; + }]; + } + } + return nil; + }]; +} + +- (BFTask *)_saveSensitiveUserDataAsync:(PFUser *)user toKeychainItemWithName:(NSString *)itemName { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + NSMutableDictionary *userData = [NSMutableDictionary dictionaryWithCapacity:2]; + @synchronized (user.lock) { + if (user.sessionToken) { + userData[PFUserSessionTokenRESTKey] = [user.sessionToken copy]; + } + if ([user.authData count]) { + userData[PFUserAuthDataRESTKey] = [user.authData copy]; + } + } + self.commonDataSource.keychainStore[itemName] = userData; + + return nil; + }]; +} + +- (void)_deleteSensitiveUserDataFromKeychainWithItemName:(NSString *)itemName { + [self.commonDataSource.keychainStore removeObjectForKey:itemName]; +} + +///-------------------------------------- +#pragma mark - Session Token +///-------------------------------------- + +- (BFTask *)getCurrentUserSessionTokenAsync { + return [[self getCurrentUserAsyncWithOptions:0] continueWithSuccessBlock:^id(BFTask *task) { + PFUser *user = task.result; + return user.sessionToken; + }]; +} + +///-------------------------------------- +#pragma mark - Lazy Login +///-------------------------------------- + +- (BFTask *)_lazyLogInUser { + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + PFUser *user = [PFAnonymousUtils _lazyLogIn]; + + // When LDS is enabled, we will immediately save the anon user to LDS. When LDS is disabled, we + // will create the anon user, but will lazily save it to Parse on an object save that has this + // user in its ACL. + // The main differences here would be that non-LDS may have different anon users in different + // sessions until an object is saved and LDS will persist the same anon user. This shouldn't be a + // big deal... + if (self.storageType == PFCurrentObjectStorageTypeOfflineStore) { + return [[self _saveCurrentUserAsync:user] continueWithSuccessResult:user]; + } + + dispatch_barrier_sync(_dataQueue, ^{ + _currentUser = user; + _currentUserMatchesDisk = YES; + }); + return user; + }]; +} + +@end diff --git a/Parse/Internal/User/PFUserPrivate.h b/Parse/Internal/User/PFUserPrivate.h new file mode 100644 index 000000000..295f376ae --- /dev/null +++ b/Parse/Internal/User/PFUserPrivate.h @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +# import +#else +# import +#endif + +#import "PFAuthenticationProvider.h" + +extern NSString *const PFUserCurrentUserFileName; +extern NSString *const PFUserCurrentUserPinName; +extern NSString *const PFUserCurrentUserKeychainItemName; + +@class BFTask; +@class PFCommandResult; + +@interface PFUser (Private) + +///-------------------------------------- +/// @name Current User +///-------------------------------------- ++ (BFTask *)_getCurrentUserSessionTokenAsync; ++ (NSString *)currentSessionToken; + +- (void)synchronizeAllAuthData; + +- (void)checkSignUpParams; + ++ (BFTask *)_logInWithAuthTypeInBackground:(NSString *)authType authData:(NSDictionary *)authData; +- (BFTask *)_handleServiceLoginCommandResult:(PFCommandResult *)result; + +- (BFTask *)_linkWithAuthTypeInBackground:(NSString *)authType authData:(NSDictionary *)authData; + +- (BFTask *)_unlinkWithAuthTypeInBackground:(NSString *)authType; + +- (void)synchronizeAuthDataWithAuthType:(NSString *)authType; + ++ (PFUser *)logInLazyUserWithAuthType:(NSString *)authType authData:(NSDictionary *)authData; +- (BFTask *)resolveLazinessAsync:(BFTask *)toAwait; +- (void)stripAnonymity; +- (void)restoreAnonymity:(id)data; + +///-------------------------------------- +/// @name Revocable Session +///-------------------------------------- ++ (BOOL)_isRevocableSessionEnabled; ++ (void)_setRevocableSessionEnabled:(BOOL)enabled; + +@end + +// Private Properties +@interface PFUser () { + BOOL isCurrentUser; + NSMutableDictionary *authData; + NSMutableSet *linkedServiceNames; + BOOL isLazy; +} + +// This earmarks the user as being an "identity" user. This will make saves write through +// to the currentUser singleton and disk object +@property (nonatomic, assign) BOOL isCurrentUser; + +@property (strong, readonly) NSMutableDictionary *authData; +@property (strong, readonly) NSMutableSet *linkedServiceNames; +@property (nonatomic, assign) BOOL isLazy; + +- (BOOL)_isAuthenticatedWithCurrentUser:(PFUser *)currentUser; + +- (BFTask *)_logOutAsync; + +@end diff --git a/Parse/Internal/User/State/PFMutableUserState.h b/Parse/Internal/User/State/PFMutableUserState.h new file mode 100644 index 000000000..43e8f2d3d --- /dev/null +++ b/Parse/Internal/User/State/PFMutableUserState.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUserState.h" + +@interface PFMutableUserState : PFUserState + +@property (nonatomic, copy, readwrite) NSString *sessionToken; +@property (nonatomic, copy, readwrite) NSDictionary *authData; + +@property (nonatomic, assign, readwrite) BOOL isNew; + +@end diff --git a/Parse/Internal/User/State/PFMutableUserState.m b/Parse/Internal/User/State/PFMutableUserState.m new file mode 100644 index 000000000..255265b49 --- /dev/null +++ b/Parse/Internal/User/State/PFMutableUserState.m @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableUserState.h" + +#import "PFUserState_Private.h" + +@implementation PFMutableUserState + +@dynamic sessionToken; +@dynamic authData; +@dynamic isNew; + +@end diff --git a/Parse/Internal/User/State/PFUserState.h b/Parse/Internal/User/State/PFUserState.h new file mode 100644 index 000000000..72fa09817 --- /dev/null +++ b/Parse/Internal/User/State/PFUserState.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObjectState.h" + +@interface PFUserState : PFObjectState + +@property (nonatomic, copy, readonly) NSString *sessionToken; +@property (nonatomic, copy, readonly) NSDictionary *authData; + +@property (nonatomic, assign, readonly) BOOL isNew; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)initWithState:(PFUserState *)state; ++ (instancetype)stateWithState:(PFUserState *)state; + +@end diff --git a/Parse/Internal/User/State/PFUserState.m b/Parse/Internal/User/State/PFUserState.m new file mode 100644 index 000000000..2c50485ce --- /dev/null +++ b/Parse/Internal/User/State/PFUserState.m @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUserState.h" +#import "PFUserState_Private.h" + +#import "PFMutableUserState.h" +#import "PFObjectState_Private.h" +#import "PFUserConstants.h" + +@implementation PFUserState + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithState:(PFUserState *)state { + self = [super initWithState:state]; + if (!self) return nil; + + _sessionToken = [state.sessionToken copy]; + _authData = [state.authData copy]; + _isNew = state.isNew; + + return self; +} + ++ (instancetype)stateWithState:(PFUserState *)state { + return [super stateWithState:state]; +} + +///-------------------------------------- +#pragma mark - Serialization +///-------------------------------------- + +- (NSDictionary *)dictionaryRepresentationWithObjectEncoder:(PFEncoder *)objectEncoder { + NSMutableDictionary *dictionary = [[super dictionaryRepresentationWithObjectEncoder:objectEncoder] mutableCopy]; + [dictionary removeObjectForKey:PFUserPasswordRESTKey]; + return dictionary; +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (id)copyWithZone:(NSZone *)zone { + return [[PFUserState allocWithZone:zone] initWithState:self]; +} + +///-------------------------------------- +#pragma mark - NSMutableCopying +///-------------------------------------- + +- (id)mutableCopyWithZone:(NSZone *)zone { + return [[PFMutableUserState allocWithZone:zone] initWithState:self]; +} + +@end diff --git a/Parse/Internal/User/State/PFUserState_Private.h b/Parse/Internal/User/State/PFUserState_Private.h new file mode 100644 index 000000000..c305f7609 --- /dev/null +++ b/Parse/Internal/User/State/PFUserState_Private.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUserState.h" + +@interface PFUserState () { +@protected + NSString *_sessionToken; + NSDictionary *_authData; + + BOOL _isNew; +} + +@property (nonatomic, copy, readwrite) NSString *sessionToken; +@property (nonatomic, copy, readwrite) NSDictionary *authData; + +@property (nonatomic, assign, readwrite) BOOL isNew; + +@end diff --git a/Parse/OSX/ParseOSX.h b/Parse/OSX/ParseOSX.h new file mode 100644 index 000000000..c4803fd0f --- /dev/null +++ b/Parse/OSX/ParseOSX.h @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import diff --git a/Parse/PFACL.h b/Parse/PFACL.h new file mode 100644 index 000000000..5c4c1b1e3 --- /dev/null +++ b/Parse/PFACL.h @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class PFRole; +@class PFUser; + +/*! + The `PFACL` class is used to control which users can access or modify a particular object. + Each can have its own `PFACL`. You can grant read and write permissions separately to specific users, + to groups of users that belong to roles, or you can grant permissions to "the public" so that, + for example, any user could read a particular object but only a particular set of users could write to that object. + */ +@interface PFACL : NSObject + +///-------------------------------------- +/// @name Creating an ACL +///-------------------------------------- + +/*! + @abstract Creates an ACL with no permissions granted. + + @returns Returns a new `PFACL`. + */ ++ (instancetype)ACL; + +/*! + @abstract Creates an ACL where only the provided user has access. + + @param user The user to assign access. + */ ++ (instancetype)ACLWithUser:(PFUser *)user; + +///-------------------------------------- +/// @name Controlling Public Access +///-------------------------------------- + +/*! + @abstract Set whether the public is allowed to read this object. + + @param allowed Whether the public can read this object. + */ +- (void)setPublicReadAccess:(BOOL)allowed; + +/*! + @abstract Gets whether the public is allowed to read this object. + + @returns `YES` if the public read access is enabled, otherwise `NO`. + */ +- (BOOL)getPublicReadAccess; + +/*! + @abstract Set whether the public is allowed to write this object. + + @param allowed Whether the public can write this object. + */ +- (void)setPublicWriteAccess:(BOOL)allowed; + +/*! + @abstract Gets whether the public is allowed to write this object. + + @returns `YES` if the public write access is enabled, otherwise `NO`. + */ +- (BOOL)getPublicWriteAccess; + +///-------------------------------------- +/// @name Controlling Access Per-User +///-------------------------------------- + +/*! + @abstract Set whether the given user id is allowed to read this object. + + @param allowed Whether the given user can write this object. + @param userId The <[PFObject objectId]> of the user to assign access. + */ +- (void)setReadAccess:(BOOL)allowed forUserId:(NSString *)userId; + +/*! + @abstract Gets whether the given user id is *explicitly* allowed to read this object. + Even if this returns `NO`, the user may still be able to access it if returns `YES` + or if the user belongs to a role that has access. + + @param userId The <[PFObject objectId]> of the user for which to retrive access. + + @returns `YES` if the user with this `objectId` has *explicit* read access, otherwise `NO`. + */ +- (BOOL)getReadAccessForUserId:(NSString *)userId; + +/*! + @abstract Set whether the given user id is allowed to write this object. + + @param allowed Whether the given user can read this object. + @param userId The `objectId` of the user to assign access. + */ +- (void)setWriteAccess:(BOOL)allowed forUserId:(NSString *)userId; + +/*! + @abstract Gets whether the given user id is *explicitly* allowed to write this object. + Even if this returns NO, the user may still be able to write it if returns `YES` + or if the user belongs to a role that has access. + + @param userId The <[PFObject objectId]> of the user for which to retrive access. + + @returns `YES` if the user with this `objectId` has *explicit* write access, otherwise `NO`. + */ +- (BOOL)getWriteAccessForUserId:(NSString *)userId; + +/*! + @abstract Set whether the given user is allowed to read this object. + + @param allowed Whether the given user can read this object. + @param user The user to assign access. + */ +- (void)setReadAccess:(BOOL)allowed forUser:(PFUser *)user; + +/*! + @abstract Gets whether the given user is *explicitly* allowed to read this object. + Even if this returns `NO`, the user may still be able to access it if returns `YES` + or if the user belongs to a role that has access. + + @param user The user for which to retrive access. + + @returns `YES` if the user has *explicit* read access, otherwise `NO`. + */ +- (BOOL)getReadAccessForUser:(PFUser *)user; + +/*! + @abstract Set whether the given user is allowed to write this object. + + @param allowed Whether the given user can write this object. + @param user The user to assign access. + */ +- (void)setWriteAccess:(BOOL)allowed forUser:(PFUser *)user; + +/*! + @abstract Gets whether the given user is *explicitly* allowed to write this object. + Even if this returns `NO`, the user may still be able to write it if returns `YES` + or if the user belongs to a role that has access. + + @param user The user for which to retrive access. + + @returns `YES` if the user has *explicit* write access, otherwise `NO`. + */ +- (BOOL)getWriteAccessForUser:(PFUser *)user; + +///-------------------------------------- +/// @name Controlling Access Per-Role +///-------------------------------------- + +/*! + @abstract Get whether users belonging to the role with the given name are allowed to read this object. + Even if this returns `NO`, the role may still be able to read it if a parent role has read access. + + @param name The name of the role. + + @returns `YES` if the role has read access, otherwise `NO`. + */ +- (BOOL)getReadAccessForRoleWithName:(NSString *)name; + +/*! + @abstract Set whether users belonging to the role with the given name are allowed to read this object. + + @param allowed Whether the given role can read this object. + @param name The name of the role. + */ +- (void)setReadAccess:(BOOL)allowed forRoleWithName:(NSString *)name; + +/*! + @abstract Get whether users belonging to the role with the given name are allowed to write this object. + Even if this returns `NO`, the role may still be able to write it if a parent role has write access. + + @param name The name of the role. + + @returns `YES` if the role has read access, otherwise `NO`. + */ +- (BOOL)getWriteAccessForRoleWithName:(NSString *)name; + +/*! + @abstract Set whether users belonging to the role with the given name are allowed to write this object. + + @param allowed Whether the given role can write this object. + @param name The name of the role. + */ +- (void)setWriteAccess:(BOOL)allowed forRoleWithName:(NSString *)name; + +/*! + @abstract Get whether users belonging to the given role are allowed to read this object. + Even if this returns `NO`, the role may still be able to read it if a parent role has read access. + + @discussion The role must already be saved on the server and + it's data must have been fetched in order to use this method. + + @param role The name of the role. + + @returns `YES` if the role has read access, otherwise `NO`. + */ +- (BOOL)getReadAccessForRole:(PFRole *)role; + +/*! + @abstract Set whether users belonging to the given role are allowed to read this object. + + @discussion The role must already be saved on the server and + it's data must have been fetched in order to use this method. + + @param allowed Whether the given role can read this object. + @param role The role to assign access. + */ +- (void)setReadAccess:(BOOL)allowed forRole:(PFRole *)role; + +/*! + @abstract Get whether users belonging to the given role are allowed to write this object. + Even if this returns `NO`, the role may still be able to write it if a parent role has write access. + + @discussion The role must already be saved on the server and + it's data must have been fetched in order to use this method. + + @param role The name of the role. + + @returns `YES` if the role has write access, otherwise `NO`. + */ +- (BOOL)getWriteAccessForRole:(PFRole *)role; + +/*! + @abstract Set whether users belonging to the given role are allowed to write this object. + + @discussion The role must already be saved on the server and + it's data must have been fetched in order to use this method. + + @param allowed Whether the given role can write this object. + @param role The role to assign access. + */ +- (void)setWriteAccess:(BOOL)allowed forRole:(PFRole *)role; + +///-------------------------------------- +/// @name Setting Access Defaults +///-------------------------------------- + +/*! + @abstract Sets a default ACL that will be applied to all instances of when they are created. + + @param acl The ACL to use as a template for all instance of created after this method has been called. + This value will be copied and used as a template for the creation of new ACLs, so changes to the + instance after this method has been called will not be reflected in new instance of . + @param currentUserAccess - If `YES`, the `PFACL` that is applied to newly-created instance of will + provide read and write access to the <[PFUser currentUser]> at the time of creation. + - If `NO`, the provided `acl` will be used without modification. + - If `acl` is `nil`, this value is ignored. + */ ++ (void)setDefaultACL:(PF_NULLABLE PFACL *)acl withAccessForCurrentUser:(BOOL)currentUserAccess; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFACL.m b/Parse/PFACL.m new file mode 100644 index 000000000..cf80ce9ff --- /dev/null +++ b/Parse/PFACL.m @@ -0,0 +1,370 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFACL.h" +#import "PFACLPrivate.h" + +#import "BFTask+Private.h" +#import "PFACLState.h" +#import "PFAssert.h" +#import "PFDefaultACLController.h" +#import "PFMacros.h" +#import "PFMutableACLState.h" +#import "PFObjectPrivate.h" +#import "PFObjectUtilities.h" +#import "PFRole.h" +#import "PFUser.h" +#import "PFUserPrivate.h" + +static NSString *const PFACLPublicKey_ = @"*"; +static NSString *const PFACLUnresolvedKey_ = @"*unresolved"; +static NSString *const PFACLCodingDataKey_ = @"ACL"; + +@interface PFACL () + +@property (atomic, strong, readwrite) PFACLState *state; + +@end + +@implementation PFACL { + PFUser *unresolvedUser; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _state = [[PFACLState alloc] init]; + + return self; +} + +///-------------------------------------- +#pragma mark - Default ACL +///-------------------------------------- + ++ (instancetype)ACL { + return [[PFACL alloc] init]; +} + ++ (instancetype)ACLWithUser:(PFUser *)user { + PFACL *acl = [PFACL ACL]; + [acl setReadAccess:YES forUser:user]; + [acl setWriteAccess:YES forUser:user]; + return acl; +} + ++ (instancetype)ACLWithDictionary:(NSDictionary *)dictionary { + return [[PFACL alloc] initWithDictionary:dictionary]; +} + ++ (PFACL *)defaultACL { + return [[[PFDefaultACLController defaultController] getDefaultACLAsync] waitForResult:NULL + withMainThreadWarning:NO]; +} + ++ (void)setDefaultACL:(PFACL *)acl withAccessForCurrentUser:(BOOL)currentUserAccess { + [[PFDefaultACLController defaultController] setDefaultACLAsync:acl withCurrentUserAccess:currentUserAccess]; +} + +- (void)setShared:(BOOL)newShared { + self.state = [self.state copyByMutatingWithBlock:^(PFMutableACLState *newState) { + newState.shared = newShared; + }]; +} + +- (BOOL)isShared { + return self.state.shared; +} +- (instancetype)createUnsharedCopy { + PFACL *newACL = [PFACL ACLWithDictionary:self.state.permissions]; + if (unresolvedUser) { + [newACL setReadAccess:[self getReadAccessForUser:unresolvedUser] forUser:unresolvedUser]; + [newACL setWriteAccess:[self getWriteAccessForUser:unresolvedUser] forUser:unresolvedUser]; + } + return newACL; +} + +- (void)resolveUser:(PFUser *)user { + if (user != unresolvedUser) { + return; + } + NSMutableDictionary *unresolvedPermissions = self.state.permissions[PFACLUnresolvedKey_]; + if (unresolvedPermissions) { + self.state = [self.state copyByMutatingWithBlock:^(PFMutableACLState *newState) { + newState.permissions[user.objectId] = unresolvedPermissions; + [newState.permissions removeObjectForKey:PFACLUnresolvedKey_]; + }]; + } + unresolvedUser = nil; +} + +- (BOOL)hasUnresolvedUser { + return unresolvedUser != nil; +} + +- (void)setAccess:(NSString *)accessType to:(BOOL)allowed forUserId:(NSString *)userId { + NSDictionary *permissions = self.state.permissions[userId]; + + // No change needed. + if ([permissions[accessType] boolValue] == allowed) { + return; + } + + NSMutableDictionary *newPermissions = [NSMutableDictionary dictionaryWithDictionary:permissions]; + if (allowed) { + newPermissions[accessType] = @YES; + } else { + [newPermissions removeObjectForKey:accessType]; + } + + self.state = [self.state copyByMutatingWithBlock:^(PFMutableACLState *newState) { + if (newPermissions.count) { + newState.permissions[userId] = [newPermissions copy]; + } else { + [newState.permissions removeObjectForKey:userId]; + } + }]; +} + +- (BOOL)getAccess:(NSString *)accessType forUserId:(NSString *)userId { + return [self.state.permissions[userId][accessType] boolValue]; +} + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary { + self = [self init]; + if (self) { + // We iterate over the input ACL rather than just copying to + // permissionsById so that we can ensure it is the right format. + [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *userId, NSDictionary *permissions, BOOL *stop) { + [permissions enumerateKeysAndObjectsUsingBlock:^(NSString *accessType, id obj, BOOL *stop) { + [self setAccess:accessType to:[obj boolValue] forUserId:userId]; + }]; + }]; + } + + return self; +} + +- (void)setReadAccess:(BOOL)allowed forUserId:(NSString *)userId { + PFParameterAssert(userId, @"Can't setReadAccess for nil userId."); + [self setAccess:@"read" to:allowed forUserId:userId]; +} + +- (BOOL)getReadAccessForUserId:(NSString *)userId { + PFParameterAssert(userId, @"Can't getReadAccessForUserId for nil userId."); + return [self getAccess:@"read" forUserId:userId]; +} + +- (void)setWriteAccess:(BOOL)allowed forUserId:(NSString *)userId { + PFParameterAssert(userId, @"Can't setWriteAccess for nil userId."); + [self setAccess:@"write" to:allowed forUserId:userId]; +} + +- (BOOL)getWriteAccessForUserId:(NSString *)userId { + PFParameterAssert(userId, @"Can't getWriteAccessForUserId for nil userId."); + return [self getAccess:@"write" forUserId:userId]; +} + +- (void)setPublicReadAccess:(BOOL)allowed { + [self setReadAccess:allowed forUserId:PFACLPublicKey_]; +} + +- (BOOL)getPublicReadAccess { + return [self getReadAccessForUserId:PFACLPublicKey_]; +} + +- (void)setPublicWriteAccess:(BOOL)allowed { + [self setWriteAccess:allowed forUserId:PFACLPublicKey_]; +} + +- (BOOL)getPublicWriteAccess { + return [self getWriteAccessForUserId:PFACLPublicKey_]; +} + +- (BOOL)getReadAccessForRoleWithName:(NSString *)name { + PFParameterAssert(name, @"Can't get read access for nil role name."); + return [self getReadAccessForUserId:[@"role:" stringByAppendingString:name]]; +} + +- (void)setReadAccess:(BOOL)allowed forRoleWithName:(NSString *)name { + PFParameterAssert(name, @"Can't set read access for nil role name."); + [self setReadAccess:allowed forUserId:[@"role:" stringByAppendingString:name]]; +} + +- (BOOL)getWriteAccessForRoleWithName:(NSString *)name { + PFParameterAssert(name, @"Can't get write access for nil role name."); + return [self getWriteAccessForUserId:[@"role:" stringByAppendingString:name]]; +} + +- (void)setWriteAccess:(BOOL)allowed forRoleWithName:(NSString *)name { + PFParameterAssert(name, @"Can't set write access for nil role name."); + [self setWriteAccess:allowed forUserId:[@"role:" stringByAppendingString:name]]; +} + +- (void)validateRoleState:(PFRole *)role { + // Validates that a role has already been saved to the server, and thus can be used in an ACL. + PFParameterAssert(role.objectId, @"Roles must be saved to the server before they can be used in an ACL."); +} + +- (BOOL)getReadAccessForRole:(PFRole *)role { + [self validateRoleState:role]; + return [self getReadAccessForRoleWithName:role.name]; +} + +- (void)setReadAccess:(BOOL)allowed forRole:(PFRole *)role { + [self validateRoleState:role]; + [self setReadAccess:allowed forRoleWithName:role.name]; +} + +- (BOOL)getWriteAccessForRole:(PFRole *)role { + [self validateRoleState:role]; + return [self getWriteAccessForRoleWithName:role.name]; +} + +- (void)setWriteAccess:(BOOL)allowed forRole:(PFRole *)role { + [self validateRoleState:role]; + [self setWriteAccess:allowed forRoleWithName:role.name]; +} + +- (void)prepareUnresolvedUser:(PFUser *)user { + // TODO: (nlutsenko) Consider making @synchronized. + if (unresolvedUser != user) { + // If the unresolved user changed, register the save listener on the new user. This listener + // will call resolveUser with the user. + self.state = [self.state copyByMutatingWithBlock:^(PFMutableACLState *newState) { + [newState.permissions removeObjectForKey:PFACLUnresolvedKey_]; + }]; + + unresolvedUser = user; + + // Note: callback is a reference back to the same block so that it can unregister itself. + @weakify(self); + __weak __block void (^weakCallback)(id result, NSError *error) = nil; + __block void (^callback)(id result, NSError *error) = [^(id result, NSError *error) { + @strongify(self); + [self resolveUser:result]; + [result unregisterSaveListener:weakCallback]; + } copy]; + weakCallback = callback; + [user registerSaveListener:callback]; + } +} + +- (void)setUnresolvedReadAccess:(BOOL)allowed forUser:(PFUser *)user { + [self prepareUnresolvedUser:user]; + [self setReadAccess:allowed forUserId:PFACLUnresolvedKey_]; +} + +- (void)setReadAccess:(BOOL)allowed forUser:(PFUser *)user { + NSString *objectId = user.objectId; + if (!objectId) { + if ([user isLazy]) { + [self setUnresolvedReadAccess:allowed forUser:user]; + return; + } + PFParameterAssert(objectId, @"Can't setReadAcccess for unsaved user."); + } + [self setReadAccess:allowed forUserId:objectId]; +} + +- (BOOL)getReadAccessForUser:(PFUser *)user { + if (user == unresolvedUser) { + return [self getReadAccessForUserId:PFACLUnresolvedKey_]; + } + NSString *objectId = user.objectId; + PFParameterAssert(objectId, @"Can't getReadAccessForUser who isn't saved."); + return [self getReadAccessForUserId:objectId]; +} + +- (void)setUnresolvedWriteAccess:(BOOL)allowed forUser:(PFUser *)user { + [self prepareUnresolvedUser:user]; + [self setWriteAccess:allowed forUserId:PFACLUnresolvedKey_]; +} + +- (void)setWriteAccess:(BOOL)allowed forUser:(PFUser *)user { + NSString *objectId = user.objectId; + if (!objectId) { + if ([user isLazy]) { + [self setUnresolvedWriteAccess:allowed forUser:user]; + return; + } + PFParameterAssert(objectId, @"Can't setWriteAccess for unsaved user."); + } + [self setWriteAccess:allowed forUserId:objectId]; +} + +- (BOOL)getWriteAccessForUser:(PFUser *)user { + if (user == unresolvedUser) { + return [self getWriteAccessForUserId:PFACLUnresolvedKey_]; + } + NSString *objectId = user.objectId; + PFParameterAssert(objectId, @"Can't getWriteAccessForUser who isn't saved."); + return [self getWriteAccessForUserId:objectId]; +} + +- (NSDictionary *)encodeIntoDictionary { + return self.state.permissions; +} + +///-------------------------------------- +#pragma mark - NSObject +///-------------------------------------- + +- (BOOL)isEqualToACL:(PFACL *)acl { + if (!acl) { + return NO; + } + + return [self.state isEqual:acl.state] && [PFObjectUtilities isObject:self->unresolvedUser + equalToObject:acl->unresolvedUser]; +} + +- (BOOL)isEqual:(id)object { + if (object == self) { + return YES; + } + + if (![object isKindOfClass:[PFACL class]]) { + return NO; + } + + return [self isEqualToACL:object]; +} + +- (NSUInteger)hash { + return [self.state hash] ^ [unresolvedUser hash]; +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (instancetype)copyWithZone:(NSZone *)zone { + return [[PFACL allocWithZone:zone] initWithDictionary:self.state.permissions]; +} + +///-------------------------------------- +#pragma mark - NSCoding +///-------------------------------------- + +- (instancetype)initWithCoder:(NSCoder *)coder { + NSDictionary *dictionary = [coder decodeObjectForKey:PFACLCodingDataKey_]; + return [self initWithDictionary:dictionary]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:[self encodeIntoDictionary] forKey:PFACLCodingDataKey_]; +} + +@end diff --git a/Parse/PFAnalytics.h b/Parse/PFAnalytics.h new file mode 100644 index 000000000..a3d32a683 --- /dev/null +++ b/Parse/PFAnalytics.h @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class BFTask; + +/*! + `PFAnalytics` provides an interface to Parse's logging and analytics backend. + + Methods will return immediately and cache the request (+ timestamp) to be + handled "eventually." That is, the request will be sent immediately if possible + or the next time a network connection is available. + */ +@interface PFAnalytics : NSObject + +///-------------------------------------- +/// @name App-Open / Push Analytics +///-------------------------------------- + +/*! + @abstract Tracks this application being launched. If this happened as the result of the + user opening a push notification, this method sends along information to + correlate this open with that push. + + @discussion Pass in `nil` to track a standard "application opened" event. + + @param launchOptions The `NSDictionary` indicating the reason the application was + launched, if any. This value can be found as a parameter to various + `UIApplicationDelegate` methods, and can be empty or `nil`. + + @returns Returns the task encapsulating the work being done. + */ ++ (BFTask *)trackAppOpenedWithLaunchOptions:(PF_NULLABLE NSDictionary *)launchOptions; + +/*! + @abstract Tracks this application being launched. + If this happened as the result of the user opening a push notification, + this method sends along information to correlate this open with that push. + + @discussion Pass in `nil` to track a standard "application opened" event. + + @param launchOptions The dictionary indicating the reason the application was + launched, if any. This value can be found as a parameter to various + `UIApplicationDelegate` methods, and can be empty or `nil`. + @param block The block to execute on server response. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)` + */ ++ (void)trackAppOpenedWithLaunchOptionsInBackground:(PF_NULLABLE NSDictionary *)launchOptions + block:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract Tracks this application being launched. If this happened as the result of the + user opening a push notification, this method sends along information to + correlate this open with that push. + + @param userInfo The Remote Notification payload, if any. This value can be + found either under `UIApplicationLaunchOptionsRemoteNotificationKey` on `launchOptions`, + or as a parameter to `application:didReceiveRemoteNotification:`. + This can be empty or `nil`. + + @returns Returns the task encapsulating the work being done. + */ ++ (BFTask *)trackAppOpenedWithRemoteNotificationPayload:(PF_NULLABLE NSDictionary *)userInfo; + +/*! + @abstract Tracks this application being launched. If this happened as the result of the + user opening a push notification, this method sends along information to + correlate this open with that push. + + @param userInfo The Remote Notification payload, if any. This value can be + found either under `UIApplicationLaunchOptionsRemoteNotificationKey` on `launchOptions`, + or as a parameter to `application:didReceiveRemoteNotification:`. This can be empty or `nil`. + @param block The block to execute on server response. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)` + */ ++ (void)trackAppOpenedWithRemoteNotificationPayloadInBackground:(PF_NULLABLE NSDictionary *)userInfo + block:(PF_NULLABLE PFBooleanResultBlock)block; + +///-------------------------------------- +/// @name Custom Analytics +///-------------------------------------- + +/*! + @abstract Tracks the occurrence of a custom event. + + @discussion Parse will store a data point at the time of invocation with the given event name. + + @param name The name of the custom event to report to Parse as having happened. + + @returns Returns the task encapsulating the work being done. + */ ++ (BFTask *)trackEvent:(NSString *)name; + +/*! + @abstract Tracks the occurrence of a custom event. Parse will store a data point at the + time of invocation with the given event name. The event will be sent at some + unspecified time in the future, even if Parse is currently inaccessible. + + @param name The name of the custom event to report to Parse as having happened. + @param block The block to execute on server response. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)` + */ ++ (void)trackEventInBackground:(NSString *)name block:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract Tracks the occurrence of a custom event with additional dimensions. Parse will + store a data point at the time of invocation with the given event name. + + @discussion Dimensions will allow segmentation of the occurrences of this custom event. + Keys and values should be NSStrings, and will throw otherwise. + + To track a user signup along with additional metadata, consider the following: + + NSDictionary *dimensions = @{ @"gender": @"m", + @"source": @"web", + @"dayType": @"weekend" }; + [PFAnalytics trackEvent:@"signup" dimensions:dimensions]; + + @warning There is a default limit of 8 dimensions per event tracked. + + @param name The name of the custom event to report to Parse as having happened. + @param dimensions The `NSDictionary` of information by which to segment this event. + + @returns Returns the task encapsulating the work being done. + */ ++ (BFTask *)trackEvent:(NSString *)name dimensions:(PF_NULLABLE NSDictionary *)dimensions; + +/*! + @abstract Tracks the occurrence of a custom event with additional dimensions. Parse will + store a data point at the time of invocation with the given event name. The + event will be sent at some unspecified time in the future, even if Parse is currently inaccessible. + + @discussionDimensions will allow segmentation of the occurrences of this custom event. + Keys and values should be NSStrings, and will throw otherwise. + + To track a user signup along with additional metadata, consider the following: + NSDictionary *dimensions = @{ @"gender": @"m", + @"source": @"web", + @"dayType": @"weekend" }; + [PFAnalytics trackEvent:@"signup" dimensions:dimensions]; + + There is a default limit of 8 dimensions per event tracked. + + @param name The name of the custom event to report to Parse as having happened. + @param dimensions The `NSDictionary` of information by which to segment this event. + @param block The block to execute on server response. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)` + */ ++ (void)trackEventInBackground:(NSString *)name + dimensions:(PF_NULLABLE NSDictionary *)dimensions + block:(PF_NULLABLE PFBooleanResultBlock)block; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFAnalytics.m b/Parse/PFAnalytics.m new file mode 100644 index 000000000..fa783f76e --- /dev/null +++ b/Parse/PFAnalytics.m @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAnalytics.h" +#import "PFAnalytics_Private.h" + +#import "BFTask+Private.h" +#import "PFAnalyticsController.h" +#import "PFAssert.h" +#import "PFEncoder.h" +#import "PFEventuallyQueue.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +@implementation PFAnalytics + +///-------------------------------------- +#pragma mark - App-Open / Push Analytics +///-------------------------------------- + ++ (BFTask *)trackAppOpenedWithLaunchOptions:(NSDictionary *)launchOptions { +#if TARGET_OS_IPHONE + NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; +#else + NSDictionary *userInfo = launchOptions[NSApplicationLaunchUserNotificationKey]; +#endif + + return [self trackAppOpenedWithRemoteNotificationPayload:userInfo]; +} + ++ (void)trackAppOpenedWithLaunchOptionsInBackground:(NSDictionary *)launchOptions block:(PFBooleanResultBlock)block { + [[self trackAppOpenedWithLaunchOptions:launchOptions] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (BFTask *)trackAppOpenedWithRemoteNotificationPayload:(NSDictionary *)userInfo { + return [[[PFUser _getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + PFAnalyticsController *controller = [Parse _currentManager].analyticsController; + return [controller trackAppOpenedEventAsyncWithRemoteNotificationPayload:userInfo sessionToken:sessionToken]; + }] continueWithSuccessResult:@YES]; +} + ++ (void)trackAppOpenedWithRemoteNotificationPayloadInBackground:(NSDictionary *)userInfo + block:(PFBooleanResultBlock)block { + [[self trackAppOpenedWithRemoteNotificationPayload:userInfo] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +///-------------------------------------- +#pragma mark - Custom Analytics +///-------------------------------------- + ++ (BFTask *)trackEvent:(NSString *)name { + return [self trackEvent:name dimensions:nil]; +} + ++ (void)trackEventInBackground:(NSString *)name block:(PFBooleanResultBlock)block { + [self trackEventInBackground:name dimensions:nil block:block]; +} + ++ (BFTask *)trackEvent:(NSString *)name dimensions:(NSDictionary *)dimensions { + PFParameterAssert([[name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length], + @"A name for the custom event must be provided."); + [dimensions enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + PFParameterAssert([key isKindOfClass:[NSString class]] && [obj isKindOfClass:[NSString class]], + @"trackEvent dimensions expect keys and values of type NSString."); + }]; + + return [[[PFUser _getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + PFAnalyticsController *controller = [Parse _currentManager].analyticsController; + return [controller trackEventAsyncWithName:name dimensions:dimensions sessionToken:sessionToken]; + }] continueWithSuccessResult:@YES]; +} + ++ (void)trackEventInBackground:(NSString *)name + dimensions:(NSDictionary *)dimensions + block:(PFBooleanResultBlock)block { + [[self trackEvent:name dimensions:dimensions] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +@end diff --git a/Parse/PFAnonymousUtils.h b/Parse/PFAnonymousUtils.h new file mode 100644 index 000000000..d1334649a --- /dev/null +++ b/Parse/PFAnonymousUtils.h @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#else +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +/*! + Provides utility functions for working with Anonymously logged-in users. + Anonymous users have some unique characteristics: + + - Anonymous users don't need a user name or password. + - Once logged out, an anonymous user cannot be recovered. + - When the current user is anonymous, the following methods can be used to switch + to a different user or convert the anonymous user into a regular one: + - signUp converts an anonymous user to a standard user with the given username and password. + Data associated with the anonymous user is retained. + - logIn switches users without converting the anonymous user. + Data associated with the anonymous user will be lost. + - Service logIn (e.g. Facebook, Twitter) will attempt to convert + the anonymous user into a standard user by linking it to the service. + If a user already exists that is linked to the service, it will instead switch to the existing user. + - Service linking (e.g. Facebook, Twitter) will convert the anonymous user + into a standard user by linking it to the service. + */ +@interface PFAnonymousUtils : NSObject + +///-------------------------------------- +/// @name Creating an Anonymous User +///-------------------------------------- + +/*! + @abstract Creates an anonymous user asynchronously and sets as a result to `BFTask`. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)logInInBackground; + +/*! + @abstract Creates an anonymous user. + + @param block The block to execute when anonymous user creation is complete. + It should have the following argument signature: `^(PFUser *user, NSError *error)`. + */ ++ (void)logInWithBlock:(PF_NULLABLE PFUserResultBlock)block; + +/* + @abstract Creates an anonymous user. + + @param target Target object for the selector. + @param selector The selector that will be called when the asynchronous request is complete. + It should have the following signature: `(void)callbackWithUser:(PFUser *)user error:(NSError *)error`. + */ ++ (void)logInWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Determining Whether a User is Anonymous +///-------------------------------------- + +/*! + @abstract Whether the object is logged in anonymously. + + @param user object to check for anonymity. The user must be logged in on this device. + + @returns `YES` if the user is anonymous. `NO` if the user is not the current user or is not anonymous. + */ ++ (BOOL)isLinkedWithUser:(PF_NULLABLE PFUser *)user; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFAnonymousUtils.m b/Parse/PFAnonymousUtils.m new file mode 100644 index 000000000..e979d7ca5 --- /dev/null +++ b/Parse/PFAnonymousUtils.m @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAnonymousUtils.h" +#import "PFAnonymousUtils_Private.h" + +#import "BFTask+Private.h" +#import "PFAnonymousAuthenticationProvider.h" +#import "PFCoreManager.h" +#import "PFInternalUtils.h" +#import "PFUserAuthenticationController.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +@implementation PFAnonymousUtils + ++ (PFAnonymousAuthenticationProvider *)_authenticationProvider { + NSString *authType = [PFAnonymousAuthenticationProvider authType]; + + PFUserAuthenticationController *controller = [Parse _currentManager].coreManager.userAuthenticationController; + PFAnonymousAuthenticationProvider *provider = [controller authenticationProviderForAuthType:authType]; + if (!provider) { + provider = [[PFAnonymousAuthenticationProvider alloc] init]; + [controller registerAuthenticationProvider:provider]; + } + return provider; +} + ++ (BOOL)isLinkedWithUser:(PFUser *)user { + return [user.linkedServiceNames containsObject:[[[self _authenticationProvider] class] authType]]; +} + ++ (BFTask *)logInInBackground { + PFUserAuthenticationController *controller = [Parse _currentManager].coreManager.userAuthenticationController; + return [controller logInUserAsyncWithAuthType:[[[self _authenticationProvider] class] authType]]; +} + ++ (void)logInWithBlock:(PFUserResultBlock)block { + [[self logInInBackground] thenCallBackOnMainThreadAsync:block]; +} + ++ (void)logInWithTarget:(id)target selector:(SEL)selector { + [PFAnonymousUtils logInWithBlock:^(PFUser *user, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:user object:error]; + }]; +} + ++ (PFUser *)_lazyLogIn { + PFAnonymousAuthenticationProvider *provider = [self _authenticationProvider]; + return [PFUser logInLazyUserWithAuthType:[[provider class] authType] authData:[provider authData]]; +} + +@end diff --git a/Parse/PFCloud.h b/Parse/PFCloud.h new file mode 100644 index 000000000..57ca6124f --- /dev/null +++ b/Parse/PFCloud.h @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class BFTask; + +/*! + The `PFCloud` class provides methods for interacting with Parse Cloud Functions. + */ +@interface PFCloud : NSObject + +/*! + @abstract Calls the given cloud function *synchronously* with the parameters provided. + + @param function The function name to call. + @param parameters The parameters to send to the function. + + @returns The response from the cloud function. + */ ++ (PF_NULLABLE_S id)callFunction:(NSString *)function withParameters:(PF_NULLABLE NSDictionary *)parameters; + +/*! + @abstract Calls the given cloud function *synchronously* with the parameters provided and + sets the error if there is one. + + @param function The function name to call. + @param parameters The parameters to send to the function. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns The response from the cloud function. + This result could be a `NSDictionary`, an `NSArray`, `NSNumber` or `NSString`. + */ ++ (PF_NULLABLE_S id)callFunction:(NSString *)function + withParameters:(PF_NULLABLE NSDictionary *)parameters + error:(NSError **)error; + +/*! + @abstract Calls the given cloud function *asynchronously* with the parameters provided. + + @param function The function name to call. + @param parameters The parameters to send to the function. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)callFunctionInBackground:(NSString *)function + withParameters:(PF_NULLABLE NSDictionary *)parameters; + +/*! + @abstract Calls the given cloud function *asynchronously* with the parameters provided + and executes the given block when it is done. + + @param function The function name to call. + @param parameters The parameters to send to the function. + @param block The block to execute when the function call finished. + It should have the following argument signature: `^(id result, NSError *error)`. + */ ++ (void)callFunctionInBackground:(NSString *)function + withParameters:(PF_NULLABLE NSDictionary *)parameters + block:(PF_NULLABLE PFIdResultBlock)block; + +/* + @abstract Calls the given cloud function *asynchronously* with the parameters provided + and then executes the given selector when it is done. + + @param function The function name to call. + @param parameters The parameters to send to the function. + @param target The object to call the selector on. + @param selector The selector to call when the function call finished. + It should have the following signature: `(void)callbackWithResult:(id)result error:(NSError *)error`. + Result will be `nil` if error is set and vice versa. + */ ++ (void)callFunctionInBackground:(NSString *)function + withParameters:(PF_NULLABLE NSDictionary *)parameters + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFCloud.m b/Parse/PFCloud.m new file mode 100644 index 000000000..bb019ae06 --- /dev/null +++ b/Parse/PFCloud.m @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCloud.h" + +#import "BFTask+Private.h" +#import "PFCloudCodeController.h" +#import "PFCommandResult.h" +#import "PFCoreManager.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +@implementation PFCloud + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + ++ (id)callFunction:(NSString *)function withParameters:(NSDictionary *)parameters { + return [self callFunction:function withParameters:parameters error:nil]; +} + ++ (id)callFunction:(NSString *)function withParameters:(NSDictionary *)parameters error:(NSError **)error { + return [[self callFunctionInBackground:function withParameters:parameters] waitForResult:error]; +} + ++ (BFTask *)callFunctionInBackground:(NSString *)functionName withParameters:(NSDictionary *)parameters { + return [[PFUser _getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + PFCloudCodeController *controller = [Parse _currentManager].coreManager.cloudCodeController; + return [controller callCloudCodeFunctionAsync:functionName + withParameters:parameters + sessionToken:sessionToken]; + }]; +} + ++ (void)callFunctionInBackground:(NSString *)function + withParameters:(NSDictionary *)parameters + target:(id)target + selector:(SEL)selector { + [self callFunctionInBackground:function withParameters:parameters block:^(id results, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:results object:error]; + }]; +} + ++ (void)callFunctionInBackground:(NSString *)function + withParameters:(NSDictionary *)parameters + block:(PFIdResultBlock)block { + [[self callFunctionInBackground:function withParameters:parameters] thenCallBackOnMainThreadAsync:block]; +} + +@end diff --git a/Parse/PFConfig.h b/Parse/PFConfig.h new file mode 100644 index 000000000..3e4c29a5d --- /dev/null +++ b/Parse/PFConfig.h @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class BFTask; +@class PFConfig; + +typedef void(^PFConfigResultBlock)(PFConfig *PF_NULLABLE_S config, NSError *PF_NULLABLE_S error); + +/*! + `PFConfig` is a representation of the remote configuration object. + It enables you to add things like feature gating, a/b testing or simple "Message of the day". + */ +@interface PFConfig : NSObject + +///-------------------------------------- +/// @name Current Config +///-------------------------------------- + +/*! + @abstract Returns the most recently fetched config. + + @discussion If there was no config fetched - this method will return an empty instance of `PFConfig`. + + @returns Current, last fetched instance of PFConfig. + */ ++ (PFConfig *)currentConfig; + +///-------------------------------------- +/// @name Retrieving Config +///-------------------------------------- + +/*! + @abstract Gets the `PFConfig` object *synchronously* from the server. + + @returns Instance of `PFConfig` if the operation succeeded, otherwise `nil`. + */ ++ (PF_NULLABLE PFConfig *)getConfig; + +/*! + @abstract Gets the `PFConfig` object *synchronously* from the server and sets an error if it occurs. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Instance of PFConfig if the operation succeeded, otherwise `nil`. + */ ++ (PF_NULLABLE PFConfig *)getConfig:(NSError **)error; + +/*! + @abstract Gets the `PFConfig` *asynchronously* and sets it as a result of a task. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)getConfigInBackground; + +/*! + @abstract Gets the `PFConfig` *asynchronously* and executes the given callback block. + + @param block The block to execute. + It should have the following argument signature: `^(PFConfig *config, NSError *error)`. + */ ++ (void)getConfigInBackgroundWithBlock:(PF_NULLABLE PFConfigResultBlock)block; + +///-------------------------------------- +/// @name Parameters +///-------------------------------------- + +/*! + @abstract Returns the object associated with a given key. + + @param key The key for which to return the corresponding configuration value. + + @returns The value associated with `key`, or `nil` if there is no such value. + */ +- (PF_NULLABLE_S id)objectForKey:(NSString *)key; + +/*! + @abstract Returns the object associated with a given key. + + @discussion This method enables usage of literal syntax on `PFConfig`. + E.g. `NSString *value = config[@"key"];` + + @see objectForKey: + + @param keyedSubscript The keyed subscript for which to return the corresponding configuration value. + + @returns The value associated with `key`, or `nil` if there is no such value. + */ +- (PF_NULLABLE_S id)objectForKeyedSubscript:(NSString *)keyedSubscript; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFConfig.m b/Parse/PFConfig.m new file mode 100644 index 000000000..b4d165540 --- /dev/null +++ b/Parse/PFConfig.m @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFConfig.h" + +#import "BFTask+Private.h" +#import "PFConfigController.h" +#import "PFCoreManager.h" +#import "PFCurrentConfigController.h" +#import "PFCurrentUserController.h" +#import "PFInternalUtils.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +NSString *const PFConfigParametersRESTKey = @"params"; + +@interface PFConfig () + +@property (atomic, copy, readwrite) NSDictionary *parametersDictionary; + +@end + +@implementation PFConfig + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (PFConfigController *)_configController { + return [Parse _currentManager].coreManager.configController; +} + +#pragma mark Public + ++ (PFConfig *)currentConfig { + return [[[self _configController].currentConfigController getCurrentConfigAsync] waitForResult:nil + withMainThreadWarning:NO]; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithFetchedConfig:(NSDictionary *)resultDictionary { + self = [self init]; + if (!self) return nil; + + _parametersDictionary = resultDictionary[PFConfigParametersRESTKey]; + + return self; +} + +///-------------------------------------- +#pragma mark - Fetch +///-------------------------------------- + ++ (PFConfig *)getConfig { + return [self getConfig:nil]; +} + ++ (PFConfig *)getConfig:(NSError **)error { + return [[self getConfigInBackground] waitForResult:error]; +} + ++ (BFTask *)getConfigInBackground { + PFCurrentUserController *controller = [Parse _currentManager].coreManager.currentUserController; + return [[controller getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [[self _configController] fetchConfigAsyncWithSessionToken:sessionToken]; + }]; +} + ++ (void)getConfigInBackgroundWithBlock:(PFConfigResultBlock)block { + [[self getConfigInBackground] thenCallBackOnMainThreadAsync:block]; +} + +///-------------------------------------- +#pragma mark - Getting Values +///-------------------------------------- + +- (id)objectForKey:(NSString *)key { + return _parametersDictionary[key]; +} + +- (id)objectForKeyedSubscript:(NSString *)keyedSubscript { + return _parametersDictionary[keyedSubscript]; +} + +#pragma mark Equality Testing + +- (NSUInteger)hash { + return [_parametersDictionary hash]; +} + +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[PFConfig class]]) { + PFConfig *other = object; + + // Compare pointers first, to account for nil dictionary + return self.parametersDictionary == other.parametersDictionary || + [self.parametersDictionary isEqual:other.parametersDictionary]; + } + + return NO; +} + +@end diff --git a/Parse/PFConstants.h b/Parse/PFConstants.h new file mode 100644 index 000000000..6a368b418 --- /dev/null +++ b/Parse/PFConstants.h @@ -0,0 +1,426 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@class PFObject; +@class PFUser; + +///-------------------------------------- +/// @name Version +///-------------------------------------- + +#define PARSE_VERSION @"1.8.0" + +extern NSInteger const PARSE_API_VERSION; + +///-------------------------------------- +/// @name Platform +///-------------------------------------- + +#define PARSE_IOS_ONLY (TARGET_OS_IPHONE) +#define PARSE_OSX_ONLY (TARGET_OS_MAC && !(TARGET_OS_IPHONE)) + +extern NSString *const PF_NONNULL_S kPFDeviceType; + +#if PARSE_IOS_ONLY +#import +#else +#import +#endif + +///-------------------------------------- +/// @name Server +///-------------------------------------- + +extern NSString *const PF_NONNULL_S kPFParseServer; + +///-------------------------------------- +/// @name Cache Policies +///-------------------------------------- + +/*! + `PFCachePolicy` specifies different caching policies that could be used with . + + This lets you show data when the user's device is offline, + or when the app has just started and network requests have not yet had time to complete. + Parse takes care of automatically flushing the cache when it takes up too much space. + + @warning Cache policy could only be set when Local Datastore is not enabled. + + @see PFQuery + */ +typedef NS_ENUM(uint8_t, PFCachePolicy) { + /*! + @abstract The query does not load from the cache or save results to the cache. + This is the default cache policy. + */ + kPFCachePolicyIgnoreCache = 0, + /*! + @abstract The query only loads from the cache, ignoring the network. + If there are no cached results, this causes a `NSError` with `kPFErrorCacheMiss` code. + */ + kPFCachePolicyCacheOnly, + /*! + @abstract The query does not load from the cache, but it will save results to the cache. + */ + kPFCachePolicyNetworkOnly, + /*! + @abstract The query first tries to load from the cache, but if that fails, it loads results from the network. + If there are no cached results, this causes a `NSError` with `kPFErrorCacheMiss` code. + */ + kPFCachePolicyCacheElseNetwork, + /*! + @abstract The query first tries to load from the network, but if that fails, it loads results from the cache. + If there are no cached results, this causes a `NSError` with `kPFErrorCacheMiss` code. + */ + kPFCachePolicyNetworkElseCache, + /*! + @abstract The query first loads from the cache, then loads from the network. + The callback will be called twice - first with the cached results, then with the network results. + Since it returns two results at different times, this cache policy cannot be used with synchronous or task methods. + */ + kPFCachePolicyCacheThenNetwork +}; + +///-------------------------------------- +/// @name Logging Levels +///-------------------------------------- + +/*! + `PFLogLevel` enum specifies different levels of logging that could be used to limit or display more messages in logs. + + @see [Parse setLogLevel:] + @see [Parse logLevel] + */ +typedef NS_ENUM(uint8_t, PFLogLevel) { + /*! + Log level that disables all logging. + */ + PFLogLevelNone = 0, + /*! + Log level that if set is going to output error messages to the log. + */ + PFLogLevelError = 1, + /*! + Log level that if set is going to output the following messages to log: + - Errors + - Warnings + */ + PFLogLevelWarning = 2, + /*! + Log level that if set is going to output the following messages to log: + - Errors + - Warnings + - Informational messages + */ + PFLogLevelInfo = 3, + /*! + Log level that if set is going to output the following messages to log: + - Errors + - Warnings + - Informational messages + - Debug messages + */ + PFLogLevelDebug = 4 +}; + +///-------------------------------------- +/// @name Errors +///-------------------------------------- + +extern NSString *const PF_NONNULL_S PFParseErrorDomain; + +/*! + `PFErrorCode` enum contains all custom error codes that are used as `code` for `NSError` for callbacks on all classes. + + These codes are used when `domain` of `NSError` that you receive is set to `PFParseErrorDomain`. + */ +typedef NS_ENUM(NSInteger, PFErrorCode) { + /*! + @abstract Internal server error. No information available. + */ + kPFErrorInternalServer = 1, + /*! + @abstract The connection to the Parse servers failed. + */ + kPFErrorConnectionFailed = 100, + /*! + @abstract Object doesn't exist, or has an incorrect password. + */ + kPFErrorObjectNotFound = 101, + /*! + @abstract You tried to find values matching a datatype that doesn't + support exact database matching, like an array or a dictionary. + */ + kPFErrorInvalidQuery = 102, + /*! + @abstract Missing or invalid classname. Classnames are case-sensitive. + They must start with a letter, and `a-zA-Z0-9_` are the only valid characters. + */ + kPFErrorInvalidClassName = 103, + /*! + @abstract Missing object id. + */ + kPFErrorMissingObjectId = 104, + /*! + @abstract Invalid key name. Keys are case-sensitive. + They must start with a letter, and `a-zA-Z0-9_` are the only valid characters. + */ + kPFErrorInvalidKeyName = 105, + /*! + @abstract Malformed pointer. Pointers must be arrays of a classname and an object id. + */ + kPFErrorInvalidPointer = 106, + /*! + @abstract Malformed json object. A json dictionary is expected. + */ + kPFErrorInvalidJSON = 107, + /*! + @abstract Tried to access a feature only available internally. + */ + kPFErrorCommandUnavailable = 108, + /*! + @abstract Field set to incorrect type. + */ + kPFErrorIncorrectType = 111, + /*! + @abstract Invalid channel name. A channel name is either an empty string (the broadcast channel) + or contains only `a-zA-Z0-9_` characters and starts with a letter. + */ + kPFErrorInvalidChannelName = 112, + /*! + @abstract Invalid device token. + */ + kPFErrorInvalidDeviceToken = 114, + /*! + @abstract Push is misconfigured. See details to find out how. + */ + kPFErrorPushMisconfigured = 115, + /*! + @abstract The object is too large. + */ + kPFErrorObjectTooLarge = 116, + /*! + @abstract That operation isn't allowed for clients. + */ + kPFErrorOperationForbidden = 119, + /*! + @abstract The results were not found in the cache. + */ + kPFErrorCacheMiss = 120, + /*! + @abstract Keys in `NSDictionary` values may not include `$` or `.`. + */ + kPFErrorInvalidNestedKey = 121, + /*! + @abstract Invalid file name. + A file name can contain only `a-zA-Z0-9_.` characters and should be between 1 and 36 characters. + */ + kPFErrorInvalidFileName = 122, + /*! + @abstract Invalid ACL. An ACL with an invalid format was saved. This should not happen if you use . + */ + kPFErrorInvalidACL = 123, + /*! + @abstract The request timed out on the server. Typically this indicates the request is too expensive. + */ + kPFErrorTimeout = 124, + /*! + @abstract The email address was invalid. + */ + kPFErrorInvalidEmailAddress = 125, + /*! + A unique field was given a value that is already taken. + */ + kPFErrorDuplicateValue = 137, + /*! + @abstract Role's name is invalid. + */ + kPFErrorInvalidRoleName = 139, + /*! + @abstract Exceeded an application quota. Upgrade to resolve. + */ + kPFErrorExceededQuota = 140, + /*! + @abstract Cloud Code script had an error. + */ + kPFScriptError = 141, + /*! + @abstract Cloud Code validation failed. + */ + kPFValidationError = 142, + /*! + @abstract Product purchase receipt is missing. + */ + kPFErrorReceiptMissing = 143, + /*! + @abstract Product purchase receipt is invalid. + */ + kPFErrorInvalidPurchaseReceipt = 144, + /*! + @abstract Payment is disabled on this device. + */ + kPFErrorPaymentDisabled = 145, + /*! + @abstract The product identifier is invalid. + */ + kPFErrorInvalidProductIdentifier = 146, + /*! + @abstract The product is not found in the App Store. + */ + kPFErrorProductNotFoundInAppStore = 147, + /*! + @abstract The Apple server response is not valid. + */ + kPFErrorInvalidServerResponse = 148, + /*! + @abstract Product fails to download due to file system error. + */ + kPFErrorProductDownloadFileSystemFailure = 149, + /*! + @abstract Fail to convert data to image. + */ + kPFErrorInvalidImageData = 150, + /*! + @abstract Unsaved file. + */ + kPFErrorUnsavedFile = 151, + /*! + @abstract Fail to delete file. + */ + kPFErrorFileDeleteFailure = 153, + /*! + @abstract Application has exceeded its request limit. + */ + kPFErrorRequestLimitExceeded = 155, + /*! + @abstract Invalid event name. + */ + kPFErrorInvalidEventName = 160, + /*! + @abstract Username is missing or empty. + */ + kPFErrorUsernameMissing = 200, + /*! + @abstract Password is missing or empty. + */ + kPFErrorUserPasswordMissing = 201, + /*! + @abstract Username has already been taken. + */ + kPFErrorUsernameTaken = 202, + /*! + @abstract Email has already been taken. + */ + kPFErrorUserEmailTaken = 203, + /*! + @abstract The email is missing, and must be specified. + */ + kPFErrorUserEmailMissing = 204, + /*! + @abstract A user with the specified email was not found. + */ + kPFErrorUserWithEmailNotFound = 205, + /*! + @abstract The user cannot be altered by a client without the session. + */ + kPFErrorUserCannotBeAlteredWithoutSession = 206, + /*! + @abstract Users can only be created through sign up. + */ + kPFErrorUserCanOnlyBeCreatedThroughSignUp = 207, + /*! + @abstract An existing Facebook account already linked to another user. + */ + kPFErrorFacebookAccountAlreadyLinked = 208, + /*! + @abstract An existing account already linked to another user. + */ + kPFErrorAccountAlreadyLinked = 208, + /*! + Error code indicating that the current session token is invalid. + */ + kPFErrorInvalidSessionToken = 209, + kPFErrorUserIdMismatch = 209, + /*! + @abstract Facebook id missing from request. + */ + kPFErrorFacebookIdMissing = 250, + /*! + @abstract Linked id missing from request. + */ + kPFErrorLinkedIdMissing = 250, + /*! + @abstract Invalid Facebook session. + */ + kPFErrorFacebookInvalidSession = 251, + /*! + @abstract Invalid linked session. + */ + kPFErrorInvalidLinkedSession = 251, +}; + +///-------------------------------------- +/// @name Blocks +///-------------------------------------- + +typedef void (^PFBooleanResultBlock)(BOOL succeeded, NSError *PF_NULLABLE_S error); +typedef void (^PFIntegerResultBlock)(int number, NSError *PF_NULLABLE_S error); +typedef void (^PFArrayResultBlock)(NSArray *PF_NULLABLE_S objects, NSError *PF_NULLABLE_S error); +typedef void (^PFObjectResultBlock)(PFObject *PF_NULLABLE_S object, NSError *PF_NULLABLE_S error); +typedef void (^PFSetResultBlock)(NSSet *PF_NULLABLE_S channels, NSError *PF_NULLABLE_S error); +typedef void (^PFUserResultBlock)(PFUser *PF_NULLABLE_S user, NSError *PF_NULLABLE_S error); +typedef void (^PFDataResultBlock)(NSData *PF_NULLABLE_S data, NSError *PF_NULLABLE_S error); +typedef void (^PFDataStreamResultBlock)(NSInputStream *PF_NULLABLE_S stream, NSError *PF_NULLABLE_S error); +typedef void (^PFStringResultBlock)(NSString *PF_NULLABLE_S string, NSError *PF_NULLABLE_S error); +typedef void (^PFIdResultBlock)(PF_NULLABLE_S id object, NSError *PF_NULLABLE_S error); +typedef void (^PFProgressBlock)(int percentDone); + +///-------------------------------------- +/// @name Deprecated Macros +///-------------------------------------- + +#ifndef PARSE_DEPRECATED +# ifdef __deprecated_msg +# define PARSE_DEPRECATED(_MSG) __deprecated_msg(_MSG) +# else +# ifdef __deprecated +# define PARSE_DEPRECATED(_MSG) __attribute__((deprecated)) +# else +# define PARSE_DEPRECATED(_MSG) +# endif +# endif +#endif + +///-------------------------------------- +/// @name Extensions Macros +///-------------------------------------- + +#ifndef PF_EXTENSION_UNAVAILABLE +# if PARSE_IOS_ONLY +# ifdef NS_EXTENSION_UNAVAILABLE_IOS +# define PF_EXTENSION_UNAVAILABLE(_msg) NS_EXTENSION_UNAVAILABLE_IOS(_msg) +# else +# define PF_EXTENSION_UNAVAILABLE(_msg) +# endif +# else +# ifdef NS_EXTENSION_UNAVAILABLE_MAC +# define PF_EXTENSION_UNAVAILABLE(_msg) NS_EXTENSION_UNAVAILABLE_MAC(_msg) +# else +# define PF_EXTENSION_UNAVAILABLE(_msg) +# endif +# endif +#endif diff --git a/Parse/PFConstants.m b/Parse/PFConstants.m new file mode 100644 index 000000000..78b8b7ecf --- /dev/null +++ b/Parse/PFConstants.m @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFConstants.h" + +NSInteger const PARSE_API_VERSION = 2; + +#if PARSE_IOS_ONLY +NSString *const kPFDeviceType = @"ios"; +#else +NSString *const kPFDeviceType = @"osx"; +#endif + +NSString *const kPFParseServer = @"https://api.parse.com"; + +NSString *const PFParseErrorDomain = @"Parse"; diff --git a/Parse/PFFile.h b/Parse/PFFile.h new file mode 100644 index 000000000..62ac01729 --- /dev/null +++ b/Parse/PFFile.h @@ -0,0 +1,392 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class BFTask; + +/*! + `PFFile` representes a file of binary data stored on the Parse servers. + This can be a image, video, or anything else that an application needs to reference in a non-relational way. + */ +@interface PFFile : NSObject + +///-------------------------------------- +/// @name Creating a PFFile +///-------------------------------------- + +/*! + @abstract Creates a file with given data. A name will be assigned to it by the server. + + @param data The contents of the new `PFFile`. + + @returns A new `PFFile`. + */ ++ (instancetype)fileWithData:(NSData *)data; + +/*! + @abstract Creates a file with given data and name. + + @param name The name of the new PFFile. The file name must begin with and + alphanumeric character, and consist of alphanumeric characters, periods, + spaces, underscores, or dashes. + @param data The contents of the new `PFFile`. + + @returns A new `PFFile` object. + */ ++ (instancetype)fileWithName:(PF_NULLABLE NSString *)name data:(NSData *)data; + +/*! + @abstract Creates a file with the contents of another file. + + @warning This method raises an exception if the file at path is not accessible + or if there is not enough disk space left. + + @param name The name of the new `PFFile`. The file name must begin with and alphanumeric character, + and consist of alphanumeric characters, periods, spaces, underscores, or dashes. + @param path The path to the file that will be uploaded to Parse. + + @returns A new `PFFile` instance. + */ ++ (instancetype)fileWithName:(PF_NULLABLE NSString *)name contentsAtPath:(NSString *)path; + +/*! + @abstract Creates a file with the contents of another file. + + @param name The name of the new `PFFile`. The file name must begin with and alphanumeric character, + and consist of alphanumeric characters, periods, spaces, underscores, or dashes. + @param path The path to the file that will be uploaded to Parse. + @param error On input, a pointer to an error object. + If an error occurs, this pointer is set to an actual error object containing the error information. + You may specify `nil` for this parameter if you do not want the error information. + + @returns A new `PFFile` instance or `nil` if the error occured. + */ ++ (instancetype)fileWithName:(PF_NULLABLE NSString *)name contentsAtPath:(NSString *)path error:(NSError **)error; + +/*! + @abstract Creates a file with given data, name and content type. + + @warning This method raises an exception if the data supplied is not accessible or could not be saved. + + @param name The name of the new `PFFile`. The file name must begin with and alphanumeric character, + and consist of alphanumeric characters, periods, spaces, underscores, or dashes. + @param data The contents of the new `PFFile`. + @param contentType Represents MIME type of the data. + + @returns A new `PFFile` instance. + */ ++ (instancetype)fileWithName:(PF_NULLABLE NSString *)name + data:(NSData *)data + contentType:(PF_NULLABLE NSString *)contentType; + +/*! + @abstract Creates a file with given data, name and content type. + + @param name The name of the new `PFFile`. The file name must begin with and alphanumeric character, + and consist of alphanumeric characters, periods, spaces, underscores, or dashes. + @param data The contents of the new `PFFile`. + @param contentType Represents MIME type of the data. + @param error On input, a pointer to an error object. + If an error occurs, this pointer is set to an actual error object containing the error information. + You may specify `nil` for this parameter if you do not want the error information. + + @returns A new `PFFile` instance or `nil` if the error occured. + */ ++ (instancetype)fileWithName:(PF_NULLABLE NSString *)name + data:(NSData *)data + contentType:(PF_NULLABLE NSString *)contentType + error:(NSError **)error; + +/*! + @abstract Creates a file with given data and content type. + + @param data The contents of the new `PFFile`. + @param contentType Represents MIME type of the data. + + @returns A new `PFFile` object. + */ ++ (instancetype)fileWithData:(NSData *)data contentType:(PF_NULLABLE NSString *)contentType; + +/*! + @abstract The name of the file. + + @discussion Before the file is saved, this is the filename given by + the user. After the file is saved, that name gets prefixed with a unique + identifier. + */ +@property (nonatomic, copy, readonly) NSString *name; + +/*! + @abstract The url of the file. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, copy, readonly) NSString *url; + +///-------------------------------------- +/// @name Storing Data with Parse +///-------------------------------------- + +/*! + @abstract Whether the file has been uploaded for the first time. + */ +@property (nonatomic, assign, readonly) BOOL isDirty; + +/*! + @abstract Saves the file *synchronously*. + + @returns Returns whether the save succeeded. + */ +- (BOOL)save; + +/*! + @abstract Saves the file *synchronously* and sets an error if it occurs. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the save succeeded. + */ +- (BOOL)save:(NSError **)error; + +/*! + @abstract Saves the file *asynchronously*. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)saveInBackground; + +/*! + @abstract Saves the file *asynchronously* + + @param progressBlock The block should have the following argument signature: `^(int percentDone)` + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)saveInBackgroundWithProgressBlock:(PF_NULLABLE PFProgressBlock)progressBlock; + +/*! + @abstract Saves the file *asynchronously* and executes the given block. + + @param block The block should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ +- (void)saveInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract Saves the file *asynchronously* and executes the given block. + + @discussion This method will execute the progressBlock periodically with the percent progress. + `progressBlock` will get called with `100` before `resultBlock` is called. + + @param block The block should have the following argument signature: `^(BOOL succeeded, NSError *error)` + @param progressBlock The block should have the following argument signature: `^(int percentDone)` + */ +- (void)saveInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block + progressBlock:(PF_NULLABLE PFProgressBlock)progressBlock; + +/* + @abstract Saves the file *asynchronously* and calls the given callback. + + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ +- (void)saveInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Getting Data from Parse +///-------------------------------------- + +/*! + @abstract Whether the data is available in memory or needs to be downloaded. + */ +@property (assign, readonly) BOOL isDataAvailable; + +/*! + @abstract *Synchronously* gets the data from cache if available or fetches its contents from the network. + + @returns The `NSData` object containing file data. Returns `nil` if there was an error in fetching. + */ +- (PF_NULLABLE NSData *)getData; + +/*! + @abstract This method is like but avoids ever holding the entire `PFFile` contents in memory at once. + + @discussion This can help applications with many large files avoid memory warnings. + + @returns A stream containing the data. Returns `nil` if there was an error in fetching. + */ +- (PF_NULLABLE NSInputStream *)getDataStream; + +/*! + @abstract *Synchronously* gets the data from cache if available or fetches its contents from the network. + Sets an error if it occurs. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns The `NSData` object containing file data. Returns `nil` if there was an error in fetching. + */ +- (PF_NULLABLE NSData *)getData:(NSError **)error; + +/*! + @abstract This method is like but avoids ever holding the entire `PFFile` contents in memory at once. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns A stream containing the data. Returns nil if there was an error in + fetching. + */ +- (PF_NULLABLE NSInputStream *)getDataStream:(NSError **)error; + +/*! + @abstract This method is like but it fetches asynchronously to avoid blocking the current thread. + + @see getData + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)getDataInBackground; + +/*! + @abstract This method is like but it fetches asynchronously to avoid blocking the current thread. + + @discussion This can help applications with many large files avoid memory warnings. + + @see getData + + @param progressBlock The block should have the following argument signature: ^(int percentDone) + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)getDataInBackgroundWithProgressBlock:(PF_NULLABLE PFProgressBlock)progressBlock; + +/*! + @abstract This method is like but avoids + ever holding the entire `PFFile` contents in memory at once. + + @discussion This can help applications with many large files avoid memory warnings. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)getDataStreamInBackground; + +/*! + @abstract This method is like , but yields a live-updating stream. + + @discussion Instead of , which yields a stream that can be read from only after the request has + completed, this method gives you a stream directly written to by the HTTP session. As this stream is not pre-buffered, + it is strongly advised to use the `NSStreamDelegate` methods, in combination with a run loop, to consume the data in + the stream, to do proper async file downloading. + + @note You MUST open this stream before reading from it. + @note Do NOT call on this task from the main thread. It may result in a deadlock. + + @returns A task that produces a *live* stream that is being written to with the data from the server. + */ +- (BFTask *)getDataDownloadStreamInBackground; + +/*! + @abstract This method is like but avoids + ever holding the entire `PFFile` contents in memory at once. + + @discussion This can help applications with many large files avoid memory warnings. + @param progressBlock The block should have the following argument signature: ^(int percentDone) + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)getDataStreamInBackgroundWithProgressBlock:(PF_NULLABLE PFProgressBlock)progressBlock; + +/*! + @abstract This method is like , but yields a live-updating stream. + + @discussion Instead of , which yields a stream that can be read from only after the request has + completed, this method gives you a stream directly written to by the HTTP session. As this stream is not pre-buffered, + it is strongly advised to use the `NSStreamDelegate` methods, in combination with a run loop, to consume the data in + the stream, to do proper async file downloading. + + @note You MUST open this stream before reading from it. + @note Do NOT call on this task from the main thread. It may result in a deadlock. + + @param progressBlock The block should have the following argument signature: `^(int percentDone)` + + @returns A task that produces a *live* stream that is being written to with the data from the server. + */ +- (BFTask *)getDataDownloadStreamInBackgroundWithProgressBlock:(PF_NULLABLE PFProgressBlock)progressBlock; + +/*! + @abstract *Asynchronously* gets the data from cache if available or fetches its contents from the network. + + @param block The block should have the following argument signature: `^(NSData *result, NSError *error)` + */ +- (void)getDataInBackgroundWithBlock:(PF_NULLABLE PFDataResultBlock)block; + +/*! + @abstract This method is like but avoids + ever holding the entire `PFFile` contents in memory at once. + + @discussion This can help applications with many large files avoid memory warnings. + + @param block The block should have the following argument signature: `(NSInputStream *result, NSError *error)` + */ +- (void)getDataStreamInBackgroundWithBlock:(PF_NULLABLE PFDataStreamResultBlock)block; + +/*! + @abstract *Asynchronously* gets the data from cache if available or fetches its contents from the network. + + @discussion This method will execute the progressBlock periodically with the percent progress. + `progressBlock` will get called with `100` before `resultBlock` is called. + + @param resultBlock The block should have the following argument signature: ^(NSData *result, NSError *error) + @param progressBlock The block should have the following argument signature: ^(int percentDone) + */ +- (void)getDataInBackgroundWithBlock:(PF_NULLABLE PFDataResultBlock)resultBlock + progressBlock:(PF_NULLABLE PFProgressBlock)progressBlock; + +/*! + @abstract This method is like but avoids + ever holding the entire `PFFile` contents in memory at once. + + @discussion This can help applications with many large files avoid memory warnings. + + @param resultBlock The block should have the following argument signature: `^(NSInputStream *result, NSError *error)`. + @param progressBlock The block should have the following argument signature: `^(int percentDone)`. + */ +- (void)getDataStreamInBackgroundWithBlock:(PF_NULLABLE PFDataStreamResultBlock)resultBlock + progressBlock:(PF_NULLABLE PFProgressBlock)progressBlock; + +/* + @abstract *Asynchronously* gets the data from cache if available or fetches its contents from the network. + + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSData *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + */ +- (void)getDataInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Interrupting a Transfer +///-------------------------------------- + +/*! + @abstract Cancels the current request (upload or download of file). + */ +- (void)cancel; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFFile.m b/Parse/PFFile.m new file mode 100644 index 000000000..1f2db855f --- /dev/null +++ b/Parse/PFFile.m @@ -0,0 +1,509 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFile.h" +#import "PFFile_Private.h" + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFAsyncTaskQueue.h" +#import "PFBlockRetryer.h" +#import "PFCommandResult.h" +#import "PFCoreManager.h" +#import "PFFileController.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFMacros.h" +#import "PFMutableFileState.h" +#import "PFRESTFileCommand.h" +#import "PFThreadsafety.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +static const unsigned long long PFFileMaxFileSize = 10 * 1024 * 1024; // 10 MB + +@interface PFFile () { + dispatch_queue_t _synchronizationQueue; +} + +@property (nonatomic, strong, readwrite) PFFileState *state; +@property (nonatomic, copy, readonly) NSString *stagedFilePath; +@property (nonatomic, assign, readonly, getter=isDirty) BOOL dirty; + +// +// Private +@property (nonatomic, strong) PFAsyncTaskQueue *taskQueue; +@property (nonatomic, strong) BFCancellationTokenSource *cancellationTokenSource; + +@end + +@implementation PFFile + +@synthesize stagedFilePath=_stagedFilePath; + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + +#pragma mark Init + ++ (instancetype)fileWithData:(NSData *)data { + return [self fileWithName:nil data:data contentType:nil]; +} + ++ (instancetype)fileWithName:(NSString *)name data:(NSData *)data { + return [self fileWithName:name data:data contentType:nil]; +} + ++ (instancetype)fileWithName:(NSString *)name contentsAtPath:(NSString *)path { + NSError *error = nil; + PFFile *file = [self fileWithName:name contentsAtPath:path error:&error]; + PFParameterAssert(!error, @"Could not access file at %@: %@", path, error); + return file; +} + ++ (instancetype)fileWithName:(NSString *)name contentsAtPath:(NSString *)path error:(NSError **)error { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL directory = NO; + PFParameterAssert([fileManager fileExistsAtPath:path isDirectory:&directory] && !directory, + @"%@ is not a valid file path for a PFFile.", path); + + NSDictionary *attributess = [fileManager attributesOfItemAtPath:path error:nil]; + unsigned long long length = [attributess[NSFileSize] unsignedLongValue]; + PFParameterAssert(length <= PFFileMaxFileSize, @"PFFile cannot be larger than %lli bytes", PFFileMaxFileSize); + + PFFile *file = [self fileWithName:name url:nil]; + if (file) { + // Copy the file write away, since we can construct staged file path only from a PFFile. + NSError *copyError = nil; + [fileManager copyItemAtPath:path toPath:file.stagedFilePath error:©Error]; + if (copyError) { + if (error) { + *error = copyError; + } + return nil; + } + } + return file; +} + ++ (instancetype)fileWithName:(NSString *)name + data:(NSData *)data + contentType:(NSString *)contentType { + NSError *error = nil; + PFFile *file = [self fileWithName:name data:data contentType:contentType error:&error]; + PFConsistencyAssert(!error, @"Could not save file data for %@ : %@", name, error); + return file; +} + ++ (instancetype)fileWithName:(NSString *)name + data:(NSData *)data + contentType:(NSString *)contentType + error:(NSError **)error { + PFParameterAssert([data length] <= PFFileMaxFileSize, + @"PFFile cannot be larger than %llu bytes", PFFileMaxFileSize); + + PFFile *file = [[self alloc] initWithName:name urlString:nil mimeType:contentType]; + + // Save the file write away, since we can construct staged file path only from a PFFile. + NSError *writeError = nil; + [[PFFileManager writeDataAsync:data toFile:file.stagedFilePath] + waitForResult:&writeError + withMainThreadWarning:NO]; + + if (writeError) { + if (error) { + *error = writeError; + } + return nil; + } + return file; +} + ++ (instancetype)fileWithData:(NSData *)data contentType:(NSString *)contentType { + return [self fileWithName:nil data:data contentType:contentType]; +} + +#pragma mark Dealloc + +- (void)dealloc { +#if !OS_OBJECT_USE_OBJC + dispatch_release(_synchronizationQueue); +#endif +} + +#pragma mark Uploading + +- (BOOL)save { + return [self save:nil]; +} + +- (BOOL)save:(NSError **)error { + return [[[self saveInBackground] waitForResult:error] boolValue]; +} + +- (BFTask *)saveInBackground { + return [self _uploadAsyncWithProgressBlock:nil]; +} + +- (BFTask *)saveInBackgroundWithProgressBlock:(PFProgressBlock)progressBlock { + return [self _uploadAsyncWithProgressBlock:progressBlock]; +} + +- (void)saveInBackgroundWithBlock:(PFBooleanResultBlock)block { + [[self saveInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +- (void)saveInBackgroundWithBlock:(PFBooleanResultBlock)block + progressBlock:(PFProgressBlock)progressBlock { + [[self _uploadAsyncWithProgressBlock:progressBlock] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +- (void)saveInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +#pragma mark Downloading + +- (NSData *)getData { + return [self getData:nil]; +} + +- (NSInputStream *)getDataStream { + return [self getDataStream:nil]; +} + +- (NSData *)getData:(NSError **)error { + return [[self getDataInBackground] waitForResult:error]; +} + +- (NSInputStream *)getDataStream:(NSError **)error { + return [[self getDataStreamInBackground] waitForResult:error]; +} + +- (BFTask *)getDataInBackground { + return [self _getDataAsyncWithProgressBlock:nil]; +} + +- (BFTask *)getDataInBackgroundWithProgressBlock:(PFProgressBlock)progressBlock { + return [self _getDataAsyncWithProgressBlock:progressBlock]; +} + +- (BFTask *)getDataStreamInBackground { + return [self _getDataStreamAsyncWithProgressBlock:nil]; +} + +- (BFTask *)getDataStreamInBackgroundWithProgressBlock:(PFProgressBlock)progressBlock { + return [self _getDataStreamAsyncWithProgressBlock:progressBlock]; +} + +- (BFTask *)getDataDownloadStreamInBackground { + return [self getDataDownloadStreamInBackgroundWithProgressBlock:nil]; +} + +- (BFTask *)getDataDownloadStreamInBackgroundWithProgressBlock:(PFProgressBlock)progressBlock { + return [self _downloadStreamAsyncWithProgressBlock:progressBlock]; +} + +- (void)getDataInBackgroundWithBlock:(PFDataResultBlock)block { + [self getDataInBackgroundWithBlock:block progressBlock:nil]; +} + +- (void)getDataStreamInBackgroundWithBlock:(PFDataStreamResultBlock)block { + [self getDataStreamInBackgroundWithBlock:block progressBlock:nil]; +} + +- (void)getDataInBackgroundWithBlock:(PFDataResultBlock)resultBlock + progressBlock:(PFProgressBlock)progressBlock { + [[self _getDataAsyncWithProgressBlock:progressBlock] thenCallBackOnMainThreadAsync:resultBlock]; +} + +- (void)getDataStreamInBackgroundWithBlock:(PFDataStreamResultBlock)resultBlock + progressBlock:(PFProgressBlock)progressBlock { + [[self _getDataStreamAsyncWithProgressBlock:progressBlock] thenCallBackOnMainThreadAsync:resultBlock]; +} + +- (void)getDataInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self getDataInBackgroundWithBlock:^(NSData *data, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:data object:error]; + }]; +} + +#pragma mark Interrupting + +- (void)cancel { + [self _performDataAccessBlock:^{ + [self.cancellationTokenSource cancel]; + self.cancellationTokenSource = nil; + }]; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +#pragma mark Init + +- (instancetype)initWithName:(NSString *)name urlString:(NSString *)url mimeType:(NSString *)mimeType { + self = [super init]; + if (!self) return nil; + + _taskQueue = [[PFAsyncTaskQueue alloc] init]; + _synchronizationQueue = PFThreadsafetyCreateQueueForObject(self); + + _state = [[PFFileState alloc] initWithName:name urlString:url mimeType:mimeType]; + + return self; +} + ++ (instancetype)fileWithName:(NSString *)name url:(NSString *)url { + return [[self alloc] initWithName:name urlString:url mimeType:nil]; +} + +#pragma mark Upload + +- (BFTask *)_uploadAsyncWithProgressBlock:(PFProgressBlock)progressBlock { + @weakify(self); + return [BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id { + @strongify(self); + + __block BFCancellationToken *cancellationToken = nil; + [self _performDataAccessBlock:^{ + if (!self.cancellationTokenSource || self.cancellationTokenSource.cancellationRequested) { + self.cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + } + cancellationToken = self.cancellationTokenSource.token; + }]; + + return [[[PFUser _getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [self.taskQueue enqueue:^id(BFTask *task) { + if (!self.dirty) { + [self _performProgressBlockAsync:progressBlock withProgress:100]; + return [BFTask taskWithResult:nil]; + } + + return [self _uploadFileAsyncWithSessionToken:sessionToken + cancellationToken:cancellationToken + progressBlock:progressBlock]; + }]; + }] continueWithSuccessResult:@YES]; + }]; +} + +- (BFTask *)_uploadFileAsyncWithSessionToken:(NSString *)sessionToken + cancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock { + if (cancellationToken.cancellationRequested) { + return [BFTask cancelledTask]; + } + + PFFileController *controller = [[self class] fileController]; + NSString *sourceFilePath = self.stagedFilePath; + @weakify(self); + return [[[controller uploadFileAsyncWithState:[self _fileState] + sourceFilePath:sourceFilePath + sessionToken:sessionToken + cancellationToken:cancellationToken + progressBlock:progressBlock] continueWithSuccessBlock:^id(BFTask *task) { + @strongify(self); + [self _performDataAccessBlock:^{ + self.state = [task.result copy]; + }]; + return nil; + } cancellationToken:cancellationToken] continueWithBlock:^id(BFTask *task) { + @strongify(self); + [self _performDataAccessBlock:^{ + self.cancellationTokenSource = nil; + }]; + return task; + }]; +} + +#pragma mark Download + +- (BFTask *)_getDataAsyncWithProgressBlock:(PFProgressBlock)progressBlock { + return [[self _downloadAsyncWithProgressBlock:progressBlock] continueAsyncWithSuccessBlock:^id(BFTask *task) { + return [self _cachedData]; + }]; +} + +- (BFTask *)_getDataStreamAsyncWithProgressBlock:(PFProgressBlock)progressBlock { + return [[self _downloadAsyncWithProgressBlock:progressBlock] continueAsyncWithSuccessBlock:^id(BFTask *task) { + return [self _cachedDataStream]; + }]; +} + +- (BFTask *)_downloadAsyncWithProgressBlock:(PFProgressBlock)progressBlock { + __block BFCancellationToken *cancellationToken = nil; + [self _performDataAccessBlock:^{ + if (!self.cancellationTokenSource || self.cancellationTokenSource.cancellationRequested) { + self.cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + } + cancellationToken = self.cancellationTokenSource.token; + }]; + + return [self _downloadAsyncWithCancellationToken:cancellationToken progressBlock:progressBlock]; +} + +- (BFTask *)_downloadStreamAsyncWithProgressBlock:(PFProgressBlock)progressBlock { + __block BFCancellationToken *cancellationToken = nil; + [self _performDataAccessBlock:^{ + if (!self.cancellationTokenSource || self.cancellationTokenSource.cancellationRequested) { + self.cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + } + cancellationToken = self.cancellationTokenSource.token; + }]; + + return [self _downloadStreamAsyncWithCancellationToken:cancellationToken progressBlock:progressBlock]; +} + +- (BFTask *)_downloadAsyncWithCancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock { + @weakify(self); + return [self.taskQueue enqueue:^id(BFTask *task) { + @strongify(self); + if (self.isDataAvailable) { + [self _performProgressBlockAsync:progressBlock withProgress:100]; + return [BFTask taskWithResult:nil]; + } + + PFFileController *controller = [[self class] fileController]; + return [[controller downloadFileAsyncWithState:[self _fileState] + cancellationToken:cancellationToken + progressBlock:progressBlock] continueWithBlock:^id(BFTask *task) { + [self _performDataAccessBlock:^{ + self.cancellationTokenSource = nil; + }]; + return task; + }]; + }]; +} + +- (BFTask *)_downloadStreamAsyncWithCancellationToken:(BFCancellationToken *)cancellationToken + progressBlock:(PFProgressBlock)progressBlock { + @weakify(self); + return [self.taskQueue enqueue:^id(BFTask *task) { + @strongify(self); + if (self.isDataAvailable) { + [self _performProgressBlockAsync:progressBlock withProgress:100]; + return [self _cachedDataStream]; + } + + PFFileController *controller = [[self class] fileController]; + return [[controller downloadFileStreamAsyncWithState:[self _fileState] + cancellationToken:cancellationToken + progressBlock:progressBlock] continueWithBlock:^id(BFTask *task) { + [self _performDataAccessBlock:^{ + self.cancellationTokenSource = nil; + }]; + return task; + }]; + }]; +} + +#pragma mark Caching + +- (NSString *)_cachedFilePath { + return [[[self class] fileController] cachedFilePathForFileState:self.state]; +} + +- (NSData *)_cachedData { + NSString *filePath = (self.dirty ? self.stagedFilePath : [self _cachedFilePath]); + return [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:NULL]; +} + +- (NSInputStream *)_cachedDataStream { + NSString *filePath = (self.dirty ? self.stagedFilePath : [[[self class] fileController] cachedFilePathForFileState:self.state]); + return [NSInputStream inputStreamWithFileAtPath:filePath]; +} + +#pragma mark Data Access + +- (NSString *)name { + __block NSString *name = nil; + [self _performDataAccessBlock:^{ + name = self.state.name; + }]; + return name; +} + +- (NSString *)url { + __block NSString *url = nil; + [self _performDataAccessBlock:^{ + url = self.state.urlString; + }]; + return url; +} + +- (BOOL)isDirty { + return !self.url; +} + +- (BOOL)isDataAvailable { + __block BOOL available = NO; + [self _performDataAccessBlock:^{ + available = self.dirty || [[NSFileManager defaultManager] fileExistsAtPath:[self _cachedFilePath]]; + }]; + return available; +} + +- (void)_performDataAccessBlock:(dispatch_block_t)block { + PFThreadsafetySafeDispatchSync(_synchronizationQueue, block); +} + +- (PFFileState *)_fileState { + __block PFFileState *state = nil; + [self _performDataAccessBlock:^{ + state = self.state; + }]; + return state; +} + +- (NSString *)stagedFilePath { + // Construct a path in PFFile instead of PFFileController, because we need a pointer to PFFile itself. + __block NSString *path = nil; + @weakify(self); + [self _performDataAccessBlock:^{ + @strongify(self); + if (!_stagedFilePath) { + NSString *filename = [NSString stringWithFormat:@"%p_%@", self, self.state.name]; + NSString *stagedDirectoryPath = [[self class] fileController].stagedFilesDirectoryPath; + _stagedFilePath = [stagedDirectoryPath stringByAppendingPathComponent:filename]; + } + path = _stagedFilePath; + }]; + return path; +} + +#pragma mark Progress + +- (void)_performProgressBlockAsync:(PFProgressBlock)block withProgress:(int)progress { + if (!block) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + block(progress); + }); +} + +///-------------------------------------- +#pragma mark - FileController +///-------------------------------------- + ++ (PFFileController *)fileController { + return [Parse _currentManager].coreManager.fileController; +} + +@end diff --git a/Parse/PFGeoPoint.h b/Parse/PFGeoPoint.h new file mode 100644 index 000000000..b45ddc6e2 --- /dev/null +++ b/Parse/PFGeoPoint.h @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class PFGeoPoint; + +typedef void(^PFGeoPointResultBlock)(PFGeoPoint *PF_NULLABLE_S geoPoint, NSError *PF_NULLABLE_S error); + +/*! + `PFGeoPoint` may be used to embed a latitude / longitude point as the value for a key in a . + It could be used to perform queries in a geospatial manner using <[PFQuery whereKey:nearGeoPoint:]>. + + Currently, instances of may only have one key associated with a `PFGeoPoint` type. + */ +@interface PFGeoPoint : NSObject + +///-------------------------------------- +/// @name Creating a Geo Point +///-------------------------------------- + +/*! + @abstract Create a PFGeoPoint object. Latitude and longitude are set to `0.0`. + + @returns Returns a new `PFGeoPoint`. + */ ++ (PFGeoPoint *)geoPoint; + +/*! + @abstract Creates a new `PFGeoPoint` object for the given `CLLocation`, set to the location's coordinates. + + @param location Instace of `CLLocation`, with set latitude and longitude. + + @returns Returns a new PFGeoPoint at specified location. + */ ++ (PFGeoPoint *)geoPointWithLocation:(PF_NULLABLE CLLocation *)location; + +/*! + @abstract Create a new `PFGeoPoint` object with the specified latitude and longitude. + + @param latitude Latitude of point in degrees. + @param longitude Longitude of point in degrees. + + @returns New point object with specified latitude and longitude. + */ ++ (PFGeoPoint *)geoPointWithLatitude:(double)latitude longitude:(double)longitude; + +/*! + @abstract Fetches the current device location and executes a block with a new `PFGeoPoint` object. + + @param resultBlock A block which takes the newly created `PFGeoPoint` as an argument. + It should have the following argument signature: `^(PFGeoPoint *geoPoint, NSError *error)` + */ ++ (void)geoPointForCurrentLocationInBackground:(PF_NULLABLE PFGeoPointResultBlock)resultBlock; + +///-------------------------------------- +/// @name Controlling Position +///-------------------------------------- + +/*! + @abstract Latitude of point in degrees. Valid range is from `-90.0` to `90.0`. + */ +@property (nonatomic, assign) double latitude; + +/*! + @abstract Longitude of point in degrees. Valid range is from `-180.0` to `180.0`. + */ +@property (nonatomic, assign) double longitude; + +///-------------------------------------- +/// @name Calculating Distance +///-------------------------------------- + +/*! + @abstract Get distance in radians from this point to specified point. + + @param point `PFGeoPoint` that represents the location of other point. + + @returns Distance in radians between the receiver and `point`. + */ +- (double)distanceInRadiansTo:(PF_NULLABLE PFGeoPoint *)point; + +/*! + @abstract Get distance in miles from this point to specified point. + + @param point `PFGeoPoint` that represents the location of other point. + + @returns Distance in miles between the receiver and `point`. + */ +- (double)distanceInMilesTo:(PF_NULLABLE PFGeoPoint *)point; + +/*! + @abstract Get distance in kilometers from this point to specified point. + + @param point `PFGeoPoint` that represents the location of other point. + + @returns Distance in kilometers between the receiver and `point`. + */ +- (double)distanceInKilometersTo:(PF_NULLABLE PFGeoPoint *)point; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFGeoPoint.m b/Parse/PFGeoPoint.m new file mode 100644 index 000000000..4e7dbc5ed --- /dev/null +++ b/Parse/PFGeoPoint.m @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFGeoPoint.h" + +#import + +#import "PFAssert.h" +#import "PFCoreManager.h" +#import "PFHash.h" +#import "PFLocationManager.h" +#import "Parse_Private.h" + +const double EARTH_RADIUS_MILES = 3958.8; +const double EARTH_RADIUS_KILOMETERS = 6371.0; + +@implementation PFGeoPoint + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (PFGeoPoint *)geoPoint { + return [[self alloc] init]; +} + ++ (PFGeoPoint *)geoPointWithLocation:(CLLocation *)location { + return [self geoPointWithLatitude:location.coordinate.latitude + longitude:location.coordinate.longitude]; +} + ++ (PFGeoPoint *)geoPointWithLatitude:(double)latitude longitude:(double)longitude { + PFGeoPoint *gpt = [PFGeoPoint geoPoint]; + gpt.latitude = latitude; + gpt.longitude = longitude; + return gpt; +} + ++ (void)geoPointForCurrentLocationInBackground:(PFGeoPointResultBlock)resultBlock { + if (!resultBlock) { + return; + } + + void(^locationHandler)(CLLocation *, NSError *) = ^(CLLocation *location, NSError *error) { + PFGeoPoint *newGeoPoint = [PFGeoPoint geoPointWithLocation:location]; + resultBlock(newGeoPoint, error); + }; + [[Parse _currentManager].coreManager.locationManager addBlockForCurrentLocation:locationHandler]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)setLatitude:(double)newLatitude { + // Restrictions for mongo ranges (exclusive at high end). + if (newLatitude >= 90.0 || newLatitude < -90.0) { + [NSException raise:NSInvalidArgumentException + format:@"latitude out of range (expect [-90.0, 90.0): %f", newLatitude]; + } + _latitude = newLatitude; +} + +- (void)setLongitude:(double)newLongitude { + if (newLongitude >= 180.0 || newLongitude < -180.0) { + [NSException raise:NSInvalidArgumentException + format:@"longitude out of range (expect [-180.0, 180.0): %f", newLongitude]; + } + _longitude = newLongitude; +} + +- (double)distanceInRadiansTo:(PFGeoPoint *)point { + double d2r = M_PI / 180.0; // radian conversion factor + double lat1rad = self.latitude * d2r; + double long1rad = self.longitude * d2r; + double lat2rad = [point latitude] * d2r; + double long2rad = [point longitude] * d2r; + double deltaLat = lat1rad - lat2rad; + double deltaLong = long1rad - long2rad; + double sinDeltaLatDiv2 = sin(deltaLat / 2.); + double sinDeltaLongDiv2 = sin(deltaLong / 2.); + // Square of half the straight line chord distance between both points. [0.0, 1.0] + double a = sinDeltaLatDiv2 * sinDeltaLatDiv2 + + cos(lat1rad) * cos(lat2rad) * sinDeltaLongDiv2 * sinDeltaLongDiv2; + a = fmin(1.0, a); + return 2. * asin(sqrt(a)); +} + +- (double)distanceInMilesTo:(PFGeoPoint *)point { + return [self distanceInRadiansTo:point] * EARTH_RADIUS_MILES; +} + +- (double)distanceInKilometersTo:(PFGeoPoint *)point { + return [self distanceInRadiansTo:point] * EARTH_RADIUS_KILOMETERS; +} + +///-------------------------------------- +#pragma mark - Encoding +///-------------------------------------- + +static NSString *const PFGeoPointCodingTypeKey = @"__type"; +static NSString *const PFGeoPointCodingLatitudeKey = @"latitude"; +static NSString *const PFGeoPointCodingLongitudeKey = @"longitude"; + +- (NSDictionary *)encodeIntoDictionary { + return @{ + PFGeoPointCodingTypeKey : @"GeoPoint", + PFGeoPointCodingLatitudeKey : @(self.latitude), + PFGeoPointCodingLongitudeKey : @(self.longitude) + }; +} + ++ (PFGeoPoint *)geoPointWithDictionary:(NSDictionary *)dictionary { + return [[self alloc] initWithEncodedDictionary:dictionary]; +} + +- (instancetype)initWithEncodedDictionary:(NSDictionary *)dictionary { + self = [self init]; + if (!self) return nil; + + id latObj = dictionary[PFGeoPointCodingLatitudeKey]; + PFParameterAssert([latObj isKindOfClass:[NSNumber class]], @"Invalid latitude type passed: %@", latObj); + + id longObj = dictionary[PFGeoPointCodingLongitudeKey]; + PFParameterAssert([longObj isKindOfClass:[NSNumber class]], @"Invalid longitude type passed: %@", longObj); + + _latitude = [latObj doubleValue]; + _longitude = [longObj doubleValue]; + + return self; +} + +///-------------------------------------- +#pragma mark - NSObject +///-------------------------------------- + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[PFGeoPoint class]]) { + return NO; + } + + PFGeoPoint *geoPoint = object; + + return (self.latitude == geoPoint.latitude && + self.longitude == geoPoint.longitude); +} + +- (NSUInteger)hash { + return PFDoublePairHash(self.latitude, self.longitude); +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, latitude: %f, longitude: %f>", + [self class], + self, + self.latitude, + self.longitude]; +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (instancetype)copyWithZone:(NSZone *)zone { + PFGeoPoint *geoPoint = [[self class] geoPointWithLatitude:self.latitude longitude:self.longitude]; + return geoPoint; +} + +///-------------------------------------- +#pragma mark - NSCoding +///-------------------------------------- + +- (instancetype)initWithCoder:(NSCoder *)coder { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + dictionary[PFGeoPointCodingTypeKey] = [coder decodeObjectForKey:PFGeoPointCodingTypeKey]; + dictionary[PFGeoPointCodingLatitudeKey] = [coder decodeObjectForKey:PFGeoPointCodingLatitudeKey]; + dictionary[PFGeoPointCodingLongitudeKey] = [coder decodeObjectForKey:PFGeoPointCodingLongitudeKey]; + return [self initWithEncodedDictionary:dictionary]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + NSDictionary *dictionary = [self encodeIntoDictionary]; + [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [coder encodeObject:obj forKey:key]; + }]; +} + +@end diff --git a/Parse/PFInstallation.h b/Parse/PFInstallation.h new file mode 100644 index 000000000..816178937 --- /dev/null +++ b/Parse/PFInstallation.h @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +/*! + A Parse Framework Installation Object that is a local representation of an + installation persisted to the Parse cloud. This class is a subclass of a + , and retains the same functionality of a PFObject, but also extends + it with installation-specific fields and related immutability and validity + checks. + + A valid `PFInstallation` can only be instantiated via + <[PFInstallation currentInstallation]> because the required identifier fields + are readonly. The and fields are also readonly properties which + are automatically updated to match the device's time zone and application badge + when the `PFInstallation` is saved, thus these fields might not reflect the + latest device state if the installation has not recently been saved. + + `PFInstallation` objects which have a valid and are saved to + the Parse cloud can be used to target push notifications. + */ + +@interface PFInstallation : PFObject + +///-------------------------------------- +/// @name Accessing the Current Installation +///-------------------------------------- + +/*! + @abstract Gets the currently-running installation from disk and returns an instance of it. + + @discussion If this installation is not stored on disk, returns a `PFInstallation` + with and fields set to those of the + current installation. + + @result Returns a `PFInstallation` that represents the currently-running installation. + */ ++ (instancetype)currentInstallation; + +///-------------------------------------- +/// @name Installation Properties +///-------------------------------------- + +/*! + @abstract The device type for the `PFInstallation`. + */ +@property (nonatomic, copy, readonly) NSString *deviceType; + +/*! + @abstract The installationId for the `PFInstallation`. + */ +@property (nonatomic, copy, readonly) NSString *installationId; + +/*! + @abstract The device token for the `PFInstallation`. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, copy) NSString *deviceToken; + +/*! + @abstract The badge for the `PFInstallation`. + */ +@property (nonatomic, assign) NSInteger badge; + +/*! + @abstract The name of the time zone for the `PFInstallation`. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, copy, readonly) NSString *timeZone; + +/*! + @abstract The channels for the `PFInstallation`. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, copy) NSArray *channels; + +/*! + @abstract Sets the device token string property from an `NSData`-encoded token. + + @param deviceTokenData A token that identifies the device. + */ +- (void)setDeviceTokenFromData:(PF_NULLABLE NSData *)deviceTokenData; + +///-------------------------------------- +/// @name Querying for Installations +///-------------------------------------- + +/*! + @abstract Creates a for `PFInstallation` objects. + + @discussion Only the following types of queries are allowed for installations: + + - `[query getObjectWithId:]` + - `[query whereKey:@"installationId" equalTo:]` + - `[query whereKey:@"installationId" matchesKey: inQuery:]` + + You can add additional query conditions, but one of the above must appear as a top-level `AND` clause in the query. + */ ++ (PF_NULLABLE PFQuery *)query; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFInstallation.m b/Parse/PFInstallation.m new file mode 100644 index 000000000..3e9886298 --- /dev/null +++ b/Parse/PFInstallation.m @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInstallation.h" +#import "PFInstallationPrivate.h" + +#import "BFTask+Private.h" +#import "PFApplication.h" +#import "PFAssert.h" +#import "PFCoreManager.h" +#import "PFCurrentInstallationController.h" +#import "PFFileManager.h" +#import "PFInstallationConstants.h" +#import "PFInstallationController.h" +#import "PFInstallationIdentifierStore.h" +#import "PFInternalUtils.h" +#import "PFObject+Subclass.h" +#import "PFObjectEstimatedData.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFPushPrivate.h" +#import "PFQueryPrivate.h" +#import "Parse_Private.h" + +@implementation PFInstallation (Private) + +static NSSet *protectedKeys; + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + protectedKeys = PF_SET(PFInstallationKeyDeviceType, + PFInstallationKeyInstallationId, + PFInstallationKeyTimeZone, + PFInstallationKeyParseVersion, + PFInstallationKeyAppVersion, + PFInstallationKeyAppName, + PFInstallationKeyAppIdentifier); + }); +} + +// Clear device token. Used for testing. +- (void)_clearDeviceToken { + [super removeObjectForKey:PFInstallationKeyDeviceToken]; +} + +// Check security on delete. +- (void)checkDeleteParams { + PFConsistencyAssert(NO, @"Installations cannot be deleted."); +} + +// Validates a class name. We override this to only allow the installation class name. ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert([className isEqualToString:[PFInstallation parseClassName]], + @"Cannot initialize a PFInstallation with a custom class name."); +} + +- (BOOL)_isCurrentInstallation { + return (self == [[self class] _currentInstallationController].memoryCachedCurrentInstallation); +} + +- (void)_markAllFieldsDirty { + @synchronized(self.lock) { + NSDictionary *estimatedData = self._estimatedData.dictionaryRepresentation; + [estimatedData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [super setObject:obj forKey:key]; + }]; + } +} + +- (NSString *)displayClassName { + return NSStringFromClass([PFInstallation class]); +} + +///-------------------------------------- +#pragma mark - Command Handlers +///-------------------------------------- + +- (BFTask *)handleSaveResultAsync:(NSDictionary *)result { + @weakify(self); + return [[super handleSaveResultAsync:result] continueWithBlock:^id(BFTask *task) { + @strongify(self); + BFTask *saveTask = [[[self class] _currentInstallationController] saveCurrentObjectAsync:self]; + return [saveTask continueWithResult:task]; + }]; +} + +///-------------------------------------- +#pragma mark - Current Installation Controller +///-------------------------------------- + ++ (PFCurrentInstallationController *)_currentInstallationController { + return [Parse _currentManager].coreManager.currentInstallationController; +} + +@end + +@implementation PFInstallation + +@dynamic deviceType; +@dynamic installationId; +@dynamic deviceToken; +@dynamic timeZone; +@dynamic channels; +@dynamic badge; + +///-------------------------------------- +#pragma mark - PFSubclassing +///-------------------------------------- + ++ (NSString *)parseClassName { + return @"_Installation"; +} + ++ (PFQuery *)query { + return [super query]; +} + +///-------------------------------------- +#pragma mark - Current Installation +///-------------------------------------- + ++ (instancetype)currentInstallation { + BFTask *task = [[self _currentInstallationController] getCurrentObjectAsync]; + return [task waitForResult:nil withMainThreadWarning:NO]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (id)objectForKey:(NSString *)key { + if ([key isEqualToString:PFInstallationKeyBadge] && [self _isCurrentInstallation]) { + // Update the data dictionary badge value from the device. + [self _updateBadgeFromDevice]; + } + + return [super objectForKey:key]; +} + +- (void)setObject:(id)object forKey:(NSString *)key { + PFParameterAssert(![protectedKeys containsObject:key], + @"Can't change the '%@' field of a PFInstallation.", key); + + if ([key isEqualToString:PFInstallationKeyBadge]) { + // Set the application badge and update the badge value in the data dictionary. + NSInteger badge = [object integerValue]; + PFParameterAssert(badge >= 0, @"Can't set the badge to less than zero."); + + [PFApplication currentApplication].iconBadgeNumber = badge; + [super setObject:@(badge) forKey:PFInstallationKeyBadge]; + } + + [super setObject:object forKey:key]; +} + +- (void)incrementKey:(NSString *)key byAmount:(NSNumber *)amount { + PFParameterAssert(![key isEqualToString:PFInstallationKeyBadge], + @"Can't atomically increment the 'badge' field of a PFInstallation."); + + [super incrementKey:key byAmount:amount]; +} + +- (void)removeObjectForKey:(NSString *)key { + PFParameterAssert(![protectedKeys containsObject:key], + @"Can't remove the '%@' field of a PFInstallation.", key); + PFParameterAssert(![key isEqualToString:PFInstallationKeyBadge], + @"Can't remove the 'badge' field of a PFInstallation."); + [super removeObjectForKey:key]; +} + +// Internal mutators override the dynamic accessor and use super to avoid +// read-only checks on automatic fields. +- (void)setDeviceType:(NSString *)deviceType { + [self _setObject:deviceType forKey:PFInstallationKeyDeviceType onlyIfDifferent:YES]; +} + +- (void)setInstallationId:(NSString *)installationId { + [self _setObject:installationId forKey:PFInstallationKeyInstallationId onlyIfDifferent:YES]; +} + +- (void)setDeviceToken:(NSString *)deviceToken { + [self _setObject:deviceToken forKey:PFInstallationKeyDeviceToken onlyIfDifferent:YES]; +} + +- (void)setDeviceTokenFromData:(NSData *)deviceTokenData { + [self _setObject:[[PFPush pushInternalUtilClass] convertDeviceTokenToString:deviceTokenData] + forKey:PFInstallationKeyDeviceToken + onlyIfDifferent:YES]; +} + +- (void)setTimeZone:(NSString *)timeZone { + [self _setObject:timeZone forKey:PFInstallationKeyTimeZone onlyIfDifferent:YES]; +} + +- (void)setChannels:(NSArray *)channels { + [self _setObject:channels forKey:PFInstallationKeyChannels onlyIfDifferent:YES]; +} + +///-------------------------------------- +#pragma mark - PFObject +///-------------------------------------- + +- (BFTask *)saveInBackground { + [self _updateAutomaticInfo]; + return [super saveInBackground]; +} + +- (BFTask *)_enqueueSaveEventuallyWithChildren:(BOOL)saveChildren { + [self _updateAutomaticInfo]; + return [super _enqueueSaveEventuallyWithChildren:saveChildren]; +} + +- (BFTask *)saveEventually { + [self _updateAutomaticInfo]; + return [super saveEventually]; +} + +- (BFTask *)saveAsync:(BFTask *)toAwait { + return [[super saveAsync:toAwait] continueWithBlock:^id(BFTask *task) { + // Do not attempt to resave an object if LDS is enabled, since changing objectId is not allowed. + if ([Parse _currentManager].offlineStoreLoaded) { + return task; + } + + if (task.error.code == kPFErrorObjectNotFound) { + @synchronized (self.lock) { + // Retry the fetch as a save operation because this Installation was deleted on the server. + // We always want [currentInstallation save] to succeed. + self.objectId = nil; + [self _markAllFieldsDirty]; + return [super saveAsync:nil]; + } + } + return task; + }]; +} + +- (BOOL)needsDefaultACL { + return NO; +} + +///-------------------------------------- +#pragma mark - Automatic Info +///-------------------------------------- + +- (void)_updateAutomaticInfo { + if ([self _isCurrentInstallation]) { + @synchronized(self.lock) { + [self _updateTimeZoneFromDevice]; + [self _updateBadgeFromDevice]; + [self _updateVersionInfoFromDevice]; + } + } +} + +- (void)_updateTimeZoneFromDevice { + // Get the system time zone (after clearing the cached value) and update + // the installation if necessary. + NSString *systemTimeZoneName = [PFInternalUtils currentSystemTimeZoneName]; + if (![systemTimeZoneName isEqualToString:self.timeZone]) { + self.timeZone = systemTimeZoneName; + } +} + +- (void)_updateBadgeFromDevice { + // Get the application icon and update the installation if necessary. + NSNumber *applicationBadge = @([PFApplication currentApplication].iconBadgeNumber); + NSNumber *installationBadge = [super objectForKey:PFInstallationKeyBadge]; + if (installationBadge == nil || ![applicationBadge isEqualToNumber:installationBadge]) { + [super setObject:applicationBadge forKey:PFInstallationKeyBadge]; + } +} + +- (void)_updateVersionInfoFromDevice { + NSDictionary *appInfo = [[NSBundle mainBundle] infoDictionary]; + NSString *appName = appInfo[(__bridge NSString *)kCFBundleNameKey]; + NSString *appVersion = appInfo[(__bridge NSString *)kCFBundleVersionKey]; + NSString *appIdentifier = appInfo[(__bridge NSString *)kCFBundleIdentifierKey]; + // It's possible that the app was created without an info.plist and we just + // cannot get the data we need. + // Note: it's important to make the possibly nil string the message receptor for + // nil propegation instead of a BAD_ACCESS + if (appName && ![self[PFInstallationKeyAppName] isEqualToString:appName]) { + [super setObject:appName forKey:PFInstallationKeyAppName]; + } + if (appVersion && ![self[PFInstallationKeyAppVersion] isEqualToString:appVersion]) { + [super setObject:appVersion forKey:PFInstallationKeyAppVersion]; + } + if (appIdentifier && ![self[PFInstallationKeyAppIdentifier] isEqualToString:appIdentifier]) { + [super setObject:appIdentifier forKey:PFInstallationKeyAppIdentifier]; + } + if (![self[PFInstallationKeyParseVersion] isEqualToString:PARSE_VERSION]) { + [super setObject:PARSE_VERSION forKey:PFInstallationKeyParseVersion]; + } +} + +///-------------------------------------- +#pragma mark - Data Source +///-------------------------------------- + ++ (id)objectController { + return [Parse _currentManager].coreManager.installationController; +} + +@end diff --git a/Parse/PFNetworkActivityIndicatorManager.h b/Parse/PFNetworkActivityIndicatorManager.h new file mode 100644 index 000000000..d5b376a53 --- /dev/null +++ b/Parse/PFNetworkActivityIndicatorManager.h @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import + +PF_ASSUME_NONNULL_BEGIN + +/*! + `PFNetworkActivityIndicatorManager` manages the state of the network activity indicator in the status bar. + When enabled, it will start managing the network activity indicator in the status bar, + according to the network operations that are performed by Parse SDK. + + The number of active requests is incremented or decremented like a stack or a semaphore, + the activity indicator will animate, as long as the number is greater than zero. + */ +@interface PFNetworkActivityIndicatorManager : NSObject + +/*! + A Boolean value indicating whether the manager is enabled. + If `YES` - the manager will start managing the status bar network activity indicator, + according to the network operations that are performed by Parse SDK. + The default value is `YES`. + */ +@property (nonatomic, assign, getter = isEnabled) BOOL enabled; + +/*! + A Boolean value indicating whether the network activity indicator is currently displayed in the status bar. + */ +@property (nonatomic, assign, readonly, getter = isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible; + +/*! + The value that indicates current network activities count. + */ +@property (nonatomic, assign, readonly) NSUInteger networkActivityCount; + +/*! + @abstract Returns the shared network activity indicator manager object for the system. + + @returns The systemwide network activity indicator manager. + */ ++ (instancetype)sharedManager; + +/*! + @abstract Increments the number of active network requests. + + @discussion If this number was zero before incrementing, + this will start animating network activity indicator in the status bar. + */ +- (void)incrementActivityCount; + +/*! + @abstract Decrements the number of active network requests. + + @discussion If this number becomes zero after decrementing, + this will stop animating network activity indicator in the status bar. + */ +- (void)decrementActivityCount; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFNetworkActivityIndicatorManager.m b/Parse/PFNetworkActivityIndicatorManager.m new file mode 100644 index 000000000..327ca0024 --- /dev/null +++ b/Parse/PFNetworkActivityIndicatorManager.m @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFNetworkActivityIndicatorManager.h" + +#import "PFApplication.h" + +static NSTimeInterval const PFNetworkActivityIndicatorVisibilityDelay = 0.17; + +@interface PFNetworkActivityIndicatorManager () { + dispatch_queue_t _networkActivityAccessQueue; +} + +@property (nonatomic, assign, readwrite) NSUInteger networkActivityCount; + +@property (nonatomic, strong) NSTimer *activityIndicatorVisibilityTimer; + +@end + +@implementation PFNetworkActivityIndicatorManager + +@synthesize enabled = _enabled; +@synthesize networkActivityCount = _networkActivityCount; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)sharedManager { + static PFNetworkActivityIndicatorManager *manager; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[self alloc] init]; + manager.enabled = YES; + }); + return manager; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _networkActivityAccessQueue = dispatch_queue_create("com.parse.networkActivityIndicatorManager", + DISPATCH_QUEUE_SERIAL); + + return self; +} + +- (void)dealloc { + [_activityIndicatorVisibilityTimer invalidate]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)setNetworkActivityCount:(NSUInteger)networkActivityCount { + dispatch_sync(_networkActivityAccessQueue, ^{ + _networkActivityCount = networkActivityCount; + }); + dispatch_async(dispatch_get_main_queue(), ^{ + [self _updateNetworkActivityIndicatorVisibilityAfterDelay]; + }); +} + +- (NSUInteger)networkActivityCount { + __block NSUInteger count = 0; + dispatch_sync(_networkActivityAccessQueue, ^{ + count = _networkActivityCount; + }); + return count; +} + +- (BOOL)isNetworkActivityIndicatorVisible { + return self.networkActivityCount > 0; +} + +///-------------------------------------- +#pragma mark - Counts +///-------------------------------------- + +- (void)incrementActivityCount { + dispatch_sync(_networkActivityAccessQueue, ^{ + _networkActivityCount++; + }); + dispatch_async(dispatch_get_main_queue(), ^{ + [self _updateNetworkActivityIndicatorVisibilityAfterDelay]; + }); +} + +- (void)decrementActivityCount { + dispatch_sync(_networkActivityAccessQueue, ^{ + _networkActivityCount = MAX(_networkActivityCount - 1, 0); + }); + dispatch_async(dispatch_get_main_queue(), ^{ + [self _updateNetworkActivityIndicatorVisibilityAfterDelay]; + }); +} + +///-------------------------------------- +#pragma mark - Network Activity Indicator +///-------------------------------------- + +- (void)_updateNetworkActivityIndicatorVisibilityAfterDelay { + if (self.enabled) { + // Delay hiding of activity indicator for a short interval, to avoid flickering + if (![self isNetworkActivityIndicatorVisible]) { + [self.activityIndicatorVisibilityTimer invalidate]; + + NSTimeInterval timeInterval = PFNetworkActivityIndicatorVisibilityDelay; + SEL selector = @selector(_updateNetworkActivityIndicatorVisibility); + self.activityIndicatorVisibilityTimer = [NSTimer timerWithTimeInterval:timeInterval + target:self + selector:selector + userInfo:nil + repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:self.activityIndicatorVisibilityTimer + forMode:NSRunLoopCommonModes]; + } else { + [self performSelectorOnMainThread:@selector(_updateNetworkActivityIndicatorVisibility) + withObject:nil + waitUntilDone:NO + modes:@[ NSRunLoopCommonModes ]]; + } + } +} + +- (void)_updateNetworkActivityIndicatorVisibility { + if (![PFApplication currentApplication].extensionEnvironment) { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:self.networkActivityIndicatorVisible]; + } +} + +@end diff --git a/Parse/PFNullability.h b/Parse/PFNullability.h new file mode 100644 index 000000000..8c1b95844 --- /dev/null +++ b/Parse/PFNullability.h @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#ifndef Parse_PFNullability_h +#define Parse_PFNullability_h + +///-------------------------------------- +/// @name Nullability Annotation Support +///-------------------------------------- + +#if __has_feature(nullability) +# define PF_NONNULL nonnull +# define PF_NONNULL_S __nonnull +# define PF_NULLABLE nullable +# define PF_NULLABLE_S __nullable +# define PF_NULLABLE_PROPERTY nullable, +#else +# define PF_NONNULL +# define PF_NONNULL_S +# define PF_NULLABLE +# define PF_NULLABLE_S +# define PF_NULLABLE_PROPERTY +#endif + +#if __has_feature(assume_nonnull) +# ifdef NS_ASSUME_NONNULL_BEGIN +# define PF_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN +# else +# define PF_ASSUME_NONNULL_BEGIN _Pragma("clang assume_nonnull begin") +# endif +# ifdef NS_ASSUME_NONNULL_END +# define PF_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END +# else +# define PF_ASSUME_NONNULL_END _Pragma("clang assume_nonnull end") +# endif +#else +# define PF_ASSUME_NONNULL_BEGIN +# define PF_ASSUME_NONNULL_END +#endif + +#endif diff --git a/Parse/PFObject+Subclass.h b/Parse/PFObject+Subclass.h new file mode 100644 index 000000000..92b869543 --- /dev/null +++ b/Parse/PFObject+Subclass.h @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#else +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class PFQuery; + +/*! + ### Subclassing Notes + + Developers can subclass `PFObject` for a more native object-oriented class structure. + Strongly-typed subclasses of `PFObject` must conform to the protocol + and must call before <[Parse setApplicationId:clientKey:]> is called. + After this it will be returned by and other `PFObject` factories. + + All methods in except for <[PFSubclassing parseClassName]> + are already implemented in the `PFObject+Subclass` category. + + Including `PFObject+Subclass.h` in your implementation file provides these implementations automatically. + + Subclasses support simpler initializers, query syntax, and dynamic synthesizers. + The following shows an example subclass: + + \@interface MYGame : PFObject + + // Accessing this property is the same as objectForKey:@"title" + @property (nonatomic, copy) NSString *title; + + + (NSString *)parseClassName; + + @end + + + @implementation MYGame + + @dynamic title; + + + (NSString *)parseClassName { + return @"Game"; + } + + @end + + + MYGame *game = [[MYGame alloc] init]; + game.title = @"Bughouse"; + [game saveInBackground]; + */ +@interface PFObject (Subclass) + +///-------------------------------------- +/// @name Methods for Subclasses +///-------------------------------------- + +/*! + @abstract Creates an instance of the registered subclass with this class's . + + @discussion This helps a subclass ensure that it can be subclassed itself. + For example, `[PFUser object]` will return a `MyUser` object if `MyUser` is a registered subclass of `PFUser`. + For this reason, `[MyClass object]` is preferred to `[[MyClass alloc] init]`. + This method can only be called on subclasses which conform to `PFSubclassing`. + A default implementation is provided by `PFObject` which should always be sufficient. + */ ++ (instancetype)object; + +/*! + @abstract Creates a reference to an existing `PFObject` for use in creating associations between `PFObjects`. + + @discussion Calling on this object will return `NO` until or has been called. + This method can only be called on subclasses which conform to . + A default implementation is provided by `PFObject` which should always be sufficient. + No network request will be made. + + @param objectId The object id for the referenced object. + + @returns An instance of `PFObject` without data. + */ ++ (instancetype)objectWithoutDataWithObjectId:(PF_NULLABLE NSString *)objectId; + +/*! + @abstract Registers an Objective-C class for Parse to use for representing a given Parse class. + + @discussion Once this is called on a `PFObject` subclass, any `PFObject` Parse creates with a class name + that matches `[self parseClassName]` will be an instance of subclass. + This method can only be called on subclasses which conform to . + A default implementation is provided by `PFObject` which should always be sufficient. + */ ++ (void)registerSubclass; + +/*! + @abstract Returns a query for objects of type . + + @discussion This method can only be called on subclasses which conform to . + A default implementation is provided by which should always be sufficient. + */ ++ (PF_NULLABLE PFQuery *)query; + +/*! + @abstract Returns a query for objects of type with a given predicate. + + @discussion A default implementation is provided by which should always be sufficient. + @warning This method can only be called on subclasses which conform to . + + @param predicate The predicate to create conditions from. + + @returns An instance of . + + @see [PFQuery queryWithClassName:predicate:] + */ ++ (PF_NULLABLE PFQuery *)queryWithPredicate:(PF_NULLABLE NSPredicate *)predicate; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFObject.h b/Parse/PFObject.h new file mode 100644 index 000000000..de63a2ced --- /dev/null +++ b/Parse/PFObject.h @@ -0,0 +1,1420 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#else +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@protocol PFSubclassing; +@class BFTask; +@class PFRelation; + +/*! + The name of the default pin that for PFObject local data store. + */ +extern NSString *const PFObjectDefaultPin; + +/*! + The `PFObject` class is a local representation of data persisted to the Parse cloud. + This is the main class that is used to interact with objects in your app. + */ +NS_REQUIRES_PROPERTY_DEFINITIONS +@interface PFObject : NSObject { + BOOL dirty; + + // An array of NSDictionary of NSString -> PFFieldOperation. + // Each dictionary has a subset of the object's keys as keys, and the + // changes to the value for that key as its value. + // There is always at least one dictionary of pending operations. + // Every time a save is started, a new dictionary is added to the end. + // Whenever a save completes, the new data is put into fetchedData, and + // a dictionary is removed from the start. + NSMutableArray *PF_NULLABLE_S operationSetQueue; +} + +///-------------------------------------- +/// @name Creating a PFObject +///-------------------------------------- + +/*! + @abstract Initializes a new empty `PFObject` instance with a class name. + + @param newClassName A class name can be any alphanumeric string that begins with a letter. + It represents an object in your app, like a 'User' or a 'Document'. + + @returns Returns the object that is instantiated with the given class name. + */ +- (instancetype)initWithClassName:(NSString *)newClassName; + +/*! + @abstract Creates a new PFObject with a class name. + + @param className A class name can be any alphanumeric string that begins with a letter. + It represents an object in your app, like a 'User' or a 'Document'. + + @returns Returns the object that is instantiated with the given class name. + */ ++ (instancetype)objectWithClassName:(NSString *)className; + +/*! + @abstract Creates a new `PFObject` with a class name, initialized with data + constructed from the specified set of objects and keys. + + @param className The object's class. + @param dictionary An `NSDictionary` of keys and objects to set on the new `PFObject`. + + @returns A PFObject with the given class name and set with the given data. + */ ++ (instancetype)objectWithClassName:(NSString *)className dictionary:(PF_NULLABLE NSDictionary *)dictionary; + +/*! + @abstract Creates a reference to an existing PFObject for use in creating associations between PFObjects. + + @discussion Calling on this object will return `NO` until has been called. + No network request will be made. + + @param className The object's class. + @param objectId The object id for the referenced object. + + @returns A `PFObject` instance without data. + */ ++ (instancetype)objectWithoutDataWithClassName:(NSString *)className objectId:(PF_NULLABLE NSString *)objectId; + +///-------------------------------------- +/// @name Managing Object Properties +///-------------------------------------- + +/*! + @abstract The class name of the object. + */ +@property (strong, readonly) NSString *parseClassName; + +/*! + @abstract The id of the object. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSString *objectId; + +/*! + @abstract When the object was last updated. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong, readonly) NSDate *updatedAt; + +/*! + @abstract When the object was created. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong, readonly) NSDate *createdAt; + +/*! + @abstract The ACL for this object. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) PFACL *ACL; + +/*! + @abstract Returns an array of the keys contained in this object. + + @discussion This does not include `createdAt`, `updatedAt`, `authData`, or `objectId`. + It does include things like username and ACL. + */ +- (NSArray *)allKeys; + +///-------------------------------------- +/// @name Accessors +///-------------------------------------- + +/*! + @abstract Returns the value associated with a given key. + + @param key The key for which to return the corresponding value. + */ +- (PF_NULLABLE_S id)objectForKey:(NSString *)key; + +/*! + @abstract Sets the object associated with a given key. + + @param object The object for `key`. A strong reference to the object is maintaned by PFObject. + Raises an `NSInvalidArgumentException` if `object` is `nil`. + If you need to represent a `nil` value - use `NSNull`. + @param key The key for `object`. + Raises an `NSInvalidArgumentException` if `key` is `nil`. + + @see setObject:forKeyedSubscript: + */ +- (void)setObject:(id)object forKey:(NSString *)key; + +/*! + @abstract Unsets a key on the object. + + @param key The key. + */ +- (void)removeObjectForKey:(NSString *)key; + +/*! + @abstract Returns the value associated with a given key. + + @discussion This method enables usage of literal syntax on `PFObject`. + E.g. `NSString *value = object[@"key"];` + + @param key The key for which to return the corresponding value. + + @see objectForKey: + */ +- (PF_NULLABLE_S id)objectForKeyedSubscript:(NSString *)key; + +/*! + @abstract Returns the value associated with a given key. + + @discussion This method enables usage of literal syntax on `PFObject`. + E.g. `object[@"key"] = @"value";` + + @param object The object for `key`. A strong reference to the object is maintaned by PFObject. + Raises an `NSInvalidArgumentException` if `object` is `nil`. + If you need to represent a `nil` value - use `NSNull`. + @param key The key for `object`. + Raises an `NSInvalidArgumentException` if `key` is `nil`. + + @see setObject:forKey: + */ +- (void)setObject:(PF_NULLABLE_S id)object forKeyedSubscript:(NSString *)key; + +/*! + @abstract Returns the relation object associated with the given key. + + @param key The key that the relation is associated with. + */ +- (PFRelation *)relationForKey:(NSString *)key; + +/*! + @abstract Returns the relation object associated with the given key. + + @param key The key that the relation is associated with. + + @deprecated Please use `[PFObject relationForKey:]` instead. + */ +- (PFRelation *)relationforKey:(NSString *)key PARSE_DEPRECATED("Please use -relationForKey: instead."); + +///-------------------------------------- +/// @name Array Accessors +///-------------------------------------- + +/*! + @abstract Adds an object to the end of the array associated with a given key. + + @param object The object to add. + @param key The key. + */ +- (void)addObject:(id)object forKey:(NSString *)key; + +/*! + @abstract Adds the objects contained in another array to the end of the array associated with a given key. + + @param objects The array of objects to add. + @param key The key. + */ +- (void)addObjectsFromArray:(NSArray *)objects forKey:(NSString *)key; + +/*! + @abstract Adds an object to the array associated with a given key, only if it is not already present in the array. + + @discussion The position of the insert is not guaranteed. + + @param object The object to add. + @param key The key. + */ +- (void)addUniqueObject:(id)object forKey:(NSString *)key; + +/*! + @abstract Adds the objects contained in another array to the array associated with a given key, + only adding elements which are not already present in the array. + + @dicsussion The position of the insert is not guaranteed. + + @param objects The array of objects to add. + @param key The key. + */ +- (void)addUniqueObjectsFromArray:(NSArray *)objects forKey:(NSString *)key; + +/*! + @abstract Removes all occurrences of an object from the array associated with a given key. + + @param object The object to remove. + @param key The key. + */ +- (void)removeObject:(id)object forKey:(NSString *)key; + +/*! + @abstract Removes all occurrences of the objects contained in another array from the array associated with a given key. + + @param objects The array of objects to remove. + @param key The key. + */ +- (void)removeObjectsInArray:(NSArray *)objects forKey:(NSString *)key; + +///-------------------------------------- +/// @name Increment +///-------------------------------------- + +/*! + @abstract Increments the given key by `1`. + + @param key The key. + */ +- (void)incrementKey:(NSString *)key; + +/*! + @abstract Increments the given key by a number. + + @param key The key. + @param amount The amount to increment. + */ +- (void)incrementKey:(NSString *)key byAmount:(NSNumber *)amount; + +///-------------------------------------- +/// @name Saving Objects +///-------------------------------------- + +/*! + @abstract *Synchronously* saves the `PFObject`. + + @returns Returns whether the save succeeded. + */ +- (BOOL)save; + +/*! + @abstract *Synchronously* saves the `PFObject` and sets an error if it occurs. + + @param error Pointer to an NSError that will be set if necessary. + + @returns Returns whether the save succeeded. + */ +- (BOOL)save:(NSError **)error; + +/*! + @abstract Saves the `PFObject` *asynchronously*. + + @returns The task that encapsulates the work being done. + */ +- (BFTask *)saveInBackground; + +/*! + @abstract Saves the `PFObject` *asynchronously* and executes the given callback block. + + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ +- (void)saveInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block; + +/* + @abstract Saves the `PFObject` asynchronously and calls the given callback. + + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ +- (void)saveInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +/*! + @abstract Saves this object to the server at some unspecified time in the future, + even if Parse is currently inaccessible. + + @discussion Use this when you may not have a solid network connection, and don't need to know when the save completes. + If there is some problem with the object such that it can't be saved, it will be silently discarded. If the save + completes successfully while the object is still in memory, then callback will be called. + + Objects saved with this method will be stored locally in an on-disk cache until they can be delivered to Parse. + They will be sent immediately if possible. Otherwise, they will be sent the next time a network connection is + available. Objects saved this way will persist even after the app is closed, in which case they will be sent the + next time the app is opened. If more than 10MB of data is waiting to be sent, subsequent calls to + will cause old saves to be silently discarded until the connection can be re-established, and the queued objects + can be saved. + + @returns The task that encapsulates the work being done. + */ +- (BFTask *)saveEventually; + +/*! + @abstract Saves this object to the server at some unspecified time in the future, + even if Parse is currently inaccessible. + + @discussion Use this when you may not have a solid network connection, and don't need to know when the save completes. + If there is some problem with the object such that it can't be saved, it will be silently discarded. If the save + completes successfully while the object is still in memory, then callback will be called. + + Objects saved with this method will be stored locally in an on-disk cache until they can be delivered to Parse. + They will be sent immediately if possible. Otherwise, they will be sent the next time a network connection is + available. Objects saved this way will persist even after the app is closed, in which case they will be sent the + next time the app is opened. If more than 10MB of data is waiting to be sent, subsequent calls to + will cause old saves to be silently discarded until the connection can be re-established, and the queued objects + can be saved. + + @param callback The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ +- (void)saveEventually:(PF_NULLABLE PFBooleanResultBlock)callback; + +///-------------------------------------- +/// @name Saving Many Objects +///-------------------------------------- + +/*! + @abstract Saves a collection of objects *synchronously all at once. + + @param objects The array of objects to save. + + @returns Returns whether the save succeeded. + */ ++ (BOOL)saveAll:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract Saves a collection of objects *synchronously* all at once and sets an error if necessary. + + @param objects The array of objects to save. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the save succeeded. + */ ++ (BOOL)saveAll:(PF_NULLABLE NSArray *)objects error:(NSError **)error; + +/*! + @abstract Saves a collection of objects all at once *asynchronously*. + + @param objects The array of objects to save. + + @returns The task that encapsulates the work being done. + */ ++ (BFTask *)saveAllInBackground:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract Saves a collection of objects all at once `asynchronously` and executes the block when done. + + @param objects The array of objects to save. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)saveAllInBackground:(PF_NULLABLE NSArray *)objects + block:(PF_NULLABLE PFBooleanResultBlock)block; + +/* + @abstract Saves a collection of objects all at once *asynchronously* and calls a callback when done. + + @param objects The array of objects to save. + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)number error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ ++ (void)saveAllInBackground:(PF_NULLABLE NSArray *)objects + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Deleting Many Objects +///-------------------------------------- + +/*! + @abstract *Synchronously* deletes a collection of objects all at once. + + @param objects The array of objects to delete. + + @returns Returns whether the delete succeeded. + */ ++ (BOOL)deleteAll:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract *Synchronously* deletes a collection of objects all at once and sets an error if necessary. + + @param objects The array of objects to delete. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the delete succeeded. + */ ++ (BOOL)deleteAll:(PF_NULLABLE NSArray *)objects error:(NSError **)error; + +/*! + @abstract Deletes a collection of objects all at once asynchronously. + @param objects The array of objects to delete. + @returns The task that encapsulates the work being done. + */ ++ (BFTask *)deleteAllInBackground:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract Deletes a collection of objects all at once *asynchronously* and executes the block when done. + + @param objects The array of objects to delete. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)deleteAllInBackground:(PF_NULLABLE NSArray *)objects + block:(PF_NULLABLE PFBooleanResultBlock)block; + +/* + @abstract Deletes a collection of objects all at once *asynchronously* and calls a callback when done. + + @param objects The array of objects to delete. + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)number error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ ++ (void)deleteAllInBackground:(PF_NULLABLE NSArray *)objects + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Getting an Object +///-------------------------------------- + +/*! + @abstract Gets whether the `PFObject` has been fetched. + + @returns `YES` if the PFObject is new or has been fetched or refreshed, otherwise `NO`. + */ +- (BOOL)isDataAvailable; + +#if PARSE_IOS_ONLY + +/*! + @abstract Refreshes the PFObject with the current data from the server. + + @deprecated Please use `-fetch` instead. + */ +- (void)refresh PARSE_DEPRECATED("Please use `-fetch` instead."); + +/*! + @abstract *Synchronously* refreshes the `PFObject` with the current data from the server and sets an error if it occurs. + + @param error Pointer to an `NSError` that will be set if necessary. + + @deprecated Please use `-fetch:` instead. + */ +- (void)refresh:(NSError **)error PARSE_DEPRECATED("Please use `-fetch:` instead."); + +/*! + @abstract *Asynchronously* refreshes the `PFObject` and executes the given callback block. + + @param block The block to execute. + The block should have the following argument signature: `^(PFObject *object, NSError *error)` + + @deprecated Please use `-fetchInBackgroundWithBlock:` instead. + */ +- (void)refreshInBackgroundWithBlock:(PF_NULLABLE PFObjectResultBlock)block PARSE_DEPRECATED("Please use `-fetchInBackgroundWithBlock:` instead."); + +/* + @abstract *Asynchronously* refreshes the `PFObject` and calls the given callback. + + @param target The target on which the selector will be called. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(PFObject *)refreshedObject error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `refreshedObject` will be the `PFObject` with the refreshed data. + + @deprecated Please use `fetchInBackgroundWithTarget:selector:` instead. + */ +- (void)refreshInBackgroundWithTarget:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector PARSE_DEPRECATED("Please use `fetchInBackgroundWithTarget:selector:` instead."); + +#endif + +/*! + @abstract *Synchronously* fetches the PFObject with the current data from the server. + */ +- (void)fetch; +/*! + @abstract *Synchronously* fetches the PFObject with the current data from the server and sets an error if it occurs. + + @param error Pointer to an `NSError` that will be set if necessary. + */ +- (void)fetch:(NSError **)error; + +/*! + @abstract *Synchronously* fetches the `PFObject` data from the server if is `NO`. + */ +- (PF_NULLABLE PFObject *)fetchIfNeeded; + +/*! + @abstract *Synchronously* fetches the `PFObject` data from the server if is `NO`. + + @param error Pointer to an `NSError` that will be set if necessary. + */ +- (PF_NULLABLE PFObject *)fetchIfNeeded:(NSError **)error; + +/*! + @abstract Fetches the `PFObject` *asynchronously* and sets it as a result for the task. + + @returns The task that encapsulates the work being done. + */ +- (BFTask *)fetchInBackground; + +/*! + @abstract Fetches the PFObject *asynchronously* and executes the given callback block. + + @param block The block to execute. + It should have the following argument signature: `^(PFObject *object, NSError *error)`. + */ +- (void)fetchInBackgroundWithBlock:(PF_NULLABLE PFObjectResultBlock)block; + +/* + @abstract Fetches the `PFObject *asynchronously* and calls the given callback. + + @param target The target on which the selector will be called. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(PFObject *)refreshedObject error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `refreshedObject` will be the `PFObject` with the refreshed data. + */ +- (void)fetchInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +/*! + @abstract Fetches the `PFObject` data *asynchronously* if isDataAvailable is `NO`, + then sets it as a result for the task. + + @returns The task that encapsulates the work being done. + */ +- (BFTask *)fetchIfNeededInBackground; + +/*! + @abstract Fetches the `PFObject` data *asynchronously* if is `NO`, then calls the callback block. + + @param block The block to execute. + It should have the following argument signature: `^(PFObject *object, NSError *error)`. + */ +- (void)fetchIfNeededInBackgroundWithBlock:(PF_NULLABLE PFObjectResultBlock)block; + +/* + @abstract Fetches the PFObject's data asynchronously if isDataAvailable is false, then calls the callback. + + @param target The target on which the selector will be called. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(PFObject *)fetchedObject error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `refreshedObject` will be the `PFObject` with the refreshed data. + */ +- (void)fetchIfNeededInBackgroundWithTarget:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Getting Many Objects +///-------------------------------------- + +/*! + @abstract *Synchronously* fetches all of the `PFObject` objects with the current data from the server. + + @param objects The list of objects to fetch. + */ ++ (void)fetchAll:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract *Synchronously* fetches all of the `PFObject` objects with the current data from the server + and sets an error if it occurs. + + @param objects The list of objects to fetch. + @param error Pointer to an `NSError` that will be set if necessary. + */ ++ (void)fetchAll:(PF_NULLABLE NSArray *)objects error:(NSError **)error; + +/*! + @abstract *Synchronously* fetches all of the `PFObject` objects with the current data from the server. + @param objects The list of objects to fetch. + */ ++ (void)fetchAllIfNeeded:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract *Synchronously* fetches all of the `PFObject` objects with the current data from the server + and sets an error if it occurs. + + @param objects The list of objects to fetch. + @param error Pointer to an `NSError` that will be set if necessary. + */ ++ (void)fetchAllIfNeeded:(PF_NULLABLE NSArray *)objects error:(NSError **)error; + +/*! + @abstract Fetches all of the `PFObject` objects with the current data from the server *asynchronously*. + + @param objects The list of objects to fetch. + + @returns The task that encapsulates the work being done. + */ ++ (BFTask *)fetchAllInBackground:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract Fetches all of the `PFObject` objects with the current data from the server *asynchronously* + and calls the given block. + + @param objects The list of objects to fetch. + @param block The block to execute. + It should have the following argument signature: `^(NSArray *objects, NSError *error)`. + */ ++ (void)fetchAllInBackground:(PF_NULLABLE NSArray *)objects + block:(PF_NULLABLE PFArrayResultBlock)block; + +/* + @abstract Fetches all of the `PFObject` objects with the current data from the server *asynchronously* + and calls the given callback. + + @param objects The list of objects to fetch. + @param target The target on which the selector will be called. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSArray *)fetchedObjects error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `fetchedObjects` will the array of `PFObject` objects that were fetched. + */ ++ (void)fetchAllInBackground:(PF_NULLABLE NSArray *)objects + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +/*! + @abstract Fetches all of the `PFObject` objects with the current data from the server *asynchronously*. + + @param objects The list of objects to fetch. + + @returns The task that encapsulates the work being done. + */ ++ (BFTask *)fetchAllIfNeededInBackground:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract Fetches all of the PFObjects with the current data from the server *asynchronously* + and calls the given block. + + @param objects The list of objects to fetch. + @param block The block to execute. + It should have the following argument signature: `^(NSArray *objects, NSError *error)`. + */ ++ (void)fetchAllIfNeededInBackground:(PF_NULLABLE NSArray *)objects + block:(PF_NULLABLE PFArrayResultBlock)block; + +/* + @abstract Fetches all of the PFObjects with the current data from the server *asynchronously* + and calls the given callback. + + @param objects The list of objects to fetch. + @param target The target on which the selector will be called. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSArray *)fetchedObjects error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `fetchedObjects` will the array of `PFObject` objects that were fetched. + */ ++ (void)fetchAllIfNeededInBackground:(PF_NULLABLE NSArray *)objects + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Fetching From Local Datastore +///-------------------------------------- + +/*! + @abstract *Synchronously* loads data from the local datastore into this object, + if it has not been fetched from the server already. + */ +- (void)fetchFromLocalDatastore; + +/*! + @abstract *Synchronously* loads data from the local datastore into this object, if it has not been fetched + from the server already. + + @discussion If the object is not stored in the local datastore, this `error` will be set to + return kPFErrorCacheMiss. + + @param error Pointer to an `NSError` that will be set if necessary. + */ +- (void)fetchFromLocalDatastore:(NSError **)error; + +/*! + @abstract *Asynchronously* loads data from the local datastore into this object, + if it has not been fetched from the server already. + + @returns The task that encapsulates the work being done. + */ +- (BFTask *)fetchFromLocalDatastoreInBackground; + +/*! + @abstract *Asynchronously* loads data from the local datastore into this object, + if it has not been fetched from the server already. + + @param block The block to execute. + It should have the following argument signature: `^(PFObject *object, NSError *error)`. + */ +- (void)fetchFromLocalDatastoreInBackgroundWithBlock:(PF_NULLABLE PFObjectResultBlock)block; + +///-------------------------------------- +/// @name Deleting an Object +///-------------------------------------- + +/*! + @abstract *Synchronously* deletes the `PFObject`. + + @returns Returns whether the delete succeeded. + */ +- (BOOL)delete; + +/*! + @abstract *Synchronously* deletes the `PFObject` and sets an error if it occurs. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the delete succeeded. + */ +- (BOOL)delete:(NSError **)error; + +/*! + @abstract Deletes the `PFObject` *asynchronously*. + + @returns The task that encapsulates the work being done. + */ +- (BFTask *)deleteInBackground; + +/*! + @abstract Deletes the `PFObject` *asynchronously* and executes the given callback block. + + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ +- (void)deleteInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block; + +/* + @abstract Deletes the `PFObject` *asynchronously* and calls the given callback. + + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ +- (void)deleteInBackgroundWithTarget:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +/*! + @abstract Deletes this object from the server at some unspecified time in the future, + even if Parse is currently inaccessible. + + @discussion Use this when you may not have a solid network connection, + and don't need to know when the delete completes. If there is some problem with the object + such that it can't be deleted, the request will be silently discarded. + + Delete instructions made with this method will be stored locally in an on-disk cache until they can be transmitted + to Parse. They will be sent immediately if possible. Otherwise, they will be sent the next time a network connection + is available. Delete requests will persist even after the app is closed, in which case they will be sent the + next time the app is opened. If more than 10MB of or commands are waiting + to be sent, subsequent calls to or will cause old requests to be silently discarded + until the connection can be re-established, and the queued requests can go through. + + @returns The task that encapsulates the work being done. + */ +- (BFTask *)deleteEventually; + +///-------------------------------------- +/// @name Dirtiness +///-------------------------------------- + +/*! + @abstract Gets whether any key-value pair in this object (or its children) + has been added/updated/removed and not saved yet. + + @returns Returns whether this object has been altered and not saved yet. + */ +- (BOOL)isDirty; + +/*! + @abstract Get whether a value associated with a key has been added/updated/removed and not saved yet. + + @param key The key to check for + + @returns Returns whether this key has been altered and not saved yet. + */ +- (BOOL)isDirtyForKey:(NSString *)key; + + +///-------------------------------------- +/// @name Pinning +///-------------------------------------- + +/*! + @abstract *Synchronously* stores the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @returns Returns whether the pin succeeded. + + @see unpin: + @see PFObjectDefaultPin + */ +- (BOOL)pin; + +/*! + @abstract *Synchronously* stores the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the pin succeeded. + + @see unpin: + @see PFObjectDefaultPin + */ +- (BOOL)pin:(NSError **)error; + +/*! + @abstract *Synchronously* stores the object and every object it points to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @param name The name of the pin. + + @returns Returns whether the pin succeeded. + + @see unpinWithName: + */ +- (BOOL)pinWithName:(NSString *)name; + +/*! + @abstract *Synchronously* stores the object and every object it points to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @param name The name of the pin. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the pin succeeded. + + @see unpinWithName: + */ +- (BOOL)pinWithName:(NSString *)name + error:(NSError **)error; + +/*! + @abstract *Asynchronously* stores the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @returns The task that encapsulates the work being done. + + @see unpinInBackground + @see PFObjectDefaultPin + */ +- (BFTask *)pinInBackground; + +/*! + @abstract *Asynchronously* stores the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see unpinInBackgroundWithBlock: + @see PFObjectDefaultPin + */ +- (void)pinInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract *Asynchronously* stores the object and every object it points to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @param name The name of the pin. + + @returns The task that encapsulates the work being done. + + @see unpinInBackgroundWithName: + */ +- (BFTask *)pinInBackgroundWithName:(NSString *)name; + +/*! + @abstract *Asynchronously* stores the object and every object it points to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + <[PFObject objectWithoutDataWithClassName:objectId:]> and then call on it. + + @param name The name of the pin. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see unpinInBackgroundWithName:block: + */ +- (void)pinInBackgroundWithName:(NSString *)name block:(PF_NULLABLE PFBooleanResultBlock)block; + +///-------------------------------------- +/// @name Pinning Many Objects +///-------------------------------------- + +/*! + @abstract *Synchronously* stores the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + + @returns Returns whether the pin succeeded. + + @see unpinAll: + @see PFObjectDefaultPin + */ ++ (BOOL)pinAll:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract *Synchronously* stores the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the pin succeeded. + + @see unpinAll:error: + @see PFObjectDefaultPin + */ ++ (BOOL)pinAll:(PF_NULLABLE NSArray *)objects error:(NSError **)error; + +/*! + @abstract *Synchronously* stores the objects and every object they point to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + @param name The name of the pin. + + @returns Returns whether the pin succeeded. + + @see unpinAll:withName: + */ ++ (BOOL)pinAll:(PF_NULLABLE NSArray *)objects withName:(NSString *)name; + +/*! + @abstract *Synchronously* stores the objects and every object they point to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + @param name The name of the pin. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the pin succeeded. + + @see unpinAll:withName:error: + */ ++ (BOOL)pinAll:(PF_NULLABLE NSArray *)objects + withName:(NSString *)name + error:(NSError **)error; + +/*! + @abstract *Asynchronously* stores the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + + @returns The task that encapsulates the work being done. + + @see unpinAllInBackground: + @see PFObjectDefaultPin + */ ++ (BFTask *)pinAllInBackground:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract *Asynchronously* stores the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see unpinAllInBackground:block: + @see PFObjectDefaultPin + */ ++ (void)pinAllInBackground:(PF_NULLABLE NSArray *)objects block:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract *Asynchronously* stores the objects and every object they point to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + @param name The name of the pin. + + @returns The task that encapsulates the work being done. + + @see unpinAllInBackground:withName: + */ ++ (BFTask *)pinAllInBackground:(PF_NULLABLE NSArray *)objects withName:(NSString *)name; + +/*! + @abstract *Asynchronously* stores the objects and every object they point to in the local datastore, recursively. + + @discussion If those other objects have not been fetched from Parse, they will not be stored. However, + if they have changed data, all the changes will be retained. To get the objects back later, you can + use a that uses <[PFQuery fromLocalDatastore]>, or you can create an unfetched pointer with + `[PFObject objectWithoutDataWithClassName:objectId:]` and then call `fetchFromLocalDatastore:` on it. + + @param objects The objects to be pinned. + @param name The name of the pin. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see unpinAllInBackground:withName:block: + */ ++ (void)pinAllInBackground:(PF_NULLABLE NSArray *)objects + withName:(NSString *)name + block:(PF_NULLABLE PFBooleanResultBlock)block; + +///-------------------------------------- +/// @name Unpinning +///-------------------------------------- + +/*! + @abstract *Synchronously* removes the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @returns Returns whether the unpin succeeded. + + @see pin: + @see PFObjectDefaultPin + */ +- (BOOL)unpin; + +/*! + @abstract *Synchronously* removes the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the unpin succeeded. + + @see pin: + @see PFObjectDefaultPin + */ +- (BOOL)unpin:(NSError **)error; + +/*! + @abstract *Synchronously* removes the object and every object it points to in the local datastore, recursively. + + @param name The name of the pin. + + @returns Returns whether the unpin succeeded. + + @see pinWithName: + */ +- (BOOL)unpinWithName:(NSString *)name; + +/*! + @abstract *Synchronously* removes the object and every object it points to in the local datastore, recursively. + + @param name The name of the pin. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the unpin succeeded. + + @see pinWithName:error: + */ +- (BOOL)unpinWithName:(NSString *)name + error:(NSError **)error; + +/*! + @abstract *Asynchronously* removes the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @returns The task that encapsulates the work being done. + + @see pinInBackground + @see PFObjectDefaultPin + */ +- (BFTask *)unpinInBackground; + +/*! + @abstract *Asynchronously* removes the object and every object it points to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see pinInBackgroundWithBlock: + @see PFObjectDefaultPin + */ +- (void)unpinInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract *Asynchronously* removes the object and every object it points to in the local datastore, recursively. + + @param name The name of the pin. + + @returns The task that encapsulates the work being done. + + @see pinInBackgroundWithName: + */ +- (BFTask *)unpinInBackgroundWithName:(NSString *)name; + +/*! + @abstract *Asynchronously* removes the object and every object it points to in the local datastore, recursively. + + @param name The name of the pin. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see pinInBackgroundWithName:block: + */ +- (void)unpinInBackgroundWithName:(NSString *)name block:(PF_NULLABLE PFBooleanResultBlock)block; + +///-------------------------------------- +/// @name Unpinning Many Objects +///-------------------------------------- + +/*! + @abstract *Synchronously* removes all objects in the local datastore + using a default pin name: `PFObjectDefaultPin`. + + @returns Returns whether the unpin succeeded. + + @see PFObjectDefaultPin + */ ++ (BOOL)unpinAllObjects; + +/*! + @abstract *Synchronously* removes all objects in the local datastore + using a default pin name: `PFObjectDefaultPin`. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the unpin succeeded. + + @see PFObjectDefaultPin + */ ++ (BOOL)unpinAllObjects:(NSError **)error; + +/*! + @abstract *Synchronously* removes all objects with the specified pin name. + + @param name The name of the pin. + + @returns Returns whether the unpin succeeded. + */ ++ (BOOL)unpinAllObjectsWithName:(NSString *)name; + +/*! + @abstract *Synchronously* removes all objects with the specified pin name. + + @param name The name of the pin. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the unpin succeeded. + */ ++ (BOOL)unpinAllObjectsWithName:(NSString *)name + error:(NSError **)error; + +/*! + @abstract *Asynchronously* removes all objects in the local datastore + using a default pin name: `PFObjectDefaultPin`. + + @returns The task that encapsulates the work being done. + + @see PFObjectDefaultPin + */ ++ (BFTask *)unpinAllObjectsInBackground; + +/*! + @abstract *Asynchronously* removes all objects in the local datastore + using a default pin name: `PFObjectDefaultPin`. + + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see PFObjectDefaultPin + */ ++ (void)unpinAllObjectsInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract *Asynchronously* removes all objects with the specified pin name. + + @param name The name of the pin. + + @returns The task that encapsulates the work being done. + */ ++ (BFTask *)unpinAllObjectsInBackgroundWithName:(NSString *)name; + +/*! + @abstract *Asynchronously* removes all objects with the specified pin name. + + @param name The name of the pin. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)unpinAllObjectsInBackgroundWithName:(NSString *)name block:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract *Synchronously* removes the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @param objects The objects. + + @returns Returns whether the unpin succeeded. + + @see pinAll: + @see PFObjectDefaultPin + */ ++ (BOOL)unpinAll:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract *Synchronously* removes the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @param objects The objects. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the unpin succeeded. + + @see pinAll:error: + @see PFObjectDefaultPin + */ ++ (BOOL)unpinAll:(PF_NULLABLE NSArray *)objects error:(NSError **)error; + +/*! + @abstract *Synchronously* removes the objects and every object they point to in the local datastore, recursively. + + @param objects The objects. + @param name The name of the pin. + + @returns Returns whether the unpin succeeded. + + @see pinAll:withName: + */ ++ (BOOL)unpinAll:(PF_NULLABLE NSArray *)objects withName:(NSString *)name; + +/*! + @abstract *Synchronously* removes the objects and every object they point to in the local datastore, recursively. + + @param objects The objects. + @param name The name of the pin. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the unpin succeeded. + + @see pinAll:withName:error: + */ ++ (BOOL)unpinAll:(PF_NULLABLE NSArray *)objects + withName:(NSString *)name + error:(NSError **)error; + +/*! + @abstract *Asynchronously* removes the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @param objects The objects. + + @returns The task that encapsulates the work being done. + + @see pinAllInBackground: + @see PFObjectDefaultPin + */ ++ (BFTask *)unpinAllInBackground:(PF_NULLABLE NSArray *)objects; + +/*! + @abstract *Asynchronously* removes the objects and every object they point to in the local datastore, recursively, + using a default pin name: `PFObjectDefaultPin`. + + @param objects The objects. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see pinAllInBackground:block: + @see PFObjectDefaultPin + */ ++ (void)unpinAllInBackground:(PF_NULLABLE NSArray *)objects block:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract *Asynchronously* removes the objects and every object they point to in the local datastore, recursively. + + @param objects The objects. + @param name The name of the pin. + + @returns The task that encapsulates the work being done. + + @see pinAllInBackground:withName: + */ ++ (BFTask *)unpinAllInBackground:(PF_NULLABLE NSArray *)objects withName:(NSString *)name; + +/*! + @abstract *Asynchronously* removes the objects and every object they point to in the local datastore, recursively. + + @param objects The objects. + @param name The name of the pin. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + + @see pinAllInBackground:withName:block: + */ ++ (void)unpinAllInBackground:(PF_NULLABLE NSArray *)objects + withName:(NSString *)name + block:(PF_NULLABLE PFBooleanResultBlock)block; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFObject.m b/Parse/PFObject.m new file mode 100644 index 000000000..c45b14a1c --- /dev/null +++ b/Parse/PFObject.m @@ -0,0 +1,2749 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObject.h" +#import "PFObject+Subclass.h" +#import "PFObjectSubclassingController.h" + +#import +#import +#import + +#import + +#import "BFTask+Private.h" +#import "PFACLPrivate.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFConstants.h" +#import "PFCoreManager.h" +#import "PFCurrentUserController.h" +#import "PFDateFormatter.h" +#import "PFDecoder.h" +#import "PFEncoder.h" +#import "PFErrorUtilities.h" +#import "PFEventuallyQueue_Private.h" +#import "PFFileManager.h" +#import "PFFile_Private.h" +#import "PFJSONSerialization.h" +#import "PFLogging.h" +#import "PFMacros.h" +#import "PFMultiProcessFileLockController.h" +#import "PFMutableObjectState.h" +#import "PFObjectBatchController.h" +#import "PFObjectConstants.h" +#import "PFObjectController.h" +#import "PFObjectEstimatedData.h" +#import "PFObjectFileCodingLogic.h" +#import "PFObjectFilePersistenceController.h" +#import "PFObjectLocalIdStore.h" +#import "PFObjectUtilities.h" +#import "PFOfflineStore.h" +#import "PFOperationSet.h" +#import "PFPin.h" +#import "PFPinningObjectStore.h" +#import "PFQueryPrivate.h" +#import "PFRESTObjectBatchCommand.h" +#import "PFRESTObjectCommand.h" +#import "PFRelation.h" +#import "PFRelationPrivate.h" +#import "PFSubclassing.h" +#import "PFTaskQueue.h" +#import "ParseInternal.h" +#import "Parse_Private.h" + +/*! + Checks if an object can be used as a value for PFObject. + */ +static void PFObjectAssertValueIsKindOfValidClass(id object) { + static NSArray *classes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + classes = @[ [NSDictionary class], [NSArray class], + [NSString class], [NSNumber class], [NSNull class], [NSDate class], [NSData class], + [PFObject class], [PFFile class], [PFACL class], [PFGeoPoint class] ]; + }); + + for (Class class in classes) { + if ([object isKindOfClass:class]) { + return; + } + } + + PFParameterAssert(NO, @"PFObject values may not have class: %@", [object class]); +} + +/*! + Checks if a class is a of container kind to be used as a value for PFObject. + */ +static BOOL PFObjectValueIsKindOfMutableContainerClass(id object) { + static NSArray *classes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + classes = @[ [NSDictionary class], [NSArray class], [PFACL class], [PFGeoPoint class] ]; + }); + + for (Class class in classes) { + if ([object isKindOfClass:class]) { + return YES; + } + } + + return NO; +} + +@interface PFObject () { + // A lock for accessing any of the internal state of this object. + // Guards basically all of the variables below. + NSObject *lock; + + PFObjectState *_state; + + PFObjectEstimatedData *_estimatedData; + NSMutableSet *_availableKeys; // TODO: (nlutsenko) Maybe decouple this further. + + // TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count. + int _deletingEventually; + + // A dictionary that maps id (objects) => PFJSONCache + NSMutableDictionary *hashedObjectsCache; + + NSString *localId; + + // This queue is used to guarantee the order of *Eventually commands + // and offload all the work to the background thread + PFTaskQueue *_eventuallyTaskQueue; +} + +@property (nonatomic, strong, readwrite) NSString *localId; + +@property (nonatomic, strong, readwrite) PFTaskQueue *taskQueue; + ++ (void)assertSubclassIsRegistered:(Class)subclass; + +@end + +@implementation PFObject (Private) + ++ (void)unregisterSubclass:(Class)subclass { + [[self subclassingController] unregisterSubclass:subclass]; +} + +/*! + Returns the object that should be used to synchronize all internal data access. + */ +- (NSObject *)lock { + return lock; +} + +/*! + Blocks until all outstanding operations have completed. + */ +- (void)waitUntilFinished { + [[self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return toAwait; + }] waitForResult:nil]; +} + +/*! + For operations that need to be put into multiple objects queues, like saveAll + and fetchAll, this method does the nasty work. + @param taskStart - A block that is called when all of the objects are ready. + It can return a promise that all of the queues will then wait on. + @param objects - The objects that this operation affects. + @returns - Returns a promise that is fulfilled once the promise returned by the + block is fulfilled. + */ ++ (BFTask *)_enqueue:(BFTask *(^)(BFTask *toAwait))taskStart forObjects:(NSArray *)objects { + // The task that will be complete when all of the child queues indicate they're ready to start. + BFTaskCompletionSource *readyToStart = [BFTaskCompletionSource taskCompletionSource]; + + // First, we need to lock the mutex for the queue for every object. We have to hold this + // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so + // that saves actually get executed in the order they were setup by taskStart(). + // The locks have to be sorted so that we always acquire them in the same order. + // Otherwise, there's some risk of deadlock. + NSMutableArray *mutexes = [NSMutableArray array]; + for (PFObject *obj in objects) { + [mutexes addObject:obj.taskQueue.mutex]; + } + [mutexes sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { + void *lock1 = (__bridge void *)obj1; + void *lock2 = (__bridge void *)obj2; + return lock1 - lock2; + }]; + for (NSObject *lock in mutexes) { + objc_sync_enter(lock); + } + + @try { + // The task produced by taskStart. By running this immediately, we allow everything prior + // to toAwait to run before waiting for all of the queues on all of the objects. + BFTask *fullTask = taskStart(readyToStart.task); + + // Add fullTask to each of the objects' queues. + NSMutableArray *childTasks = [NSMutableArray array]; + for (PFObject *obj in objects) { + [obj.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + [childTasks addObject:toAwait]; + return fullTask; + }]; + } + + // When all of the objects' queues are ready, signal fullTask that it's ready to go on. + [[BFTask taskForCompletionOfAllTasks:childTasks] continueWithBlock:^id(BFTask *task) { + readyToStart.result = nil; + return nil; + }]; + + return fullTask; + + } @finally { + for (NSObject *lock in mutexes) { + objc_sync_exit(lock); + } + } +} + +///-------------------------------------- +#pragma mark - Children helpers +///-------------------------------------- + +/*! + Finds all of the objects that are reachable from child, including child itself, + and adds them to the given mutable array. It traverses arrays and json objects. + @param node An kind object to search for children. + @param dirtyChildren The array to collect the result into. + @param seen The set of all objects that have already been seen. + @param seenNew The set of new objects that have already been seen since the + last existing object. + */ ++ (void)collectDirtyChildren:(id)node + children:(NSMutableSet *)dirtyChildren + files:(NSMutableSet *)dirtyFiles + seen:(NSSet *)seen + seenNew:(NSSet *)seenNew + currentUser:(PFUser *)currentUser { + if ([node isKindOfClass:[NSArray class]]) { + for (id elem in node) { + @autoreleasepool { + [PFObject collectDirtyChildren:elem + children:dirtyChildren + files:dirtyFiles + seen:seen + seenNew:seenNew + currentUser:currentUser]; + } + } + } else if ([node isKindOfClass:[NSDictionary class]]) { + [node enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [PFObject collectDirtyChildren:obj + children:dirtyChildren + files:dirtyFiles + seen:seen + seenNew:seenNew + currentUser:currentUser]; + }]; + } else if ([node isKindOfClass:[PFACL class]]) { + PFACL *acl = (PFACL *)node; + if ([acl hasUnresolvedUser]) { + [PFObject collectDirtyChildren:currentUser + children:dirtyChildren + files:dirtyFiles + seen:seen + seenNew:seenNew + currentUser:currentUser]; + } + + } else if ([node isKindOfClass:[PFObject class]]) { + PFObject *object = (PFObject *)node; + + @synchronized ([object lock]) { + // Check for cycles of new objects. Any such cycle means it will be + // impossible to save this collection of objects, so throw an exception. + if (object.objectId) { + seenNew = [NSSet set]; + } else { + if ([seenNew containsObject:object]) { + [NSException raise:NSInternalInconsistencyException + format:@"Found a circular dependency when saving."]; + } + seenNew = [seenNew setByAddingObject:object]; + } + + // Check for cycles of any object. If this occurs, then there's no + // problem, but we shouldn't recurse any deeper, because it would be + // an infinite recursion. + if ([seen containsObject:object]) { + return; + } + seen = [seen setByAddingObject:object]; + + // Recurse into this object's children looking for dirty children. + // We only need to look at the child object's current estimated data, + // because that's the only data that might need to be saved now. + [PFObject collectDirtyChildren:object->_estimatedData.dictionaryRepresentation + children:dirtyChildren + files:dirtyFiles + seen:seen + seenNew:seenNew + currentUser:currentUser]; + + if ([object isDirty:NO]) { + [dirtyChildren addObject:object]; + } + } + + } else if ([node isKindOfClass:[PFFile class]]) { + PFFile *file = (PFFile *)node; + if (!file.url) { + [dirtyFiles addObject:node]; + } + } +} + +// Helper version of collectDirtyChildren:children:seen:seenNew so that callers +// don't have to add the internally used parameters. ++ (void)collectDirtyChildren:(id)child + children:(NSMutableSet *)dirtyChildren + files:(NSMutableSet *)dirtyFiles + currentUser:(PFUser *)currentUser { + [PFObject collectDirtyChildren:child + children:dirtyChildren + files:dirtyFiles + seen:[NSSet set] + seenNew:[NSSet set] + currentUser:currentUser]; +} + +// Returns YES if the given object can be serialized for saving as a value +// that is pointed to by a PFObject. +// @param value The object we want to serialize as a value. +// @param saved The set of all objects we can assume will be saved before this one. +// @param error The reason why it can't be serialized. ++ (BOOL)canBeSerializedAsValue:(id)value + afterSaving:(NSMutableArray *)saved + error:(NSError **)error { + if ([value isKindOfClass:[PFObject class]]) { + PFObject *object = (PFObject *)value; + if (!object.objectId && ![saved containsObject:object]) { + if (error) { + *error = [PFErrorUtilities errorWithCode:kPFErrorInvalidPointer + message:@"Pointer to an unsaved object."]; + } + return NO; + } + + } else if ([value isKindOfClass:[NSDictionary class]]) { + __block BOOL retValue = YES; + [value enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if (![[self class] canBeSerializedAsValue:obj + afterSaving:saved + error:error]) { + retValue = NO; + *stop = YES; + } + }]; + return retValue; + } else if ([value isKindOfClass:[NSArray class]]) { + NSArray *array = (NSArray *)value; + for (NSString *item in array) { + if (![[self class] canBeSerializedAsValue:item + afterSaving:saved + error:error]) { + return NO; + } + } + } + + return YES; +} + +// Returns YES if this object can be serialized for saving. +// @param saved A set of objects that we can assume will have been saved. +// @param error The reason why it can't be serialized. +- (BOOL)canBeSerializedAfterSaving:(NSMutableArray *)saved withCurrentUser:(PFUser *)user error:(NSError **)error { + @synchronized (lock) { + // This method is only used for batching sets of objects for saveAll + // and when saving children automatically. Since it's only used to + // determine whether or not save should be called on them, it only + // needs to examine their current values, so we use estimatedData. + if (![[self class] canBeSerializedAsValue:_estimatedData.dictionaryRepresentation + afterSaving:saved + error:error]) { + return NO; + } + + if ([self isDataAvailableForKey:@"ACL"] && + [[self ACLWithoutCopying] hasUnresolvedUser] && + ![saved containsObject:user]) { + if (error) { + *error = [PFErrorUtilities errorWithCode:kPFErrorInvalidACL + message:@"User associated with ACL must be signed up."]; + } + return NO; + } + + return YES; + } +} + +// Delete all objects in the array. ++ (BFTask *)deleteAllAsync:(NSArray *)objects withSessionToken:(NSString *)sessionToken { + if ([objects count] == 0) { + return [BFTask taskWithResult:@YES]; + } + + return [[[BFTask taskFromExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id{ + return [PFObject _enqueue:^BFTask *(BFTask *toAwait) { + NSMutableSet *uniqueObjects = [NSMutableSet set]; + NSMutableArray *commands = [NSMutableArray arrayWithCapacity:[objects count]]; + for (PFObject *object in objects) { + @synchronized (object->lock) { + //Just continue, there is no action to be taken here + if (!object.objectId) { + continue; + } + + NSString *uniqueCheck = [NSString stringWithFormat:@"%@-%@", + object.parseClassName, + [object objectId]]; + if (![uniqueObjects containsObject:uniqueCheck]) { + [object checkDeleteParams]; + + [commands addObject:[object _currentDeleteCommandWithSessionToken:sessionToken]]; + [uniqueObjects addObject:uniqueCheck]; + } + } + } + + // Batch requests have currently a limit of 50 packaged requests per single request + // This splitting will split the overall array into segments of upto 50 requests + // and execute them concurrently with a wrapper task for all of them. + NSArray *commandBatches = [PFInternalUtils arrayBySplittingArray:commands + withMaximumComponentsPerSegment:PFRESTObjectBatchCommandSubcommandsLimit]; + NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:[commandBatches count]]; + for (NSArray *commandBatch in commandBatches) { + PFRESTCommand *command = [PFRESTObjectBatchCommand batchCommandWithCommands:commandBatch + sessionToken:sessionToken]; + BFTask *task = [[[Parse _currentManager].commandRunner runCommandAsync:command withOptions:0] + continueAsyncWithSuccessBlock:^id(BFTask *task) { + NSArray *results = [task.result result]; + for (NSDictionary *result in results) { + NSDictionary *errorResult = result[@"error"]; + if (errorResult) { + NSError *error = [PFErrorUtilities errorFromResult:errorResult]; + return [BFTask taskWithError:error]; + } + } + + return task; + }]; + [tasks addObject:task]; + } + return [BFTask taskForCompletionOfAllTasks:tasks]; + } forObjects:objects]; + }] continueWithBlock:^id(BFTask *task) { + if (!task.exception) { + return task; + } + + // Return the first exception, instead of the aggregated one + // for the sake of compatability with old versions + + if ([task.exception.name isEqualToString:BFTaskMultipleExceptionsException]) { + NSException *firstException = [task.exception.userInfo[@"exceptions"] firstObject]; + if (firstException) { + return [BFTask taskWithException:firstException]; + } + } + + return task; + }] continueWithSuccessResult:@YES]; +} + +// This saves all of the objects and files reachable from the given object. +// It does its work in multiple waves, saving as many as possible in each wave. +// If there's ever an error, it just gives up, sets error, and returns NO; ++ (BFTask *)_deepSaveAsync:(id)object withCurrentUser:(PFUser *)currentUser sessionToken:(NSString *)sessionToken { + BFTask *task = [BFTask taskWithResult:@YES]; + + NSMutableSet *uniqueObjects = [NSMutableSet set]; + NSMutableSet *uniqueFiles = [NSMutableSet set]; + [PFObject collectDirtyChildren:object children:uniqueObjects files:uniqueFiles currentUser:currentUser]; + for (PFFile *file in uniqueFiles) { + task = [task continueAsyncWithSuccessBlock:^id(BFTask *task) { + return [[file saveInBackground] continueAsyncWithBlock:^id(BFTask *task) { + // This is a stupid hack because our current behavior is to fail file + // saves with an error when a file save inside it is cancelled. + if (task.isCancelled) { + NSError *newError = [PFErrorUtilities errorWithCode:kPFErrorUnsavedFile + message:@"A file save was cancelled."]; + return [BFTask taskWithError:newError]; + } + return task; + }]; + }]; + } + + // TODO: (nlutsenko) Get rid of this once we allow localIds in batches. + NSArray *remaining = [uniqueObjects allObjects]; + NSMutableArray *finished = [NSMutableArray array]; + while ([remaining count] > 0) { + // Partition the objects into two sets: those that can be save immediately, + // and those that rely on other objects to be created first. + NSMutableArray *current = [NSMutableArray array]; + NSMutableArray *nextBatch = [NSMutableArray array]; + for (PFObject *object in remaining) { + if ([object canBeSerializedAfterSaving:finished withCurrentUser:currentUser error:nil]) { + [current addObject:object]; + } else { + [nextBatch addObject:object]; + } + } + remaining = nextBatch; + + if (current.count == 0) { + // We do cycle-detection when building the list of objects passed to this + // function, so this should never get called. But we should check for it + // anyway, so that we get an exception instead of an infinite loop. + [NSException raise:NSInternalInconsistencyException + format:@"Unable to save a PFObject with a relation to a cycle."]; + } + + // If a lazy user is one of the objects in the array, resolve its laziness now and + // remove it from the list of things to save. + // + // This has to happen separately from everything else because there [PFUser save] + // is special-cased to work for lazy users, but new users can't be created by + // PFMultiCommand's regular save. + if ([currentUser isLazy] && [current containsObject:currentUser]) { + task = [task continueAsyncWithSuccessBlock:^id(BFTask *task) { + return [currentUser saveInBackground]; + }]; + + [finished addObject:currentUser]; + [current removeObject:currentUser]; + if (current.count == 0) { + continue; + } + } + + task = [task continueAsyncWithSuccessBlock:^id(BFTask *task) { + // Batch requests have currently a limit of 50 packaged requests per single request + // This splitting will split the overall array into segments of upto 50 requests + // and execute them concurrently with a wrapper task for all of them. + NSArray *objectBatches = [PFInternalUtils arrayBySplittingArray:current + withMaximumComponentsPerSegment:PFRESTObjectBatchCommandSubcommandsLimit]; + NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:[objectBatches count]]; + + for (NSArray *objectBatch in objectBatches) { + BFTask *batchTask = [PFObject _enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + NSMutableArray *commands = [NSMutableArray arrayWithCapacity:[objectBatch count]]; + for (PFObject *object in objectBatch) { + PFRESTCommand *command = nil; + @synchronized ([object lock]) { + [object _checkSaveParametersWithCurrentUser:currentUser]; + command = [object _constructSaveCommandForChanges:[object unsavedChanges] + sessionToken:sessionToken + objectEncoder:[PFPointerObjectEncoder objectEncoder]]; + [object startSave]; + } + [commands addObject:command]; + } + + PFRESTCommand *batchCommand = [PFRESTObjectBatchCommand batchCommandWithCommands:commands + sessionToken:sessionToken]; + return [[[Parse _currentManager].commandRunner runCommandAsync:batchCommand withOptions:0] + continueAsyncWithBlock:^id(BFTask *commandRunnerTask) { + NSArray *results = [commandRunnerTask.result result]; + + NSMutableArray *handleSaveTasks = [NSMutableArray arrayWithCapacity:[objectBatch count]]; + + __block NSError *error = task.error; + [objectBatch enumerateObjectsUsingBlock:^(PFObject *object, NSUInteger idx, BOOL *stop) { + // If the task resulted in an error - don't even bother looking into + // the result of the command, just roll the error further + + BFTask *task = nil; + if (commandRunnerTask.error) { + task = [object handleSaveResultAsync:nil]; + } else { + NSDictionary *commandResult = results[idx]; + + NSDictionary *errorResult = commandResult[@"error"]; + if (errorResult) { + error = [PFErrorUtilities errorFromResult:errorResult]; + task = [[object handleSaveResultAsync:nil] continueWithBlock:^id(BFTask *task) { + return [BFTask taskWithError:error]; + }]; + } else { + NSDictionary *successfulResult = commandResult[@"success"]; + task = [object handleSaveResultAsync:successfulResult]; + } + } + [handleSaveTasks addObject:task]; + }]; + + return [[BFTask taskForCompletionOfAllTasks:handleSaveTasks] continueAsyncWithBlock:^id(BFTask *task) { + if (commandRunnerTask.error || commandRunnerTask.cancelled || commandRunnerTask.exception) { + return commandRunnerTask; + } + + // Reiterate saveAll tasks, return first error. + for (BFTask *handleSaveTask in handleSaveTasks) { + if (handleSaveTask.error || handleSaveTask.exception) { + return handleSaveTask; + } + } + + return @YES; + }]; + }]; + }]; + } forObjects:objectBatch]; + [tasks addObject:batchTask]; + } + + return [[BFTask taskForCompletionOfAllTasks:tasks] continueWithBlock:^id(BFTask *task) { + // Return the first exception, instead of the aggregated one + // for the sake of compatability with old versions + + if ([task.exception.name isEqualToString:BFTaskMultipleExceptionsException]) { + NSException *firstException = [task.exception.userInfo[@"exceptions"] firstObject]; + if (firstException) { + return [BFTask taskWithException:firstException]; + } + } + + if (task.error || task.cancelled || task.exception) { + return task; + } + + return @YES; + }]; + }]; + + [finished addObjectsFromArray:current]; + } + + return task; +} + +// Just like deepSaveAsync, but uses saveEventually instead of saveAsync. +// Because you shouldn't wait for saveEventually calls to complete, this +// does not return any operation. ++ (BFTask *)_enqueueSaveEventuallyChildrenOfObject:(PFObject *)object + currentUser:(PFUser *)currentUser { + return [BFTask taskFromExecutor:[BFExecutor defaultExecutor] withBlock:^id{ + NSMutableSet *uniqueObjects = [NSMutableSet set]; + NSMutableSet *uniqueFiles = [NSMutableSet set]; + [PFObject collectDirtyChildren:object children:uniqueObjects files:uniqueFiles currentUser:currentUser]; + for (PFFile *file in uniqueFiles) { + if (!file.url) { + NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Unable to saveEventually a PFObject with a relation to a new, unsaved PFFile." + userInfo:nil]; + return [BFTask taskWithException:exception]; + } + } + + // Remove object from the queue of objects to save as this method should only save children. + [uniqueObjects removeObject:object]; + + NSArray *remaining = [uniqueObjects allObjects]; + NSMutableArray *finished = [NSMutableArray array]; + NSMutableArray *enqueueTasks = [NSMutableArray array]; + while ([remaining count] > 0) { + // Partition the objects into two sets: those that can be save immediately, + // and those that rely on other objects to be created first. + NSMutableArray *current = [NSMutableArray array]; + NSMutableArray *nextBatch = [NSMutableArray array]; + for (PFObject *object in remaining) { + if ([object canBeSerializedAfterSaving:finished withCurrentUser:currentUser error:nil]) { + [current addObject:object]; + } else { + [nextBatch addObject:object]; + } + } + remaining = nextBatch; + + if (current.count == 0) { + // We do cycle-detection when building the list of objects passed to this + // function, so this should never get called. But we should check for it + // anyway, so that we get an exception instead of an infinite loop. + [NSException raise:NSInternalInconsistencyException + format:@"Unable to save a PFObject with a relation to a cycle."]; + } + + // If a lazy user is one of the objects in the array, resolve its laziness now and + // remove it from the list of things to save. + // + // This has to happen separately from everything else because there [PFUser save] + // is special-cased to work for lazy users, but new users can't be created by + // PFMultiCommand's regular save. + // + // Unfortunately, ACLs with lazy users still cannot be saved, because the ACL does + // does not get updated after the user save completes. + // TODO: (nlutsenko) Make the ACL update after the user is saved. + if ([currentUser isLazy] && [current containsObject:currentUser]) { + [enqueueTasks addObject:[currentUser _enqueueSaveEventuallyWithChildren:NO]]; + [finished addObject:currentUser]; + [current removeObject:currentUser]; + if (current.count == 0) { + continue; + } + } + + // TODO: (nlutsenko) Allow batching with saveEventually. + for (PFObject *object in current) { + [enqueueTasks addObject:[object _enqueueSaveEventuallyWithChildren:NO]]; + } + + [finished addObjectsFromArray:current]; + } + return [BFTask taskForCompletionOfAllTasks:enqueueTasks]; + }]; +} + +- (BFTask *)_saveChildrenInBackgroundWithCurrentUser:(PFUser *)currentUser sessionToken:(NSString *)sessionToken { + @synchronized (lock) { + return [PFObject _deepSaveAsync:_estimatedData.dictionaryRepresentation + withCurrentUser:currentUser + sessionToken:sessionToken]; + } +} + +///-------------------------------------- +#pragma mark - Dirtiness helper +///-------------------------------------- + +- (BOOL)isDirty:(BOOL)considerChildren { + @synchronized (lock) { + [self checkForChangesToMutableContainers]; + if (self._state.deleted || dirty || [self _hasChanges]) { + return YES; + } + if (considerChildren) { + // We only need to consider the currently estimated children here, + // because they're the only ones that might need to be saved in a + // subsequent call to save, which is the meaning of "dirtiness". + __block BOOL retValue = NO; + [_estimatedData enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + if ([obj isKindOfClass:[PFObject class]] && [obj isDirty]) { + retValue = YES; + *stop = YES; + } + }]; + return retValue; + } + return NO; + } +} + +- (void)_setDirty:(BOOL)aDirty { + @synchronized (lock) { + dirty = aDirty; + } +} + +///-------------------------------------- +#pragma mark - Mutable container management +///-------------------------------------- + +- (void)checkpointAllMutableContainers { + @synchronized (lock) { + [_estimatedData enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + [self checkpointMutableContainer:obj]; + }]; + } +} + +- (void)checkpointMutableContainer:(id)object { + @synchronized (lock) { + if (PFObjectValueIsKindOfMutableContainerClass(object)) { + [hashedObjectsCache setObject:[PFJSONCacheItem cacheFromObject:object] + forKey:[NSValue valueWithNonretainedObject:object]]; + } + } +} + +- (void)checkForChangesToMutableContainer:(id)object forKey:(NSString *)key { + @synchronized (lock) { + // If this is a mutable container, we should check its contents. + if (PFObjectValueIsKindOfMutableContainerClass(object)) { + PFJSONCacheItem *oldCacheItem = [hashedObjectsCache objectForKey:[NSValue valueWithNonretainedObject:object]]; + if (!oldCacheItem) { + [NSException raise:NSInternalInconsistencyException + format:@"PFObject contains container item that isn't cached."]; + } else { + PFJSONCacheItem *newCacheItem = [PFJSONCacheItem cacheFromObject:object]; + if (![oldCacheItem isEqual:newCacheItem]) { + // A mutable container changed out from under us. Treat it as a set operation. + [self setObject:object forKey:key]; + } + } + } else { + [hashedObjectsCache removeObjectForKey:[NSValue valueWithNonretainedObject:object]]; + } + } +} + +- (void)checkForChangesToMutableContainers { + @synchronized (lock) { + NSMutableArray *unexaminedCacheKeys = [[hashedObjectsCache allKeys] mutableCopy]; + NSDictionary *reachableData = _estimatedData.dictionaryRepresentation; + [reachableData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [unexaminedCacheKeys removeObject:[NSValue valueWithNonretainedObject:obj]]; + [self checkForChangesToMutableContainer:obj forKey:key]; + }]; + + // Remove unchecked cache entries. + [hashedObjectsCache removeObjectsForKeys:unexaminedCacheKeys]; + } +} + +///-------------------------------------- +#pragma mark - Data Availability +///-------------------------------------- + +// TODO: (nlutsenko) Remove this when rest of PFObject is decoupled. +- (void)setHasBeenFetched:(BOOL)fetched { + @synchronized (lock) { + if (self._state.complete != fetched) { + PFMutableObjectState *state = [_state mutableCopy]; + state.complete = fetched; + self._state = state; + } + } +} + +- (void)_setDeleted:(BOOL)deleted { + @synchronized (lock) { + if (self._state.deleted != deleted) { + PFMutableObjectState *state = [_state mutableCopy]; + state.deleted = deleted; + self._state = state; + } + } +} + +- (BOOL)isDataAvailableForKey:(NSString *)key { + if (!key) { + return NO; + } + + @synchronized (lock) { + if ([self isDataAvailable]) { + return YES; + } + return [_availableKeys containsObject:key]; + } +} + +///-------------------------------------- +#pragma mark - Validations +///-------------------------------------- + +// Validations that are done on delete. For now, there is nothing. +- (void)checkDeleteParams { + return; +} + +// Validations that are done on save. For now, there is nothing. +- (void)_checkSaveParametersWithCurrentUser:(PFUser *)currentUser { + return; +} + +/*! + Checks if Parse class name could be used to initialize a given instance of PFObject or it's subclass. + */ ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert(className, @"Class name can't be 'nil'."); + PFParameterAssert(![className hasPrefix:@"_"], @"Invalid class name. Class names cannot start with an underscore."); +} + + +///-------------------------------------- +#pragma mark - Serialization helpers +///-------------------------------------- + +- (NSString *)getOrCreateLocalId { + @synchronized(lock) { + if (!self.localId) { + PFConsistencyAssert(!self._state.objectId, + @"A localId should not be created for an object with an objectId."); + self.localId = [[Parse _currentManager].coreManager.objectLocalIdStore createLocalId]; + } + } + return self.localId; +} + +- (void)resolveLocalId { + @synchronized (lock) { + PFConsistencyAssert(self.localId, @"Tried to resolve a localId for an object with no localId."); + NSString *newObjectId = [[Parse _currentManager].coreManager.objectLocalIdStore objectIdForLocalId:self.localId]; + + // If we are resolving local ids, then this object is about to go over the network. + // But if it has local ids that haven't been resolved yet, then that's not going to + // be possible. + if (!newObjectId) { + [NSException raise:NSInternalInconsistencyException + format:@"Tried to save an object with a pointer to a new, unsaved object."]; + } + + // Nil out the localId so that the new objectId won't be saved back to the PFObjectLocalIdStore. + self.localId = nil; + self.objectId = newObjectId; + } +} + ++ (id)_objectFromDictionary:(NSDictionary *)dictionary + defaultClassName:(NSString *)defaultClassName + completeData:(BOOL)completeData { + return [self _objectFromDictionary:dictionary + defaultClassName:defaultClassName + completeData:completeData + decoder:[PFDecoder objectDecoder]]; +} + +// When merging results from a query, ensure that any supplied `selectedKeys` are marked as available. This special +// handling is necessary because keys with an `undefined` value are not guaranteed to be included in the server's +// response data. +// +// See T3336562 ++ (id)_objectFromDictionary:(NSDictionary *)dictionary + defaultClassName:(NSString *)defaultClassName + selectedKeys:(NSArray *)selectedKeys { + PFObject *result = [self _objectFromDictionary:dictionary + defaultClassName:defaultClassName + completeData:(selectedKeys == nil) + decoder:[PFDecoder objectDecoder]]; + [result->_availableKeys addObjectsFromArray:selectedKeys]; + return result; +} + +/*! + Creates a PFObject from a dictionary object. + + @param dictionary Undecoded dictionary. + @param defaultClassName The className of the resulting object if none is given by the dictionary. + @param completeData Whether to use complete data. + @param decoder Decoder used to decode the dictionary. + */ ++ (id)_objectFromDictionary:(NSDictionary *)dictionary + defaultClassName:(NSString *)defaultClassName + completeData:(BOOL)completeData + decoder:(PFDecoder *)decoder { + NSString *objectId = nil; + NSString *className = nil; + if (dictionary != nil) { + objectId = dictionary[@"objectId"]; + className = dictionary[@"className"] ?: defaultClassName; + } + PFObject *object = [PFObject objectWithoutDataWithClassName:className objectId:objectId]; + [object _mergeAfterFetchWithResult:dictionary decoder:decoder completeData:completeData]; + return object; +} + +/*! + When the app was previously a non-LDS app and want to enable LDS, currentUser and currentInstallation + will be discarded if we don't migrate them. This is a helper method to migrate user/installation + from disk to pin. + + @param fileName the file in which the object was saved. + @param pinName the name of the pin in which the object should be stored. + */ ++ (BFTask *)_migrateObjectInBackgroundFromFile:(NSString *)fileName + toPin:(NSString *)pinName { + return [self _migrateObjectInBackgroundFromFile:fileName toPin:pinName usingMigrationBlock:nil]; +} + +/*! + When the app was previously a non-LDS app and want to enable LDS, currentUser and currentInstallation + will be discarded if we don't migrate them. This is a helper method to migrate user/installation + from disk to pin. + + @param fileName the file in which the object was saved. + @param pinName the name of the pin in which the object should be stored. + @param migrationBlock The block that will be called if there is an object on disk and before the object is pinned. + */ ++ (BFTask *)_migrateObjectInBackgroundFromFile:(NSString *)fileName + toPin:(NSString *)pinName + usingMigrationBlock:(BFContinuationBlock)migrationBlock { + PFObjectFilePersistenceController *controller = [Parse _currentManager].coreManager.objectFilePersistenceController; + BFTask *task = [controller loadPersistentObjectAsyncForKey:fileName]; + if (migrationBlock) { + task = [task continueWithSuccessBlock:^id(BFTask *task) { + PFObject *object = task.result; + if (object) { + return [[task continueWithBlock:migrationBlock] continueWithResult:object]; + } + return task; + }]; + } + return [task continueWithSuccessBlock:^id(BFTask *task) { + PFObject *object = task.result; + return [[object _pinInBackgroundWithName:pinName includeChildren:NO] continueWithBlock:^id(BFTask *task) { + BFTask *resultTask = [BFTask taskWithResult:object]; + + // Only delete if we successfully pin it so that it retries the migration next time. + if (!task.error && !task.exception && !task.cancelled) { + NSString *path = [[Parse _currentManager].fileManager parseDataItemPathForPathComponent:fileName]; + return [[PFFileManager removeItemAtPathAsync:path] continueWithBlock:^id(BFTask *task) { + // We don't care if it fails to delete the file, so return the + return resultTask; + }]; + } + return resultTask; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - REST operations +///-------------------------------------- + +/*! + Encodes parse object into NSDictionary suitable for persisting into LDS. + */ +- (NSDictionary *)RESTDictionaryWithObjectEncoder:(PFEncoder *)objectEncoder + operationSetUUIDs:(NSArray **)operationSetUUIDs { + @synchronized (lock) { + [self checkForChangesToMutableContainers]; + PFObjectState *state = self._state; + return [self RESTDictionaryWithObjectEncoder:objectEncoder + operationSetUUIDs:operationSetUUIDs + state:state + operationSetQueue:operationSetQueue]; + } +} + +- (NSDictionary *)RESTDictionaryWithObjectEncoder:(PFEncoder *)objectEncoder + operationSetUUIDs:(NSArray **)operationSetUUIDs + state:(PFObjectState *)state + operationSetQueue:(NSArray *)queue { + NSMutableDictionary *result = [[state dictionaryRepresentationWithObjectEncoder:objectEncoder] mutableCopy]; + result[PFObjectClassNameRESTKey] = state.parseClassName; + result[PFObjectCompleteRESTKey] = @(state.complete); + + result[PFObjectIsDeletingEventuallyRESTKey] = @(_deletingEventually); + + // TODO (hallucinogen): based on some note from Android's toRest, we'll need to put this + // stuff somewhere else + NSMutableArray *operations = [NSMutableArray array]; + NSMutableArray *mutableOperationSetUUIDs = [NSMutableArray array]; + for (PFOperationSet *operation in queue) { + NSArray *ooSetUUIDs = nil; + [operations addObject:[operation RESTDictionaryUsingObjectEncoder:objectEncoder + operationSetUUIDs:&ooSetUUIDs]]; + [mutableOperationSetUUIDs addObjectsFromArray:ooSetUUIDs]; + } + + *operationSetUUIDs = mutableOperationSetUUIDs; + + result[PFObjectOperationsRESTKey] = operations; + return result; +} + +- (void)mergeFromRESTDictionary:(NSDictionary *)object withDecoder:(PFDecoder *)decoder { + @synchronized (lock) { + BOOL mergeServerData = NO; + + PFMutableObjectState *state = [self._state mutableCopy]; + + // If LDS has `updatedAt` and we have it - compare, then if stuff is newer - merge. + // If LDS doesn't have `updatedAt` and we don't have it - merge anyway. + NSString *updatedAtString = object[PFObjectUpdatedAtRESTKey]; + if (updatedAtString) { + NSDate *updatedDate = [[PFDateFormatter sharedFormatter] dateFromString:updatedAtString]; + mergeServerData = ([state.updatedAt compare:updatedDate] != NSOrderedDescending); + } else if (!state.updatedAt) { + mergeServerData = YES; + } + [object enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([key isEqualToString:PFObjectOperationsRESTKey]) { + PFOperationSet *remoteOperationSet = nil; + NSArray *operations = (NSArray *)obj; + if ([operations count] > 0) { + // Add and enqueue any saveEventually operations, roll forward any other + // operations sets (operations sets here are generally failed/incomplete saves). + PFOperationSet *current = nil; + for (id rawOperationSet in operations) { + PFOperationSet *operationSet = [PFOperationSet operationSetFromRESTDictionary:rawOperationSet + usingDecoder:decoder]; + if (operationSet.saveEventually) { + if (current != nil) { + [[self unsavedChanges] mergeOperationSet:current]; + current = nil; + } + + // Check if queue already contains this operation set and discard it if does + if (![self _containsOperationSet:operationSet]) { + // Insert the `saveEventually` operationSet before the last operation set at all times. + NSUInteger index = ([operationSetQueue count] == 0 ? 0 : [operationSetQueue count] - 1); + [operationSetQueue insertObject:operationSet atIndex:index]; + [self _enqueueSaveEventuallyOperationAsync:operationSet]; + } + + continue; + } + + if (current != nil) { + [operationSet mergeOperationSet:current]; + } + current = operationSet; + } + if (current != nil) { + remoteOperationSet = current; + } + } + + PFOperationSet *localOperationSet = [self unsavedChanges]; + if (localOperationSet.updatedAt != nil && + [localOperationSet.updatedAt compare:remoteOperationSet.updatedAt] != NSOrderedAscending) { + [localOperationSet mergeOperationSet:remoteOperationSet]; + } else { + NSUInteger index = [operationSetQueue indexOfObject:localOperationSet]; + [remoteOperationSet mergeOperationSet:localOperationSet]; + [operationSetQueue replaceObjectAtIndex:index withObject:remoteOperationSet]; + } + + return; + } + + if ([key isEqualToString:PFObjectCompleteRESTKey]) { + // If server data is complete, consider this object to be fetched + state.complete = state.complete || [obj boolValue]; + return; + } + if ([key isEqualToString:PFObjectIsDeletingEventuallyRESTKey]) { + _deletingEventually = [obj intValue]; + return; + } + + [_availableKeys addObject:key]; + + // If server data in dictionary is older - don't merge it. + if (!mergeServerData) { + return; + } + + if ([key isEqualToString:PFObjectTypeRESTKey] || [key isEqualToString:PFObjectClassNameRESTKey]) { + return; + } + if ([key isEqualToString:PFObjectObjectIdRESTKey]) { + state.objectId = obj; + return; + } + if ([key isEqualToString:PFObjectCreatedAtRESTKey]) { + [state setCreatedAtFromString:obj]; + return; + } + if ([key isEqualToString:PFObjectUpdatedAtRESTKey]) { + [state setUpdatedAtFromString:obj]; + return; + } + + if ([key isEqualToString:PFObjectACLRESTKey]) { + PFACL *acl = [PFACL ACLWithDictionary:obj]; + [state setServerDataObject:acl forKey:PFObjectACLRESTKey]; + [self checkpointMutableContainer:acl]; + return; + } + + // Should be decoded + id decodedObject = [decoder decodeObject:obj]; + if (PFObjectValueIsKindOfMutableContainerClass(decodedObject)) { + [self checkpointMutableContainer:decodedObject]; + } + [state setServerDataObject:decodedObject forKey:key]; + }]; + if (state.updatedAt == nil && state.createdAt != nil) { + state.updatedAt = state.createdAt; + } + BOOL previousDirtyState = dirty; + self._state = state; + dirty = previousDirtyState; + + if (mergeServerData) { + if ([object[PFObjectCompleteRESTKey] boolValue]) { + [self removeOldKeysAfterFetch:object]; + } else { + // Unmark the object as fetched, because we merged from incomplete new data. + [self setHasBeenFetched:NO]; + } + } + [self rebuildEstimatedData]; + [self checkpointAllMutableContainers]; + } +} + +///-------------------------------------- +#pragma mark - Eventually Helper +///-------------------------------------- + +/*! + Enqueues saveEventually operation asynchronously. + + @returns A task which result is a saveEventually task. + */ +- (BFTask *)_enqueueSaveEventuallyWithChildren:(BOOL)saveChildren { + return [_eventuallyTaskQueue enqueue:^BFTask *(BFTask *toAwait) { + PFUser *currentUser = [PFUser currentUser]; + NSString *sessionToken = currentUser.sessionToken; + return [[toAwait continueAsyncWithBlock:^id(BFTask *task) { + return [self _validateSaveEventuallyAsync]; + }] continueWithSuccessBlock:^id(BFTask *task) { + @synchronized (lock) { + if (![self isDirty:NO]) { + return [BFTask taskWithResult:@YES]; + } + } + + BFTask *saveChildrenTask = nil; + if (saveChildren) { + saveChildrenTask = [PFObject _enqueueSaveEventuallyChildrenOfObject:self currentUser:currentUser]; + } else { + saveChildrenTask = [BFTask taskWithResult:nil]; + } + + return [saveChildrenTask continueWithSuccessBlock:^id(BFTask *task) { + BFTask *saveTask = nil; + @synchronized (lock) { + // Snapshot the current set of changes, and push a new changeset into the queue. + PFOperationSet *changes = [self unsavedChanges]; + changes.saveEventually = YES; + [self startSave]; + [self _checkSaveParametersWithCurrentUser:currentUser]; + PFRESTCommand *command = [self _constructSaveCommandForChanges:changes + sessionToken:sessionToken + objectEncoder:[PFPointerOrLocalIdObjectEncoder objectEncoder]]; + + // Enqueue the eventually operation! + saveTask = [[Parse _currentManager].eventuallyQueue enqueueCommandInBackground:command withObject:self]; + [self _enqueueSaveEventuallyOperationAsync:changes]; + } + saveTask = [saveTask continueWithBlock:^id(BFTask *task) { + @try { + if (!task.isCancelled && !task.exception && !task.error) { + PFCommandResult *result = task.result; + // PFPinningEventuallyQueue handle save result directly. + if (![Parse _currentManager].offlineStoreLoaded) { + return [self handleSaveResultAsync:result.result]; + } + } + return task; + } @finally { + [[Parse _currentManager].eventuallyQueue _notifyTestHelperObjectUpdated]; + } + }]; + return [BFTask taskWithResult:saveTask]; + }]; + }]; + }]; +} + + +/*! + Enqueues the saveEventually PFOperationSet in PFObject taskQueue + */ +- (BFTask *)_enqueueSaveEventuallyOperationAsync:(PFOperationSet *)operationSet { + if (!operationSet.isSaveEventually) { + NSString *message = @"This should only be used to enqueue saveEventually operation sets"; + NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException + reason:message + userInfo:nil]; + return [BFTask taskWithException:exception]; + } + + return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + // Use default priority background to break a chain and make sure this operation is truly asynchronous + return [toAwait continueWithExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id(BFTask *task) { + PFEventuallyQueue *queue = [Parse _currentManager].eventuallyQueue; + id queueSubClass = (id)queue; + return [queueSubClass _waitForOperationSet:operationSet eventuallyPin:nil]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Data model manipulation +///-------------------------------------- + +- (NSMutableDictionary *)_convertToDictionaryForSaving:(PFOperationSet *)changes + withObjectEncoder:(PFEncoder *)encoder { + @synchronized (lock) { + [self checkForChangesToMutableContainers]; + + NSMutableDictionary *serialized = [NSMutableDictionary dictionary]; + [changes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + serialized[key] = obj; + }]; + return [encoder encodeObject:serialized]; + } +} + +/*! + performOperation:forKey: is like setObject:forKey, but instead of just taking a + new value, it takes a PFFieldOperation that modifies the value. + */ +- (void)performOperation:(PFFieldOperation *)operation forKey:(NSString *)key { + @synchronized (lock) { + id newValue = [_estimatedData applyFieldOperation:operation forKey:key]; + + PFFieldOperation *oldOperation = [[self unsavedChanges] objectForKey:key]; + PFFieldOperation *newOperation = [operation mergeWithPrevious:oldOperation]; + [[self unsavedChanges] setObject:newOperation forKey:key]; + [self checkpointMutableContainer:newValue]; + [_availableKeys addObject:key]; + } +} + +- (BOOL)_containsOperationSet:(PFOperationSet *)operationSet { + @synchronized (lock) { + for (PFOperationSet *existingOperationSet in operationSetQueue) { + if (existingOperationSet == operationSet || + [existingOperationSet.uuid isEqualToString:operationSet.uuid]) { + return YES; + } + } + } + return NO; +} + +/*! + Returns the set of PFFieldOperations that will be sent in the next save. + */ +- (PFOperationSet *)unsavedChanges { + @synchronized (lock) { + return [operationSetQueue lastObject]; + } +} + +/*! + @returns YES if there's unsaved changes in this object. This complements ivar `dirty` for `isDirty` check. + */ +- (BOOL)_hasChanges { + @synchronized (lock) { + return [[self unsavedChanges] count] > 0; + } +} + +/*! + @returns YES if this PFObject has operations in operationSetQueue that haven't been completed yet, + NO if there are no operations in the operationSetQueue. + */ +- (BOOL)_hasOutstandingOperations { + @synchronized (lock) { + // > 1 since 1 is unsaved changes. + return [operationSetQueue count] > 1; + } +} + +- (void)rebuildEstimatedData { + @synchronized (lock) { + _estimatedData = [PFObjectEstimatedData estimatedDataFromServerData:self._state.serverData + operationSetQueue:operationSetQueue]; + } +} + +- (PFObject *)mergeFromObject:(PFObject *)other { + @synchronized (lock) { + if (self == other) { + // If they point to the same instance, then don't merge. + return self; + } + + PFMutableObjectState *state = [self._state mutableCopy]; + state.objectId = other.objectId; + state.createdAt = other.createdAt; + state.updatedAt = other.updatedAt; + state.serverData = [other._state.serverData mutableCopy]; + self._state = state; + [self checkpointAllMutableContainers]; + + dirty = NO; + + [self rebuildEstimatedData]; + return self; + } +} + +- (void)_mergeAfterFetchWithResult:(NSDictionary *)result decoder:(PFDecoder *)decoder completeData:(BOOL)completeData { + @synchronized (lock) { + [self checkForChangesToMutableContainers]; + [self _mergeFromServerWithResult:result decoder:decoder completeData:completeData]; + if (completeData) { + [self removeOldKeysAfterFetch:result]; + } + [self rebuildEstimatedData]; + [self checkpointAllMutableContainers]; + } +} + +- (void)removeOldKeysAfterFetch:(NSDictionary *)result { + @synchronized (lock) { + PFMutableObjectState *state = [self._state mutableCopy]; + + NSMutableDictionary *removedDictionary = [NSMutableDictionary dictionaryWithDictionary:state.serverData]; + [removedDictionary removeObjectsForKeys:[result allKeys]]; + + NSArray *removedKeys = [removedDictionary allKeys]; + [state removeServerDataObjectsForKeys:removedKeys]; + [_availableKeys minusSet:[NSSet setWithArray:removedKeys]]; + + self._state = state; + } +} + +- (void)_mergeAfterSaveWithResult:(NSDictionary *)result decoder:(PFDecoder *)decoder { + @synchronized (lock) { + PFOperationSet *operationsBeforeSave = operationSetQueue[0]; + [operationSetQueue removeObjectAtIndex:0]; + + if (!result) { + // Merge the data from the failed save into the next save. + PFOperationSet *operationsForNextSave = operationSetQueue[0]; + [operationsForNextSave mergeOperationSet:operationsBeforeSave]; + } else { + // Merge the data from the save and the data from the server into serverData. + [self checkForChangesToMutableContainers]; + + PFMutableObjectState *state = [self._state mutableCopy]; + [state applyOperationSet:operationsBeforeSave]; + self._state = state; + + [self _mergeFromServerWithResult:result decoder:decoder completeData:NO]; + [self rebuildEstimatedData]; + [self checkpointAllMutableContainers]; + } + } +} + +- (void)_mergeFromServerWithResult:(NSDictionary *)result decoder:(PFDecoder *)decoder completeData:(BOOL)completeData { + @synchronized (lock) { + PFMutableObjectState *state = [self._state mutableCopy]; + + // If the server's data is complete, consider this object to be fetched. + state.complete |= completeData; + + [result enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([key isEqualToString:PFObjectObjectIdRESTKey]) { + state.objectId = obj; + } else if ([key isEqualToString:PFObjectCreatedAtRESTKey]) { + [state setCreatedAtFromString:obj]; + } else if ([key isEqualToString:PFObjectUpdatedAtRESTKey]) { + [state setUpdatedAtFromString:obj]; + } else if ([key isEqualToString:PFObjectACLRESTKey]) { + PFACL *acl = [PFACL ACLWithDictionary:obj]; + [state setServerDataObject:acl forKey:key]; + [self checkpointMutableContainer:acl]; + } else { + [state setServerDataObject:[decoder decodeObject:obj] forKey:key]; + } + }]; + if (state.updatedAt == nil && state.createdAt != nil) { + state.updatedAt = state.createdAt; + } + self._state = state; + [_availableKeys addObjectsFromArray:[result allKeys]]; + + dirty = NO; + } +} + +///-------------------------------------- +#pragma mark - Command handlers +///-------------------------------------- + +// We can't get rid of these handlers, because subclasses override them +// to add special actions after operations. + +- (BFTask *)handleSaveResultAsync:(NSDictionary *)result { + BFTask *task = [BFTask taskWithResult:nil]; + + NSDictionary *fetchedObjects = [self _collectFetchedObjects]; + + [task continueWithBlock:^id(BFTask *task) { + PFKnownParseObjectDecoder *decoder = [PFKnownParseObjectDecoder decoderWithFetchedObjects:fetchedObjects]; + @synchronized (self.lock) { + // TODO (hallucinogen): t5611821 we need to make mergeAfterSave that accepts decoder and operationBeforeSave + [self _mergeAfterSaveWithResult:result decoder:decoder]; + } + return nil; + }]; + + PFOfflineStore *store = [Parse _currentManager].offlineStore; + if (store != nil) { + task = [task continueWithBlock:^id(BFTask *task) { + return [store updateDataForObjectAsync:self]; + }]; + } + + return [task continueWithBlock:^id(BFTask *task) { + @synchronized (lock) { + if (self.saveDelegate) { + [self.saveDelegate invoke:self error:nil]; + } + return [BFTask taskWithResult:@(!!result)]; + } + }]; +} + +///-------------------------------------- +#pragma mark - Asynchronous operations +///-------------------------------------- + +- (void)startSave { + @synchronized (lock) { + [operationSetQueue addObject:[[PFOperationSet alloc] init]]; + } +} + +- (BFTask *)saveAsync:(BFTask *)toAwait { + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller getCurrentObjectAsync] continueWithBlock:^id(BFTask *task) { + PFUser *currentUser = task.result; + NSString *sessionToken = currentUser.sessionToken; + + BFTask *await = toAwait ?: [BFTask taskWithResult:nil]; + return [[await continueAsyncWithBlock:^id(BFTask *task) { + PFOfflineStore *offlineStore = [Parse _currentManager].offlineStore; + if (offlineStore != nil) { + return [offlineStore fetchObjectLocallyAsync:self]; + } + return nil; + }] continueWithBlock:^id(BFTask *task) { + @synchronized (lock) { + if (![self isDirty:YES]) { + return [BFTask taskWithResult:@YES]; + } + + // Snapshot the current set of changes, and push a new changeset into the queue. + PFOperationSet *changes = [self unsavedChanges]; + + [self startSave]; + BFTask *childrenTask = [self _saveChildrenInBackgroundWithCurrentUser:currentUser + sessionToken:sessionToken]; + if (!dirty && ![changes count]) { + return childrenTask; + } + return [[childrenTask continueWithSuccessBlock:^id(BFTask *task) { + [self _checkSaveParametersWithCurrentUser:currentUser]; + PFRESTCommand *command = [self _constructSaveCommandForChanges:changes + sessionToken:sessionToken + objectEncoder:[PFPointerObjectEncoder objectEncoder]]; + return [[Parse _currentManager].commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueAsyncWithBlock:^id(BFTask *task) { + if (task.isCancelled || task.exception || task.error) { + // If there was an error, we want to roll forward the save changes before rethrowing. + BFTask *commandRunnerTask = task; + return [[self handleSaveResultAsync:nil] continueWithBlock:^id(BFTask *task) { + return commandRunnerTask; + }]; + } + PFCommandResult *result = task.result; + return [self handleSaveResultAsync:result.result]; + }]; + } + }]; + }]; +} + +- (BFTask *)fetchAsync:(BFTask *)toAwait { + PFCurrentUserController *controller = [[self class] currentUserController]; + @weakify(self); + return [[controller getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + @strongify(self); + return [[[self class] objectController] fetchObjectAsync:self withSessionToken:sessionToken]; + }]; + }]; +} + +- (BFTask *)deleteAsync:(BFTask *)toAwait { + [self checkDeleteParams]; + + PFCurrentUserController *controller = [[self class] currentUserController]; + @weakify(self); + return [[controller getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + @strongify(self); + return [[[self class] objectController] deleteObjectAsync:self withSessionToken:sessionToken]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Command constructors +///-------------------------------------- + +- (PFRESTCommand *)_constructSaveCommandForChanges:(PFOperationSet *)changes + sessionToken:(NSString *)sessionToken + objectEncoder:(PFEncoder *)encoder { + @synchronized (lock) { + NSDictionary *parameters = [self _convertToDictionaryForSaving:changes withObjectEncoder:encoder]; + + if (self._state.objectId) { + return [PFRESTObjectCommand updateObjectCommandForObjectState:self._state + changes:parameters + operationSetUUID:changes.uuid + sessionToken:sessionToken]; + } + + return [PFRESTObjectCommand createObjectCommandForObjectState:self._state + changes:parameters + operationSetUUID:changes.uuid + sessionToken:sessionToken]; + + } +} + +- (PFRESTCommand *)_currentDeleteCommandWithSessionToken:(NSString *)sessionToken { + @synchronized (lock) { + [self checkDeleteParams]; + return [PFRESTObjectCommand deleteObjectCommandForObjectState:self._state + withSessionToken:sessionToken]; + } +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)_setObject:(id)object forKey:(NSString *)key onlyIfDifferent:(BOOL)onlyIfDifferent { + PFParameterAssert(object != nil && key != nil, + @"Can't use nil for keys or values on PFObject. Use NSNull for values."); + PFParameterAssert([key isKindOfClass:[NSString class]], @"PFObject keys must be NSStrings."); + + if (onlyIfDifferent) { + id currentObject = self[key]; + if (currentObject == object || + [currentObject isEqual:object]) { + return; + } + } + + @synchronized (lock) { + if ([object isKindOfClass:[PFFieldOperation class]]) { + [self performOperation:object forKey:key]; + return; + } + + PFObjectAssertValueIsKindOfValidClass(object); + [self performOperation:[PFSetOperation setWithValue:object] forKey:key]; + } +} + +///-------------------------------------- +#pragma mark - Misc helpers +///-------------------------------------- + +- (NSString *)displayObjectId { + return self._state.objectId ?: @"new"; +} + +- (NSString *)displayClassName { + return self._state.parseClassName; +} + +- (void)registerSaveListener:(void (^)(id result, NSError *error))callback { + @synchronized (lock) { + if (!self.saveDelegate) { + self.saveDelegate = [[PFMulticastDelegate alloc] init]; + } + [self.saveDelegate subscribe:callback]; + } +} + +- (void)unregisterSaveListener:(void (^)(id result, NSError *error))callback { + @synchronized (lock) { + if (!self.saveDelegate) { + self.saveDelegate = [[PFMulticastDelegate alloc] init]; + } + [self.saveDelegate unsubscribe:callback]; + } +} + +- (PFACL *)ACLWithoutCopying { + @synchronized (lock) { + return _estimatedData[@"ACL"]; + } +} + +// Overriden by classes which want to ignore the default ACL. +- (void)setDefaultValues { + if ([self needsDefaultACL]) { + PFACL *defaultACL = [PFACL defaultACL]; + if (defaultACL) { + self.ACL = defaultACL; + } + } +} + +- (BOOL)needsDefaultACL { + return YES; +} + +- (NSDictionary *)_collectFetchedObjects { + NSMutableDictionary *fetchedObjects = [NSMutableDictionary dictionary]; + @synchronized (lock) { + NSDictionary *dictionary = _estimatedData.dictionaryRepresentation; + [PFInternalUtils traverseObject:dictionary usingBlock:^id(id obj) { + if ([obj isKindOfClass:[PFObject class]]) { + PFObject *object = obj; + NSString *objectId = object.objectId; + if (objectId && [object isDataAvailable]) { + fetchedObjects[objectId] = object; + } + } + return obj; + }]; + } + return fetchedObjects; +} + +@end + +@implementation PFObject + +@synthesize _state = _state; +@synthesize _availableKeys = _availableKeys; + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + if (!_state) { + PFConsistencyAssert([self conformsToProtocol:@protocol(PFSubclassing)], + @"Can only call -[PFObject init] on subclasses conforming to PFSubclassing."); + [PFObject assertSubclassIsRegistered:[self class]]; + _state = [[self class] _newObjectStateWithParseClassName:[[self class] parseClassName] + objectId:nil + isComplete:YES]; + } + [[self class] _assertValidInstanceClassName:_state.parseClassName]; + + lock = [[NSObject alloc] init]; + operationSetQueue = [NSMutableArray arrayWithObject:[[PFOperationSet alloc] init]]; + _estimatedData = [PFObjectEstimatedData estimatedDataFromServerData:_state.serverData + operationSetQueue:operationSetQueue]; + _availableKeys = [NSMutableSet set]; + hashedObjectsCache = [[NSMutableDictionary alloc] init]; + self.taskQueue = [[PFTaskQueue alloc] init]; + _eventuallyTaskQueue = [[PFTaskQueue alloc] init]; + + if (_state.complete) { + dirty = YES; + [self setDefaultValues]; + } + + return self; +} + +- (instancetype)initWithClassName:(NSString *)className { + PFObjectState *state = [[self class] _newObjectStateWithParseClassName:className objectId:nil isComplete:YES]; + return [self initWithObjectState:state]; +} + +- (instancetype)initWithObjectState:(PFObjectState *)state { + _state = state; + return [self init]; +} + ++ (instancetype)objectWithClassName:(NSString *)className + objectId:(NSString *)objectId + completeData:(BOOL)completeData { + Class class = [[[self class] subclassingController] subclassForParseClassName:className] ?: [PFObject class]; + PFObjectState *state = [class _newObjectStateWithParseClassName:className objectId:objectId isComplete:completeData]; + PFObject *object = [[class alloc] initWithObjectState:state]; + if (!completeData) { + PFConsistencyAssert(![object _hasChanges], + @"The init method of %@ set values on the object, which is not allowed.", class); + } + return object; +} + ++ (instancetype)objectWithClassName:(NSString *)className { + return [self objectWithClassName:className objectId:nil completeData:YES]; +} + ++ (instancetype)objectWithClassName:(NSString *)className dictionary:(NSDictionary *)dictionary { + PFObject *object = [self objectWithClassName:className]; + [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + object[key] = obj; + }]; + return object; +} + ++ (instancetype)objectWithoutDataWithClassName:(NSString *)className objectId:(NSString *)objectId { + // Try get single instance from OfflineStore + PFOfflineStore *store = [Parse _currentManager].offlineStore; + if (store != nil && objectId != nil) { + PFObject *singleObject = [store getOrCreateObjectWithoutDataWithClassName:className objectId:objectId]; + if (singleObject) { + return singleObject; + } + } + + // Local Datastore is not enabled or cannot found the single instance using objectId, let's use the old way + return [self objectWithClassName:className objectId:objectId completeData:NO]; +} + +#pragma mark Subclassing + ++ (instancetype)object { + PFConsistencyAssert([self conformsToProtocol:@protocol(PFSubclassing)], + @"Can only call +object on subclasses conforming to PFSubclassing"); + NSString *className = [(id)self parseClassName]; + Class class = [[self subclassingController] subclassForParseClassName:className] ?: [PFObject class]; + return [class objectWithClassName:className]; +} + ++ (instancetype)objectWithoutDataWithObjectId:(NSString *)objectId { + PFConsistencyAssert([self conformsToProtocol:@protocol(PFSubclassing)], + @"Can only call objectWithoutDataWithObjectId: on subclasses conforming to PFSubclassing"); + return [self objectWithoutDataWithClassName:[(id)self parseClassName] objectId:objectId]; +} + +#pragma mark Private + ++ (instancetype)objectWithoutDataWithClassName:(NSString *)className localId:(NSString *)localId { + PFObject *object = [self objectWithoutDataWithClassName:className objectId:nil]; + object.localId = localId; + return object; +} + +///-------------------------------------- +#pragma mark - PFObjectPrivateSubclass +///-------------------------------------- + +#pragma mark State + ++ (PFObjectState *)_newObjectStateWithParseClassName:(NSString *)className + objectId:(NSString *)objectId + isComplete:(BOOL)complete { + return [PFObjectState stateWithParseClassName:className objectId:objectId isComplete:complete]; +} + +#pragma mark Validation + +- (BFTask *)_validateSaveEventuallyAsync { + return [BFTask taskWithResult:nil]; +} + +///-------------------------------------- +#pragma mark - Properties +///-------------------------------------- + +- (void)set_state:(PFObjectState *)state { + @synchronized(lock) { + NSString *oldObjectId = _state.objectId; + if (self._state != state) { + _state = [state copy]; + } + + NSString *newObjectId = _state.objectId; + if (![PFObjectUtilities isObject:oldObjectId equalToObject:newObjectId]) { + [self _notifyObjectIdChangedFrom:oldObjectId toObjectId:newObjectId]; + } + } +} + +- (PFObjectState *)_state { + @synchronized(lock) { + return _state; + } +} + +- (PFObjectEstimatedData *)_estimatedData { + @synchronized (lock) { + return _estimatedData; + } +} + +- (void)setObjectId:(NSString *)objectId { + @synchronized (lock) { + NSString *oldObjectId = self._state.objectId; + if ([PFObjectUtilities isObject:oldObjectId equalToObject:objectId]) { + return; + } + + dirty = YES; + + PFMutableObjectState *state = [self._state mutableCopy]; + state.objectId = objectId; + _state = state; + + [self _notifyObjectIdChangedFrom:oldObjectId toObjectId:objectId]; + } +} + +- (NSString *)objectId { + return self._state.objectId; +} + +- (void)_notifyObjectIdChangedFrom:(NSString *)fromObjectId toObjectId:(NSString *)toObjectId { + @synchronized (self.lock) { + // The OfflineStore might raise exception if this object already had a different objectId. + PFOfflineStore *store = [Parse _currentManager].offlineStore; + if (store != nil) { + [store updateObjectIdForObject:self oldObjectId:fromObjectId newObjectId:toObjectId]; + } + if (self.localId) { + [[Parse _currentManager].coreManager.objectLocalIdStore setObjectId:toObjectId forLocalId:self.localId]; + self.localId = nil; + } + } +} + +- (NSString *)parseClassName { + return self._state.parseClassName; +} + +- (NSDate *)updatedAt { + return self._state.updatedAt; +} + +- (NSDate *)createdAt { + return self._state.createdAt; +} + +- (PFACL *)ACL { + return self[@"ACL"]; +} + +- (void)setACL:(PFACL *)ACL { + if (!ACL) { + [self removeObjectForKey:@"ACL"]; + } else { + self[@"ACL"] = ACL; + } +} + +// PFObject(): +@synthesize localId; +@synthesize taskQueue; + +// PFObject(Private): +@synthesize saveDelegate; + +///-------------------------------------- +#pragma mark - PFObject factory methods for Subclassing +///-------------------------------------- + +// Reverse compatibility note: many people may have built PFObject subclasses before +// we officially supported them. Our implementation can do cool stuff, but requires +// the parseClassName class method. ++ (void)registerSubclass { + [[self subclassingController] registerSubclass:self]; +} + ++ (PFQuery *)query { + PFConsistencyAssert([self conformsToProtocol:@protocol(PFSubclassing)], + @"+[PFObject query] can only be called on subclasses conforming to PFSubclassing."); + [PFObject assertSubclassIsRegistered:[self class]]; + return [PFQuery queryWithClassName:[(id)self parseClassName]]; +} + ++ (PFQuery *)queryWithPredicate:(NSPredicate *)predicate { + NSAssert([self conformsToProtocol:@protocol(PFSubclassing)], + @"+[PFObject queryWithPredicate:] can only be called on subclasses conforming to PFSubclassing."); + [PFObject assertSubclassIsRegistered:[self class]]; + return [PFQuery queryWithClassName:[(id)self parseClassName] predicate:predicate]; +} + ++ (void)assertSubclassIsRegistered:(Class)subclass { + // If people hacked their own subclass together before we supported it officially, we shouldn't break their app. + if ([subclass conformsToProtocol:@protocol(PFSubclassing)]) { + Class registration = [[self subclassingController] subclassForParseClassName:[subclass parseClassName]]; + + // It's OK to subclass a subclass (i.e. custom PFUser implementation) + PFConsistencyAssert(registration && (registration == subclass || [registration isKindOfClass:subclass]), + @"The class %@ must be registered with registerSubclass before using Parse.", subclass); + } +} + +///-------------------------------------- +#pragma mark - Delete commands +///-------------------------------------- + +- (BOOL)delete { + return [self delete:nil]; +} + +- (BOOL)delete:(NSError **)error { + return [[[self deleteInBackground] waitForResult:error] boolValue]; +} + +- (BFTask *)deleteInBackground { + return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [[self deleteAsync:toAwait] continueWithSuccessResult:@YES]; + }]; +} + +- (void)deleteInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self deleteInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +- (void)deleteInBackgroundWithBlock:(PFBooleanResultBlock)block { + [[self deleteInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +///-------------------------------------- +#pragma mark - Save commands +///-------------------------------------- + +- (BOOL)save { + return [self save:nil]; +} + +- (BOOL)save:(NSError **)error { + return [[[self saveInBackground] waitForResult:error] boolValue]; +} + +- (BFTask *)saveInBackground { + return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [self saveAsync:toAwait]; + }]; +} + +- (void)saveInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +- (void)saveInBackgroundWithBlock:(PFBooleanResultBlock)block { + [[self saveInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +- (BFTask *)saveEventually { + return [[self _enqueueSaveEventuallyWithChildren:YES] continueWithSuccessBlock:^id(BFTask *task) { + // The result of the previous task will be an instance of BFTask. + // Returning it here will trigger the whole task stack become an actual save task. + return task.result; + }]; +} + +- (void)saveEventually:(PFBooleanResultBlock)block { + [[self saveEventually] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +- (BFTask *)deleteEventually { + return [[[_eventuallyTaskQueue enqueue:^BFTask *(BFTask *toAwait) { + NSString *sessionToken = [PFUser currentSessionToken]; + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + @synchronized (lock) { + [self checkDeleteParams]; + _deletingEventually += 1; + + PFOfflineStore *store = [Parse _currentManager].offlineStore; + BFTask *updateDataTask = store ? [store updateDataForObjectAsync:self] : [BFTask taskWithResult:nil]; + + PFRESTCommand *command = [self _currentDeleteCommandWithSessionToken:sessionToken]; + BFTask *deleteTask = [updateDataTask continueWithBlock:^id(BFTask *task) { + return [[Parse _currentManager].eventuallyQueue enqueueCommandInBackground:command withObject:self]; + }]; + deleteTask = [deleteTask continueWithSuccessBlock:^id(BFTask *task) { + // PFPinningEventuallyQueue handles delete result directly. + if (![Parse _currentManager].offlineStoreLoaded) { + PFCommandResult *result = task.result; + return [[[self class] objectController] processDeleteResultAsync:result.result forObject:self]; + } + return task; + }]; + return deleteTask; + } + }]; + }] continueWithSuccessBlock:^id(BFTask *task) { + // The result of the previous task will be an instance of BFTask. + // Returning it here will trigger the whole task stack become an actual save task. + return task.result; + }] continueWithSuccessResult:@YES]; +} + +///-------------------------------------- +#pragma mark - Dirtiness +///-------------------------------------- + +- (BOOL)isDirty { + return [self isDirty:YES]; +} + +- (BOOL)isDirtyForKey:(NSString *)key { + @synchronized (lock) { + [self checkForChangesToMutableContainer:_estimatedData[key] forKey:key]; + return !![[self unsavedChanges] objectForKey:key]; + } +} + +///-------------------------------------- +#pragma mark - Fetch +///-------------------------------------- + +- (BOOL)isDataAvailable { + return self._state.complete; +} + +- (void)refresh { + [self fetch]; +} + +- (void)refresh:(NSError **)error { + [self fetch:error]; +} + +- (void)refreshInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self fetchInBackgroundWithTarget:target selector:selector]; +} + +- (void)refreshInBackgroundWithBlock:(PFObjectResultBlock)block { + [self fetchInBackgroundWithBlock:block]; +} + +- (void)fetch { + [self fetch:nil]; +} + +- (void)fetch:(NSError **)error { + [[self fetchInBackground] waitForResult:error]; +} + +- (BFTask *)fetchInBackground { + //TODO: (nlutsenko) Replace with an error? + @synchronized (lock) { + PFParameterAssert(self._state.objectId, @"Can't refresh an object that hasn't been saved to the server."); + } + return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [self fetchAsync:toAwait]; + }]; +} + +- (void)fetchInBackgroundWithBlock:(PFObjectResultBlock)block { + [[self fetchInBackground] thenCallBackOnMainThreadAsync:block]; +} + +- (void)fetchInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self fetchInBackgroundWithBlock:^(PFObject *object, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:object object:error]; + }]; +} + +- (PFObject *)fetchIfNeeded { + return [self fetchIfNeeded:nil]; +} + +- (PFObject *)fetchIfNeeded:(NSError **)error { + return [[self fetchIfNeededInBackground] waitForResult:error]; +} + +- (BFTask *)fetchIfNeededInBackground { + if ([self isDataAvailable]) { + return [BFTask taskWithResult:self]; + } + return [self fetchInBackground]; +} + +- (void)fetchIfNeededInBackgroundWithBlock:(PFObjectResultBlock)block { + [[self fetchIfNeededInBackground] thenCallBackOnMainThreadAsync:block]; +} + +- (void)fetchIfNeededInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self fetchIfNeededInBackgroundWithBlock:^(PFObject *object, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:object object:error]; + }]; +} + +///-------------------------------------- +#pragma mark - Fetching Many Objects +///-------------------------------------- + ++ (void)fetchAll:(NSArray *)objects { + [PFObject fetchAll:objects error:nil]; +} + ++ (void)fetchAllIfNeeded:(NSArray *)objects { + [PFObject fetchAllIfNeeded:objects error:nil]; +} + ++ (void)fetchAll:(NSArray *)objects error:(NSError **)error { + [[self fetchAllInBackground:objects] waitForResult:error]; +} + ++ (void)fetchAllIfNeeded:(NSArray *)objects error:(NSError **)error { + [[self fetchAllIfNeededInBackground:objects] waitForResult:error]; +} + ++ (BFTask *)fetchAllInBackground:(NSArray *)objects { + // Snapshot the objects array. + NSArray *fetchObjects = [objects copy]; + + if (fetchObjects.count == 0) { + return [BFTask taskWithResult:fetchObjects]; + } + NSArray *uniqueObjects = [PFObjectBatchController uniqueObjectsArrayFromArray:fetchObjects omitObjectsWithData:NO]; + return [[[[self currentUserController] getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [PFObject _enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + return [[self objectBatchController] fetchObjectsAsync:uniqueObjects withSessionToken:sessionToken]; + }]; + } forObjects:uniqueObjects]; + }] continueWithSuccessResult:fetchObjects]; +} + ++ (void)fetchAllInBackground:(NSArray *)objects target:(id)target selector:(SEL)selector { + [self fetchAllInBackground:objects block:^(NSArray *objects, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:objects object:error]; + }]; +} + ++ (void)fetchAllInBackground:(NSArray *)objects block:(PFArrayResultBlock)block { + [[self fetchAllInBackground:objects] thenCallBackOnMainThreadAsync:block]; +} + ++ (BFTask *)fetchAllIfNeededInBackground:(NSArray *)objects { + NSArray *fetchObjects = [objects copy]; + if (fetchObjects.count == 0) { + return [BFTask taskWithResult:fetchObjects]; + } + NSArray *uniqueObjects = [PFObjectBatchController uniqueObjectsArrayFromArray:fetchObjects omitObjectsWithData:YES]; + return [[[[self currentUserController] getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [PFObject _enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + return [[self objectBatchController] fetchObjectsAsync:uniqueObjects withSessionToken:sessionToken]; + }]; + } forObjects:uniqueObjects]; + }] continueWithSuccessResult:fetchObjects]; +} + ++ (void)fetchAllIfNeededInBackground:(NSArray *)objects target:(id)target selector:(SEL)selector { + [self fetchAllIfNeededInBackground:objects block:^(NSArray *objects, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:objects object:error]; + }]; +} + ++ (void)fetchAllIfNeededInBackground:(NSArray *)objects block:(PFArrayResultBlock)block { + [[self fetchAllIfNeededInBackground:objects] thenCallBackOnMainThreadAsync:block]; +} + +///-------------------------------------- +#pragma mark - Fetch From Local Datastore +///-------------------------------------- + +- (void)fetchFromLocalDatastore { + [self fetchFromLocalDatastore:nil]; +} + +- (void)fetchFromLocalDatastore:(NSError **)error { + [[self fetchFromLocalDatastoreInBackground] waitForResult:error]; +} + +- (void)fetchFromLocalDatastoreInBackgroundWithBlock:(PFObjectResultBlock)block { + [[self fetchFromLocalDatastoreInBackground] thenCallBackOnMainThreadAsync:block]; +} + +- (BFTask *)fetchFromLocalDatastoreInBackground { + PFOfflineStore *store = [Parse _currentManager].offlineStore; + PFConsistencyAssert(store != nil, @"You must enable the local datastore before calling fetchFromLocalDatastore()."); + return [store fetchObjectLocallyAsync:self]; +} + +///-------------------------------------- +#pragma mark - Key/Value Accessors +///-------------------------------------- + +- (void)setObject:(id)object forKey:(NSString *)key { + [self _setObject:object forKey:key onlyIfDifferent:NO]; +} + +- (void)setObject:(id)object forKeyedSubscript:(NSString *)key { + [self setObject:object forKey:key]; +} + +- (id)objectForKey:(NSString *)key { + @synchronized (lock) { + PFConsistencyAssert([self isDataAvailableForKey:key], + @"Key \"%@\" has no data. Call fetchIfNeeded before getting its value.", key); + + id result = _estimatedData[key]; + if ([key isEqualToString:PFObjectACLRESTKey] && [result isKindOfClass:[PFACL class]]) { + PFACL *acl = result; + if ([acl isShared]) { + PFACL *copy = [acl createUnsharedCopy]; + self[PFObjectACLRESTKey] = copy; + return copy; + } + } + + // A relation may be deserialized without a parent or key. Either way, make sure it's consistent. + // TODO: (nlutsenko) This should be removable after we clean up the serialization code. + if ([result isKindOfClass:[PFRelation class]]) { + [result ensureParentIs:self andKeyIs:key]; + } + + return result; + } +} + +- (id)objectForKeyedSubscript:(NSString *)key { + return [self objectForKey:key]; +} + +- (void)removeObjectForKey:(NSString *)key { + @synchronized (lock) { + if ([self objectForKey:key]) { + PFDeleteOperation *operation = [[PFDeleteOperation alloc] init]; + [self performOperation:operation forKey:key]; + } + } +} + +#pragma mark Relations + +- (PFRelation *)relationforKey:(NSString *)key { + return [self relationForKey:key]; +} + +- (PFRelation *)relationForKey:(NSString *)key { + @synchronized (lock) { + // All the sanity checking is done when addObject or + // removeObject is called on the relation. + PFRelation *relation = [PFRelation relationForObject:self forKey:key]; + + id object = _estimatedData[key]; + if ([object isKindOfClass:[PFRelation class]]) { + relation.targetClass = ((PFRelation *)object).targetClass; + } + return relation; + } +} + +#pragma mark Array + +- (void)addObject:(id)object forKey:(NSString *)key { + [self addObjectsFromArray:@[ object ] forKey:key]; +} + +- (void)addObjectsFromArray:(NSArray *)objects forKey:(NSString *)key { + [self performOperation:[PFAddOperation addWithObjects:objects] forKey:key]; +} + +- (void)addUniqueObject:(id)object forKey:(NSString *)key { + [self addUniqueObjectsFromArray:@[ object ] forKey:key]; +} + +- (void)addUniqueObjectsFromArray:(NSArray *)objects forKey:(NSString *)key { + [self performOperation:[PFAddUniqueOperation addUniqueWithObjects:objects] forKey:key]; +} + +- (void)removeObject:(id)object forKey:(NSString *)key { + [self removeObjectsInArray:@[ object ] forKey:key]; +} + +- (void)removeObjectsInArray:(NSArray *)objects forKey:(NSString *)key { + [self performOperation:[PFRemoveOperation removeWithObjects:objects] forKey:key]; +} + +#pragma mark Increment + +- (void)incrementKey:(NSString *)key { + [self incrementKey:key byAmount:@1]; +} + +- (void)incrementKey:(NSString *)key byAmount:(NSNumber *)amount { + [self performOperation:[PFIncrementOperation incrementWithAmount:amount] forKey:key]; +} + +///-------------------------------------- +#pragma mark - Key Value Coding +///-------------------------------------- + +- (id)valueForUndefinedKey:(NSString *)key { + return self[key]; +} + +- (void)setValue:(id)value forUndefinedKey:(NSString *)key { + self[key] = value; +} + +///-------------------------------------- +#pragma mark - Misc +///-------------------------------------- + +- (NSArray *)allKeys { + @synchronized (lock) { + return [_estimatedData allKeys]; + } +} + +- (NSString *)description { + static NSString *descriptionKey = @"PFObject-PrintingDescription"; + + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + if ([threadDictionary[descriptionKey] boolValue]) { + return [self _flatDescription]; + } + threadDictionary[descriptionKey] = @YES; + NSString *description = [self _recursiveDescription]; + [threadDictionary removeObjectForKey:descriptionKey]; + return description; +} + +- (NSString *)_recursiveDescription { + @synchronized (lock) { + return [NSString stringWithFormat:@"%@ %@", + [self _flatDescription], [_estimatedData.dictionaryRepresentation description]]; + } +} + +- (NSString *)_flatDescription { + @synchronized (lock) { + return [NSString stringWithFormat:@"<%@: %p, objectId: %@, localId: %@>", + self.displayClassName, self, [self displayObjectId], localId]; + } +} + +///-------------------------------------- +#pragma mark - Save all +///-------------------------------------- + ++ (BOOL)saveAll:(NSArray *)objects { + return [PFObject saveAll:objects error:nil]; +} + ++ (BOOL)saveAll:(NSArray *)objects error:(NSError **)error { + return [[[self saveAllInBackground:objects] waitForResult:error] boolValue]; +} + ++ (BFTask *)saveAllInBackground:(NSArray *)objects { + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller getCurrentObjectAsync] continueWithBlock:^id(BFTask *task) { + PFUser *currentUser = task.result; + NSString *sessionToken = currentUser.sessionToken; + return [PFObject _deepSaveAsync:objects withCurrentUser:currentUser sessionToken:sessionToken]; + }]; +} + ++ (void)saveAllInBackground:(NSArray *)objects target:(id)target selector:(SEL)selector { + [PFObject saveAllInBackground:objects block:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + ++ (void)saveAllInBackground:(NSArray *)objects block:(PFBooleanResultBlock)block { + [[PFObject saveAllInBackground:objects] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +///-------------------------------------- +#pragma mark - Delete all +///-------------------------------------- + ++ (BOOL)deleteAll:(NSArray *)objects { + return [PFObject deleteAll:objects error:nil]; +} + ++ (BOOL)deleteAll:(NSArray *)objects error:(NSError **)error { + return [[[self deleteAllInBackground:objects] waitForResult:error] boolValue]; +} + ++ (BFTask *)deleteAllInBackground:(NSArray *)objects { + return [[[self currentUserController] getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [PFObject deleteAllAsync:objects withSessionToken:sessionToken]; + }]; +} + ++ (void)deleteAllInBackground:(NSArray *)objects target:(id)target selector:(SEL)selector { + [PFObject deleteAllInBackground:objects block:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + ++ (void)deleteAllInBackground:(NSArray *)objects block:(PFBooleanResultBlock)block { + [[self deleteAllInBackground:objects] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +///-------------------------------------- +#pragma mark - Dynamic synthesizers +///-------------------------------------- + +// NOTE: The ONLY reason this needs to exist is to support mocking PFObject subclasses. +// +// The reason mocking doesn't work is because OCMClassMock looks for methods that exist on the class already, and will +// not be able to use our dynamic instance-level method resolving. By implementing this, we give this method a signature +// once, and then tell the runtime to forward that message on from there. +// +// Note that by implementing it this way, we no longer need to implement -methodSignatureForSelector: or +// -respondsToSelector:, as the method will be dynamically resolved by the runtime when either of those methods is +// invoked. ++ (BOOL)resolveInstanceMethod:(SEL)sel { + if (self == [PFObject class]) { + return NO; + } + + NSMethodSignature *signature = [[self subclassingController] forwardingMethodSignatureForSelector:sel ofClass:self]; + if (!signature) { + return NO; + } + + // Convert the method signature *back* into a objc type string (sidenote, why isn't this a built in?). + NSMutableString *typeString = [NSMutableString stringWithFormat:@"%s", [signature methodReturnType]]; + for (NSUInteger argumentIndex = 0; argumentIndex < [signature numberOfArguments]; argumentIndex++) { + [typeString appendFormat:@"%s", [signature getArgumentTypeAtIndex:argumentIndex]]; + } + + // TODO: (richardross) Support stret return here (will need to introspect the method signature to do so). + class_addMethod(self, sel, _objc_msgForward, [typeString UTF8String]); + + return YES; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + if (![[[self class] subclassingController] forwardObjectInvocation:anInvocation + withObject:(PFObject *)self]) { + [self doesNotRecognizeSelector:anInvocation.selector]; + } +} + +///-------------------------------------- +#pragma mark - Pinning +///-------------------------------------- + +- (BOOL)pin { + return [self pin:nil]; +} + +- (BOOL)pin:(NSError **)error { + return [self pinWithName:PFObjectDefaultPin error:error]; +} + +- (BFTask *)pinInBackground { + return [self pinInBackgroundWithName:PFObjectDefaultPin]; +} + +- (void)pinInBackgroundWithBlock:(PFBooleanResultBlock)block { + [[self pinInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +- (BOOL)pinWithName:(NSString *)name { + return [self pinWithName:name error:nil]; +} + +- (BOOL)pinWithName:(NSString *)name error:(NSError **)error { + return [[[self pinInBackgroundWithName:name] waitForResult:error] boolValue]; +} + +- (void)pinInBackgroundWithName:(NSString *)name block:(PFBooleanResultBlock)block { + [[self pinInBackgroundWithName:name] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +- (BFTask *)pinInBackgroundWithName:(NSString *)name { + return [self _pinInBackgroundWithName:name includeChildren:YES]; +} + +- (BFTask *)_pinInBackgroundWithName:(NSString *)name includeChildren:(BOOL)includeChildren { + return [[self class] _pinAllInBackground:@[ self ] withName:name includeChildren:includeChildren]; +} + +///-------------------------------------- +#pragma mark - Pinning Many Objects +///-------------------------------------- + ++ (BOOL)pinAll:(NSArray *)objects { + return [self pinAll:objects error:nil]; +} + ++ (BOOL)pinAll:(NSArray *)objects error:(NSError **)error { + return [self pinAll:objects withName:PFObjectDefaultPin error:error]; +} + ++ (BFTask *)pinAllInBackground:(NSArray *)objects { + return [self pinAllInBackground:objects withName:PFObjectDefaultPin]; +} + ++ (void)pinAllInBackground:(NSArray *)objects + block:(PFBooleanResultBlock)block { + [[self pinAllInBackground:objects] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (BOOL)pinAll:(NSArray *)objects withName:(NSString *)name { + return [self pinAll:objects withName:name error:nil]; +} + ++ (BOOL)pinAll:(NSArray *)objects withName:(NSString *)name error:(NSError **)error { + return [[[self pinAllInBackground:objects withName:name] waitForResult:error] boolValue]; +} + ++ (BFTask *)pinAllInBackground:(NSArray *)objects withName:(NSString *)name { + return [self _pinAllInBackground:objects withName:name includeChildren:YES]; +} + ++ (void)pinAllInBackground:(NSArray *)objects + withName:(NSString *)name + block:(PFBooleanResultBlock)block { + [[self pinAllInBackground:objects withName:name] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (BFTask *)_pinAllInBackground:(NSArray *)objects + withName:(NSString *)name + includeChildren:(BOOL)includeChildren { + return [[self pinningObjectStore] pinObjectsAsync:objects + withPinName:name + includeChildren:includeChildren]; +} + +///-------------------------------------- +#pragma mark - Unpinning +///-------------------------------------- + +- (BOOL)unpin { + return [self unpinWithName:PFObjectDefaultPin]; +} + +- (BOOL)unpin:(NSError **)error { + return [self unpinWithName:PFObjectDefaultPin error:error]; +} + +- (BFTask *)unpinInBackground { + return [self unpinInBackgroundWithName:PFObjectDefaultPin]; +} + +- (void)unpinInBackgroundWithBlock:(PFBooleanResultBlock)block { + [[self unpinInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +- (BOOL)unpinWithName:(NSString *)name { + return [self unpinWithName:name error:nil]; +} + +- (BOOL)unpinWithName:(NSString *)name error:(NSError **)error { + return [[[self unpinInBackgroundWithName:name] waitForResult:error] boolValue]; +} + +- (BFTask *)unpinInBackgroundWithName:(NSString *)name { + return [PFObject unpinAllInBackground:@[ self ] withName:name]; +} + +- (void)unpinInBackgroundWithName:(NSString *)name block:(PFBooleanResultBlock)block { + [[self unpinInBackgroundWithName:name] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +///-------------------------------------- +#pragma mark - Unpinning Many Objects +///-------------------------------------- + ++ (BOOL)unpinAllObjects { + return [self unpinAllObjects:nil]; +} + ++ (BOOL)unpinAllObjects:(NSError **)error { + return [self unpinAllObjectsWithName:PFObjectDefaultPin error:error]; +} + ++ (BFTask *)unpinAllObjectsInBackground { + return [self unpinAllObjectsInBackgroundWithName:PFObjectDefaultPin]; +} + ++ (void)unpinAllObjectsInBackgroundWithBlock:(PFBooleanResultBlock)block { + [[self unpinAllObjectsInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (BOOL)unpinAllObjectsWithName:(NSString *)name { + return [self unpinAllObjectsWithName:name error:nil]; +} + ++ (BOOL)unpinAllObjectsWithName:(NSString *)name error:(NSError **)error { + return [[[self unpinAllObjectsInBackgroundWithName:name] waitForResult:error] boolValue]; +} + ++ (void)unpinAllObjectsInBackgroundWithName:(NSString *)name block:(PFBooleanResultBlock)block { + [[self unpinAllObjectsInBackgroundWithName:name] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (BFTask *)unpinAllObjectsInBackgroundWithName:(NSString *)name { + return [[self pinningObjectStore] unpinAllObjectsAsyncWithPinName:name]; +} + ++ (BOOL)unpinAll:(NSArray *)objects { + return [self unpinAll:objects error:nil]; +} + ++ (BOOL)unpinAll:(NSArray *)objects error:(NSError **)error { + return [self unpinAll:objects withName:PFObjectDefaultPin error:error]; +} + ++ (BFTask *)unpinAllInBackground:(NSArray *)objects { + return [self unpinAllInBackground:objects withName:PFObjectDefaultPin]; +} + ++ (void)unpinAllInBackground:(NSArray *)objects block:(PFBooleanResultBlock)block { + [[self unpinAllInBackground:objects] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (BOOL)unpinAll:(NSArray *)objects withName:(NSString *)name { + return [self unpinAll:objects withName:name error:nil]; +} + ++ (BOOL)unpinAll:(NSArray *)objects withName:(NSString *)name error:(NSError **)error { + return [[[self unpinAllInBackground:objects withName:name] waitForResult:error] boolValue]; +} + ++ (BFTask *)unpinAllInBackground:(NSArray *)objects withName:(NSString *)name { + return [[self pinningObjectStore] unpinObjectsAsync:objects withPinName:name]; +} + ++ (void)unpinAllInBackground:(NSArray *)objects + withName:(NSString *)name + block:(PFBooleanResultBlock)block { + [[self unpinAllInBackground:objects withName:name] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +///-------------------------------------- +#pragma mark - Data Source +///-------------------------------------- + ++ (id)objectController { + return [Parse _currentManager].coreManager.objectController; +} + ++ (PFObjectFileCodingLogic *)objectFileCodingLogic { + return [PFObjectFileCodingLogic codingLogic]; +} + ++ (PFObjectBatchController *)objectBatchController { + return [Parse _currentManager].coreManager.objectBatchController; +} + ++ (PFPinningObjectStore *)pinningObjectStore { + return [Parse _currentManager].coreManager.pinningObjectStore; +} + ++ (PFCurrentUserController *)currentUserController { + return [Parse _currentManager].coreManager.currentUserController; +} + ++ (PFObjectSubclassingController *)subclassingController { + return [PFObjectSubclassingController defaultController]; +} + +@end diff --git a/Parse/PFProduct.h b/Parse/PFProduct.h new file mode 100644 index 000000000..ddb6f0faa --- /dev/null +++ b/Parse/PFProduct.h @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import +#import +#import + +PF_ASSUME_NONNULL_BEGIN + +/*! + The `PFProduct` class represents an in-app purchase product on the Parse server. + By default, products can only be created via the Data Browser. Saving a `PFProduct` will result in error. + However, the products' metadata information can be queried and viewed. + + This class is currently for iOS only. + */ +@interface PFProduct : PFObject + +///-------------------------------------- +/// @name Product-specific Properties +///-------------------------------------- + +/*! + @abstract The product identifier of the product. + + @discussion This should match the product identifier in iTunes Connect exactly. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSString *productIdentifier; + +/*! + @abstract The icon of the product. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) PFFile *icon; + +/*! + @abstract The title of the product. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSString *title; + +/*! + @abstract The subtitle of the product. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSString *subtitle; + +/*! + @abstract The order in which the product information is displayed in . + + @discussion The product with a smaller order is displayed earlier in the . + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSNumber *order; + +/*! + @abstract The name of the associated download. + + @discussion If there is no downloadable asset, it should be `nil`. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong, readonly) NSString *downloadName; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFProduct.m b/Parse/PFProduct.m new file mode 100644 index 000000000..4eb2f2b56 --- /dev/null +++ b/Parse/PFProduct.m @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFProduct.h" +#import "PFProduct+Private.h" + +#import "PFAssert.h" +#import "PFObject+Subclass.h" + +@implementation PFProduct + +@dynamic productIdentifier; +@dynamic icon; +@dynamic title; +@dynamic subtitle; +@dynamic order; +@dynamic downloadName; + +///-------------------------------------- +#pragma mark - PFSubclassing +///-------------------------------------- + +// Validates a class name. We override this to only allow the product class name. ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert([className isEqualToString:[PFProduct parseClassName]], + @"Cannot initialize a PFProduct with a custom class name."); +} + ++ (NSString *)parseClassName { + return @"_Product"; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + +@dynamic price; +@dynamic priceLocale; +@dynamic contentPath; +@dynamic progress; + +@end diff --git a/Parse/PFPurchase.h b/Parse/PFPurchase.h new file mode 100644 index 000000000..a297bea01 --- /dev/null +++ b/Parse/PFPurchase.h @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import + +@class PFProduct; + +/*! + `PFPurchase` provides a set of APIs for working with in-app purchases. + + This class is currently for iOS only. + */ +@interface PFPurchase : NSObject + +/*! + @abstract Add application logic block which is run when buying a product. + + @discussion This method should be called once for each product, and should be called before + calling . All invocations to should happen within + the same method, and on the main thread. It is recommended to place all invocations of this method + in `application:didFinishLaunchingWithOptions:`. + + @param productIdentifier the product identifier + @param block The block to be run when buying a product. + */ ++ (void)addObserverForProduct:(NSString *)productIdentifier + block:(void(^)(SKPaymentTransaction *transaction))block; + +/*! + @abstract *Asynchronously* initiates the purchase for the product. + + @param productIdentifier the product identifier + @param block the completion block. + */ ++ (void)buyProduct:(NSString *)productIdentifier block:(void(^)(NSError *error))block; + +/*! + @abstract *Asynchronously* download the purchased asset, which is stored on Parse's server. + + @discussion Parse verifies the receipt with Apple and delivers the content only if the receipt is valid. + + @param transaction the transaction, which contains the receipt. + @param completion the completion block. + */ ++ (void)downloadAssetForTransaction:(SKPaymentTransaction *)transaction + completion:(void(^)(NSString *filePath, NSError *error))completion; + +/*! + @abstract *Asynchronously* download the purchased asset, which is stored on Parse's server. + + @discussion Parse verifies the receipt with Apple and delivers the content only if the receipt is valid. + + @param transaction the transaction, which contains the receipt. + @param completion the completion block. + @param progress the progress block, which is called multiple times to reveal progress of the download. + */ ++ (void)downloadAssetForTransaction:(SKPaymentTransaction *)transaction + completion:(void(^)(NSString *filePath, NSError *error))completion + progress:(PFProgressBlock)progress; + +/*! + @abstract *Asynchronously* restore completed transactions for the current user. + + @discussion Only nonconsumable purchases are restored. If observers for the products have been added before + calling this method, invoking the method reruns the application logic associated with the purchase. + + @warning This method is only important to developers who want to preserve purchase states across + different installations of the same app. + */ ++ (void)restore; + +/*! + @abstract Returns a content path of the asset of a product, if it was purchased and downloaded. + + @discussion To download and verify purchases use . + + @warning This method will return `nil`, if the purchase wasn't verified or if the asset was not downloaded. + */ ++ (NSString *)assetContentPathForProduct:(PFProduct *)product; + +@end diff --git a/Parse/PFPurchase.m b/Parse/PFPurchase.m new file mode 100644 index 000000000..03f0d161a --- /dev/null +++ b/Parse/PFPurchase.m @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPurchase.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFConstants.h" +#import "PFPaymentTransactionObserver.h" +#import "PFProduct.h" +#import "PFPurchaseController.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +@implementation PFPurchase + +///-------------------------------------- +#pragma mark - Public +///-------------------------------------- + ++ (void)addObserverForProduct:(NSString *)productIdentifier block:(void(^)(SKPaymentTransaction *))block { + // We require the following method to run on the main thread because we want to add the observer + // *after* all products handlers have been added. Developers might be calling this method multiple + // times; and if the observer is added after the first call, the observer might not know how to + // handle some purchases. + + PFConsistencyAssert([NSThread isMainThread], @"%@ must be called on the main thread.", NSStringFromSelector(_cmd)); + PFParameterAssert(productIdentifier, @"You must pass in a valid product identifier."); + PFParameterAssert(block, @"You must pass in a valid block for the product."); + + [[Parse _currentManager].purchaseController.transactionObserver handle:productIdentifier block:block]; +} + ++ (void)buyProduct:(NSString *)productIdentifier block:(void(^)(NSError *))completion { + [[[self _purchaseController] buyProductAsyncWithIdentifier:productIdentifier] continueWithBlock:^id(BFTask *task) { + if (completion) { + completion(task.error); + } + return nil; + }]; +} + ++ (void)restore { + [[self _purchaseController].paymentQueue restoreCompletedTransactions]; +} + ++ (void)downloadAssetForTransaction:(SKPaymentTransaction *)transaction completion:(PFStringResultBlock)completion { + [self downloadAssetForTransaction:transaction completion:completion progress:nil]; +} + ++ (void)downloadAssetForTransaction:(SKPaymentTransaction *)transaction + completion:(PFStringResultBlock)completion + progress:(PFProgressBlock)progress { + @weakify(self); + [[[PFUser _getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + @strongify(self); + NSString *sessionToken = task.result; + return [[self _purchaseController] downloadAssetAsyncForTransaction:transaction + withProgressBlock:progress + sessionToken:sessionToken]; + }] continueWithMainThreadResultBlock:completion executeIfCancelled:YES]; +} + ++ (NSString *)assetContentPathForProduct:(PFProduct *)product { + NSString *path = [[self _purchaseController] assetContentPathForProductWithIdentifier:product.productIdentifier + fileName:product.downloadName]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + return path; + } + + return nil; +} + +///-------------------------------------- +#pragma mark - Purchase Controller +///-------------------------------------- + ++ (PFPurchaseController *)_purchaseController { + return [Parse _currentManager].purchaseController; +} + +@end diff --git a/Parse/PFPush.h b/Parse/PFPush.h new file mode 100644 index 000000000..69984d691 --- /dev/null +++ b/Parse/PFPush.h @@ -0,0 +1,530 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@class BFTask; +@class PFQuery; + +/*! + The `PFPush` class defines a push notification that can be sent from a client device. + + The preferred way of modifying or retrieving channel subscriptions is to use + the class, instead of the class methods in `PFPush`. + */ +@interface PFPush : NSObject + +///-------------------------------------- +/// @name Creating a Push Notification +///-------------------------------------- + ++ (instancetype)push; + +///-------------------------------------- +/// @name Configuring a Push Notification +///-------------------------------------- + +/*! + @abstract Sets the channel on which this push notification will be sent. + + @param channel The channel to set for this push. + The channel name must start with a letter and contain only letters, numbers, dashes, and underscores. + */ +- (void)setChannel:(NSString *)channel; + +/*! + @abstract Sets the array of channels on which this push notification will be sent. + + @param channels The array of channels to set for this push. + Each channel name must start with a letter and contain only letters, numbers, dashes, and underscores. + */ +- (void)setChannels:(NSArray *)channels; + +/*! + @abstract Sets an installation query to which this push notification will be sent. + + @discussion The query should be created via <[PFInstallation query]> and should not specify a skip, limit, or order. + + @param query The installation query to set for this push. + */ +- (void)setQuery:(PFQuery *)query; + +/*! + @abstract Sets an alert message for this push notification. + + @warning This will overwrite any data specified in setData. + + @param message The message to send in this push. + */ +- (void)setMessage:(NSString *)message; + +/*! + @abstract Sets an arbitrary data payload for this push notification. + + @discussion See the guide for information about the dictionary structure. + + @warning This will overwrite any data specified in setMessage. + + @param data The data to send in this push. + */ +- (void)setData:(NSDictionary *)data; + +/*! + @abstract Sets whether this push will go to Android devices. + + @param pushToAndroid Whether this push will go to Android devices. + + @deprecated Please use a `[PFInstallation query]` with a constraint on deviceType instead. + */ +- (void)setPushToAndroid:(BOOL)pushToAndroid PARSE_DEPRECATED("Please use a [PFInstallation query] with a constraint on deviceType. This method is deprecated and won't do anything."); + +/*! + @abstract Sets whether this push will go to iOS devices. + + @param pushToIOS Whether this push will go to iOS devices. + + @deprecated Please use a `[PFInstallation query]` with a constraint on deviceType instead. + */ +- (void)setPushToIOS:(BOOL)pushToIOS PARSE_DEPRECATED("Please use a [PFInstallation query] with a constraint on deviceType. This method is deprecated and won't do anything."); + +/*! + @abstract Sets the expiration time for this notification. + + @discussion The notification will be sent to devices which are either online + at the time the notification is sent, or which come online before the expiration time is reached. + Because device clocks are not guaranteed to be accurate, + most applications should instead use . + + @see expireAfterTimeInterval: + + @param date The time at which the notification should expire. + */ +- (void)expireAtDate:(NSDate *)date; + +/*! + @abstract Sets the time interval after which this notification should expire. + + @discussion This notification will be sent to devices which are either online at + the time the notification is sent, or which come online within the given + time interval of the notification being received by Parse's server. + An interval which is less than or equal to zero indicates that the + message should only be sent to devices which are currently online. + + @param timeInterval The interval after which the notification should expire. + */ +- (void)expireAfterTimeInterval:(NSTimeInterval)timeInterval; + +/*! + @abstract Clears both expiration values, indicating that the notification should never expire. + */ +- (void)clearExpiration; + +///-------------------------------------- +/// @name Sending Push Notifications +///-------------------------------------- + +/*! + @abstract *Synchronously* send a push message to a channel. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param message The message to send. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the send succeeded. + */ ++ (BOOL)sendPushMessageToChannel:(NSString *)channel + withMessage:(NSString *)message + error:(NSError **)error; + +/*! + @abstract *Asynchronously* send a push message to a channel. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param message The message to send. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)sendPushMessageToChannelInBackground:(NSString *)channel + withMessage:(NSString *)message; + +/*! + @abstract *Asynchronously* sends a push message to a channel and calls the given block. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param message The message to send. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)` + */ ++ (void)sendPushMessageToChannelInBackground:(NSString *)channel + withMessage:(NSString *)message + block:(PFBooleanResultBlock)block; + +/* + @abstract *Asynchronously* send a push message to a channel. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param message The message to send. + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ ++ (void)sendPushMessageToChannelInBackground:(NSString *)channel + withMessage:(NSString *)message + target:(id)target + selector:(SEL)selector; + +/*! + @abstract Send a push message to a query. + + @param query The query to send to. The query must be a query created with <[PFInstallation query]>. + @param message The message to send. + @param error Pointer to an NSError that will be set if necessary. + + @returns Returns whether the send succeeded. + */ ++ (BOOL)sendPushMessageToQuery:(PFQuery *)query + withMessage:(NSString *)message + error:(NSError **)error; + +/*! + @abstract *Asynchronously* send a push message to a query. + + @param query The query to send to. The query must be a query created with <[PFInstallation query]>. + @param message The message to send. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)sendPushMessageToQueryInBackground:(PFQuery *)query + withMessage:(NSString *)message; + +/*! + @abstract *Asynchronously* sends a push message to a query and calls the given block. + + @param query The query to send to. The query must be a PFInstallation query + created with [PFInstallation query]. + @param message The message to send. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)` + */ ++ (void)sendPushMessageToQueryInBackground:(PFQuery *)query + withMessage:(NSString *)message + block:(PFBooleanResultBlock)block; + +/*! + @abstract *Synchronously* send this push message. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the send succeeded. + */ +- (BOOL)sendPush:(NSError **)error; + +/*! + @abstract *Asynchronously* send this push message. + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)sendPushInBackground; + +/*! + @abstract *Asynchronously* send this push message and executes the given callback block. + + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ +- (void)sendPushInBackgroundWithBlock:(PFBooleanResultBlock)block; + +/* + @abstract *Asynchronously* send this push message and calls the given callback. + + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ +- (void)sendPushInBackgroundWithTarget:(id)target selector:(SEL)selector; + +/*! + @abstract *Synchronously* send a push message with arbitrary data to a channel. + + @discussion See the guide for information about the dictionary structure. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param data The data to send. + @param error Pointer to an NSError that will be set if necessary. + + @returns Returns whether the send succeeded. + */ ++ (BOOL)sendPushDataToChannel:(NSString *)channel + withData:(NSDictionary *)data + error:(NSError **)error; + +/*! + @abstract *Asynchronously* send a push message with arbitrary data to a channel. + + @discussion See the guide for information about the dictionary structure. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param data The data to send. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)sendPushDataToChannelInBackground:(NSString *)channel + withData:(NSDictionary *)data; + +/*! + @abstract Asynchronously sends a push message with arbitrary data to a channel and calls the given block. + + @discussion See the guide for information about the dictionary structure. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param data The data to send. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)sendPushDataToChannelInBackground:(NSString *)channel + withData:(NSDictionary *)data + block:(PFBooleanResultBlock)block; + +/* + @abstract *Asynchronously* send a push message with arbitrary data to a channel. + + @discussion See the guide for information about the dictionary structure. + + @param channel The channel to send to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param data The data to send. + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ ++ (void)sendPushDataToChannelInBackground:(NSString *)channel + withData:(NSDictionary *)data + target:(id)target + selector:(SEL)selector; + +/*! + @abstract *Synchronously* send a push message with arbitrary data to a query. + + @discussion See the guide for information about the dictionary structure. + + @param query The query to send to. The query must be a query + created with <[PFInstallation query]>. + @param data The data to send. + @param error Pointer to an NSError that will be set if necessary. + + @returns Returns whether the send succeeded. + */ ++ (BOOL)sendPushDataToQuery:(PFQuery *)query + withData:(NSDictionary *)data + error:(NSError **)error; + +/*! + @abstract Asynchronously send a push message with arbitrary data to a query. + + @discussion See the guide for information about the dictionary structure. + + @param query The query to send to. The query must be a query + created with <[PFInstallation query]>. + @param data The data to send. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)sendPushDataToQueryInBackground:(PFQuery *)query + withData:(NSDictionary *)data; + +/*! + @abstract *Asynchronously* sends a push message with arbitrary data to a query and calls the given block. + + @discussion See the guide for information about the dictionary structure. + + @param query The query to send to. The query must be a query + created with <[PFInstallation query]>. + @param data The data to send. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)sendPushDataToQueryInBackground:(PFQuery *)query + withData:(NSDictionary *)data + block:(PFBooleanResultBlock)block; + +///-------------------------------------- +/// @name Handling Notifications +///-------------------------------------- + +/*! + @abstract A default handler for push notifications while the app is active that + could be used to mimic the behavior of iOS push notifications while the app is backgrounded or not running. + + @discussion Call this from `application:didReceiveRemoteNotification:`. + If push has a dictionary containing loc-key and loc-args in the alert, + we support up to 10 items in loc-args (`NSRangeException` if limit exceeded). + + @warning This method is available only on iOS. + + @param userInfo The userInfo dictionary you get in `appplication:didReceiveRemoteNotification:`. + */ ++ (void)handlePush:(NSDictionary *)userInfo NS_AVAILABLE_IOS(3_0); + +///-------------------------------------- +/// @name Managing Channel Subscriptions +///-------------------------------------- + +/*! + @abstract Store the device token locally for push notifications. + + @discussion Usually called from you main app delegate's `didRegisterForRemoteNotificationsWithDeviceToken:`. + + @param deviceToken Either as an `NSData` straight from `application:didRegisterForRemoteNotificationsWithDeviceToken:` + or as an `NSString` if you converted it yourself. + */ ++ (void)storeDeviceToken:(id)deviceToken; + +/*! + @abstract *Synchronously* get all the channels that this device is subscribed to. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns an `NSSet` containing all the channel names this device is subscribed to. + */ ++ (NSSet *)getSubscribedChannels:(NSError **)error; + +/*! + @abstract *Asynchronously* get all the channels that this device is subscribed to. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)getSubscribedChannelsInBackground; + +/*! + @abstract *Asynchronously* get all the channels that this device is subscribed to. + @param block The block to execute. + It should have the following argument signature: `^(NSSet *channels, NSError *error)`. + */ ++ (void)getSubscribedChannelsInBackgroundWithBlock:(PFSetResultBlock)block; + +/* + @abstract *Asynchronously* get all the channels that this device is subscribed to. + + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSSet *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + */ ++ (void)getSubscribedChannelsInBackgroundWithTarget:(id)target + selector:(SEL)selector; + +/*! + @abstract *Synchrnously* subscribes the device to a channel of push notifications. + + @param channel The channel to subscribe to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the subscribe succeeded. + */ ++ (BOOL)subscribeToChannel:(NSString *)channel error:(NSError **)error; + +/*! + @abstract *Asynchronously* subscribes the device to a channel of push notifications. + + @param channel The channel to subscribe to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)subscribeToChannelInBackground:(NSString *)channel; + +/*! + @abstract *Asynchronously* subscribes the device to a channel of push notifications and calls the given block. + + @param channel The channel to subscribe to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)` + */ ++ (void)subscribeToChannelInBackground:(NSString *)channel + block:(PFBooleanResultBlock)block; + +/* + @abstract *Asynchronously* subscribes the device to a channel of push notifications and calls the given callback. + + @param channel The channel to subscribe to. The channel name must start with + a letter and contain only letters, numbers, dashes, and underscores. + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ ++ (void)subscribeToChannelInBackground:(NSString *)channel + target:(id)target + selector:(SEL)selector; + +/*! + @abstract *Synchronously* unsubscribes the device to a channel of push notifications. + + @param channel The channel to unsubscribe from. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns whether the unsubscribe succeeded. + */ ++ (BOOL)unsubscribeFromChannel:(NSString *)channel error:(NSError **)error; + +/*! + @abstract *Asynchronously* unsubscribes the device from a channel of push notifications. + + @param channel The channel to unsubscribe from. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)unsubscribeFromChannelInBackground:(NSString *)channel; + +/*! + @abstract *Asynchronously* unsubscribes the device from a channel of push notifications and calls the given block. + + @param channel The channel to unsubscribe from. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)unsubscribeFromChannelInBackground:(NSString *)channel + block:(PFBooleanResultBlock)block; + +/* + @abstract *Asynchronously* unsubscribes the device from a channel of push notifications and calls the given callback. + + @param channel The channel to unsubscribe from. + @param target The object to call selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ ++ (void)unsubscribeFromChannelInBackground:(NSString *)channel + target:(id)target + selector:(SEL)selector; + +@end diff --git a/Parse/PFPush.m b/Parse/PFPush.m new file mode 100644 index 000000000..07eb36209 --- /dev/null +++ b/Parse/PFPush.m @@ -0,0 +1,461 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPush.h" +#import "PFPushPrivate.h" + +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFEncoder.h" +#import "PFHash.h" +#import "PFInstallationPrivate.h" +#import "PFKeychainStore.h" +#import "PFMacros.h" +#import "PFMutablePushState.h" +#import "PFMutableQueryState.h" +#import "PFPushChannelsController.h" +#import "PFPushController.h" +#import "PFPushManager.h" +#import "PFPushUtilities.h" +#import "PFQueryPrivate.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +static Class _pushInternalUtilClass = nil; + +@interface PFPush () + +@property (nonatomic, strong) PFMutablePushState *state; +@property (nonatomic, strong) PFQuery *query; + +@end + +@implementation PFPush (Private) + ++ (Class)pushInternalUtilClass { + return _pushInternalUtilClass ?: [PFPushUtilities class]; +} + ++ (void)setPushInternalUtilClass:(Class)utilClass { + if (utilClass) { + PFParameterAssert([utilClass conformsToProtocol:@protocol(PFPushInternalUtils)], + @"utilClass must conform to PFPushInternalUtils protocol"); + } + _pushInternalUtilClass = utilClass; +} + +@end + +@implementation PFPush + +///-------------------------------------- +#pragma mark - Instance +///-------------------------------------- + +#pragma mark Init + ++ (instancetype)push { + return [[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _state = [[PFMutablePushState alloc] init]; + + return self; +} + +#pragma mark Accessors + +- (void)setQuery:(PFQuery *)query { + PFParameterAssert(!self.state.channels || !query, @"Can't set both the query and channel(s) properties."); + _query = query; +} + +- (void)setChannelSet:(NSSet *)channelSet { + PFParameterAssert(!self.query || !channelSet, @"Can't set both the query and channel(s) properties."); + self.state.channels = channelSet; +} + +- (void)setChannel:(NSString *)channel { + self.channelSet = PF_SET(channel); +} + +- (void)setChannels:(NSArray *)channels { + self.channelSet = [NSSet setWithArray:channels]; +} + +- (void)setMessage:(NSString *)message { + [self.state setPayloadWithMessage:message]; +} + +- (void)expireAtDate:(NSDate *)date { + self.state.expirationDate = date; + self.state.expirationTimeInterval = nil; +} + +- (void)expireAfterTimeInterval:(NSTimeInterval)timeInterval { + self.state.expirationDate = nil; + self.state.expirationTimeInterval = @(timeInterval); +} + +- (void)clearExpiration { + self.state.expirationDate = nil; + self.state.expirationTimeInterval = nil; +} + +- (void)setData:(NSDictionary *)data { + self.state.payload = data; +} + +#pragma mark Sending + +- (BOOL)sendPush:(NSError **)error { + return [[[self sendPushInBackground] waitForResult:error] boolValue]; +} + +- (BFTask *)sendPushInBackground { + if (self.query) { + PFParameterAssert(!self.query.state.sortKeys, @"Cannot send push notifications to an ordered query."); + PFParameterAssert(self.query.state.limit == -1, @"Cannot send push notifications to a limit query."); + PFParameterAssert(self.query.state.skip == 0, @"Cannot send push notifications to a skip query."); + } + + // Capture state first. + PFPushController *pushController = [[self class] pushController]; + PFPushState *state = [self _currentStateCopy]; + return [[PFUser _getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [pushController sendPushNotificationAsyncWithState:state sessionToken:sessionToken]; + }]; +} + +- (void)sendPushInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self sendPushInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +- (void)sendPushInBackgroundWithBlock:(PFBooleanResultBlock)block { + [[self sendPushInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + +#pragma mark Command + +- (PFPushState *)_currentStateCopy { + if (self.query) { + PFMutablePushState *state = [self.state mutableCopy]; + state.queryState = self.query.state; + return [state copy]; + } + return [self.state copy]; +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (instancetype)copyWithZone:(NSZone *)zone { + PFPush *push = [[PFPush allocWithZone:zone] init]; + push.state = [self.state mutableCopy]; + return push; +} + +///-------------------------------------- +#pragma mark - NSObject +///-------------------------------------- + +- (NSUInteger)hash { + return PFIntegerPairHash([self.query hash], [self.state hash]); +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[PFPush class]]) { + return NO; + } + + PFPush *push = (PFPush *)object; + return (((self.query == nil && push.query == nil) || + [self.query isEqual:push.query]) && + [self.state isEqual:push.state]); +} + +///-------------------------------------- +#pragma mark - Sending Push Notifications +///-------------------------------------- + +#pragma mark To Channel + ++ (BOOL)sendPushMessageToChannel:(NSString *)channel + withMessage:(NSString *)message + error:(NSError **)error { + return [[[self sendPushMessageToChannelInBackground:channel withMessage:message] waitForResult:error] boolValue]; +} + ++ (BFTask *)sendPushMessageToChannelInBackground:(NSString *)channel + withMessage:(NSString *)message { + NSDictionary *data = @{ @"alert" : message }; + return [self sendPushDataToChannelInBackground:channel withData:data]; +} + ++ (void)sendPushMessageToChannelInBackground:(NSString *)channel + withMessage:(NSString *)message + block:(PFBooleanResultBlock)block { + [[self sendPushMessageToChannelInBackground:channel + withMessage:message] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (void)sendPushMessageToChannelInBackground:(NSString *)channel + withMessage:(NSString *)message + target:(id)target + selector:(SEL)selector { + [self sendPushMessageToChannelInBackground:channel withMessage:message block:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + ++ (BOOL)sendPushDataToChannel:(NSString *)channel + withData:(NSDictionary *)data + error:(NSError **)error { + return [[[PFPush sendPushDataToChannelInBackground:channel withData:data] waitForResult:error] boolValue]; +} + ++ (BFTask *)sendPushDataToChannelInBackground:(NSString *)channel withData:(NSDictionary *)data { + PFPush *push = [self push]; + [push setChannel:channel]; + [push setData:data]; + return [push sendPushInBackground]; +} + ++ (void)sendPushDataToChannelInBackground:(NSString *)channel + withData:(NSDictionary *)data + block:(PFBooleanResultBlock)block { + [[self sendPushDataToChannelInBackground:channel withData:data] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (void)sendPushDataToChannelInBackground:(NSString *)channel + withData:(NSDictionary *)data + target:(id)target + selector:(SEL)selector { + [self sendPushDataToChannelInBackground:channel withData:data block:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +#pragma mark To Query + ++ (BOOL)sendPushMessageToQuery:(PFQuery *)query + withMessage:(NSString *)message + error:(NSError **)error { + PFPush *push = [PFPush push]; + push.query = query; + push.message = message; + return [push sendPush:error]; +} + ++ (BFTask *)sendPushMessageToQueryInBackground:(PFQuery *)query + withMessage:(NSString *)message { + PFPush *push = [PFPush push]; + push.query = query; + push.message = message; + return [push sendPushInBackground]; +} + ++ (void)sendPushMessageToQueryInBackground:(PFQuery *)query + withMessage:(NSString *)message + block:(PFBooleanResultBlock)block { + PFPush *push = [PFPush push]; + push.query = query; + push.message = message; + [push sendPushInBackgroundWithBlock:block]; +} + + ++ (BOOL)sendPushDataToQuery:(PFQuery *)query + withData:(NSDictionary *)data + error:(NSError **)error { + PFPush *push = [PFPush push]; + push.query = query; + push.data = data; + return [push sendPush:error]; +} + ++ (BFTask *)sendPushDataToQueryInBackground:(PFQuery *)query + withData:(NSDictionary *)data { + PFPush *push = [PFPush push]; + push.query = query; + push.data = data; + return [push sendPushInBackground]; +} + ++ (void)sendPushDataToQueryInBackground:(PFQuery *)query + withData:(NSDictionary *)data + block:(PFBooleanResultBlock)block { + PFPush *push = [PFPush push]; + push.query = query; + push.data = data; + [push sendPushInBackgroundWithBlock:block]; +} + +///-------------------------------------- +#pragma mark - Channels +///-------------------------------------- + +#pragma mark Get + ++ (NSSet *)getSubscribedChannels:(NSError **)error { + return [[self getSubscribedChannelsInBackground] waitForResult:error]; +} + ++ (BFTask *)getSubscribedChannelsInBackground { + return [[self channelsController] getSubscribedChannelsAsync]; +} + ++ (void)getSubscribedChannelsInBackgroundWithBlock:(PFSetResultBlock)block { + [[self getSubscribedChannelsInBackground] thenCallBackOnMainThreadAsync:block]; +} + ++ (void)getSubscribedChannelsInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self getSubscribedChannelsInBackgroundWithBlock:^(NSSet *channels, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:channels object:error]; + }]; +} + +#pragma mark Subscribe + ++ (BOOL)subscribeToChannel:(NSString *)channel error:(NSError **)error { + return [[[self subscribeToChannelInBackground:channel] waitForResult:error] boolValue]; +} + ++ (BFTask *)subscribeToChannelInBackground:(NSString *)channel { + return [[self channelsController] subscribeToChannelAsyncWithName:channel]; +} + ++ (void)subscribeToChannelInBackground:(NSString *)channel block:(PFBooleanResultBlock)block { + [[self subscribeToChannelInBackground:channel] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (void)subscribeToChannelInBackground:(NSString *)channel target:(id)target selector:(SEL)selector { + [self subscribeToChannelInBackground:channel block:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +#pragma mark Unsubscribe + ++ (BOOL)unsubscribeFromChannel:(NSString *)channel error:(NSError **)error { + return [[[self unsubscribeFromChannelInBackground:channel] waitForResult:error] boolValue]; +} + ++ (BFTask *)unsubscribeFromChannelInBackground:(NSString *)channel { + return [[self channelsController] unsubscribeFromChannelAsyncWithName:channel]; +} + ++ (void)unsubscribeFromChannelInBackground:(NSString *)channel block:(PFBooleanResultBlock)block { + [[self unsubscribeFromChannelInBackground:channel] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (void)unsubscribeFromChannelInBackground:(NSString *)channel target:(id)target selector:(SEL)selector { + [self unsubscribeFromChannelInBackground:channel block:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +///-------------------------------------- +#pragma mark - Handling Notifications +///-------------------------------------- + +#if PARSE_IOS_ONLY ++ (void)handlePush:(NSDictionary *)userInfo { + UIApplication *application = [UIApplication sharedApplication]; + if ([application applicationState] != UIApplicationStateActive) { + return; + } + + NSDictionary *aps = userInfo[@"aps"]; + id alert = aps[@"alert"]; + + if (alert) { + NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey]; + NSString *message = nil; + if ([alert isKindOfClass:[NSString class]]) { + message = alert; + } else if ([alert isKindOfClass:[NSDictionary class]]) { + NSDictionary *alertDict = alert; + NSString *locKey = alertDict[@"loc-key"]; + if (locKey) { + message = [PFInternalUtils _stringWithFormat:NSLocalizedString(locKey, nil) + arguments:alertDict[@"loc-args"]]; + } + } + if (message) { + [[self pushInternalUtilClass] showAlertViewWithTitle:appName message:message]; + } + } + + NSNumber *badgeNumber = aps[@"badge"]; + if (badgeNumber) { + NSInteger number = [aps[@"badge"] integerValue]; + [application setApplicationIconBadgeNumber:number]; + } + + NSString *soundName = aps[@"sound"]; + + if (soundName.length == 0 || [soundName isEqualToString:@"default"]) { + [[self pushInternalUtilClass] playVibrate]; + } else { + [[self pushInternalUtilClass] playAudioWithName:soundName]; + } + +} +#endif + +///-------------------------------------- +#pragma mark - Store Token +///-------------------------------------- + ++ (void)storeDeviceToken:(id)deviceToken { + NSString *deviceTokenString = [[self pushInternalUtilClass] convertDeviceTokenToString:deviceToken]; + [PFInstallation currentInstallation].deviceToken = deviceTokenString; +} + +///-------------------------------------- +#pragma mark - Deprecated +///-------------------------------------- + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)setPushToIOS:(BOOL)pushToIOS { +} + +- (void)setPushToAndroid:(BOOL)pushToAndroid { +} +#pragma clang diagnostic pop + +///-------------------------------------- +#pragma mark - Push Manager +///-------------------------------------- + ++ (PFPushController *)pushController { + return [Parse _currentManager].pushManager.pushController; +} + ++ (PFPushChannelsController *)channelsController { + return [Parse _currentManager].pushManager.channelsController; +} + +@end diff --git a/Parse/PFQuery.h b/Parse/PFQuery.h new file mode 100644 index 000000000..81463513c --- /dev/null +++ b/Parse/PFQuery.h @@ -0,0 +1,893 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#import +#import +#else +#import +#import +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class BFTask; + +/*! + The `PFQuery` class defines a query that is used to query for s. + */ +@interface PFQuery : NSObject + +///-------------------------------------- +/// @name Creating a Query for a Class +///-------------------------------------- + +/*! + @abstract Initializes the query with a class name. + + @param className The class name. + */ +- (instancetype)initWithClassName:(NSString *)className; + +/*! + @abstract Returns a `PFQuery` for a given class. + + @param className The class to query on. + + @returns A `PFQuery` object. + */ ++ (instancetype)queryWithClassName:(NSString *)className; + +/*! + @abstract Creates a PFQuery with the constraints given by predicate. + + @discussion The following types of predicates are supported: + + - Simple comparisons such as `=`, `!=`, `<`, `>`, `<=`, `>=`, and `BETWEEN` with a key and a constant. + - Containment predicates, such as `x IN {1, 2, 3}`. + - Key-existence predicates, such as `x IN SELF`. + - BEGINSWITH expressions. + - Compound predicates with `AND`, `OR`, and `NOT`. + - SubQueries with `key IN %@`, subquery. + + The following types of predicates are NOT supported: + + - Aggregate operations, such as `ANY`, `SOME`, `ALL`, or `NONE`. + - Regular expressions, such as `LIKE`, `MATCHES`, `CONTAINS`, or `ENDSWITH`. + - Predicates comparing one key to another. + - Complex predicates with many ORed clauses. + + @param className The class to query on. + @param predicate The predicate to create conditions from. + */ ++ (instancetype)queryWithClassName:(NSString *)className predicate:(PF_NULLABLE NSPredicate *)predicate; + +/*! + The class name to query for. + */ +@property (nonatomic, strong) NSString *parseClassName; + +///-------------------------------------- +/// @name Adding Basic Constraints +///-------------------------------------- + +/*! + @abstract Make the query include PFObjects that have a reference stored at the provided key. + + @discussion This has an effect similar to a join. You can use dot notation to specify which fields in + the included object are also fetch. + + @param key The key to load child s for. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)includeKey:(NSString *)key; + +/*! + @abstract Make the query restrict the fields of the returned s to include only the provided keys. + + @discussion If this is called multiple times, then all of the keys specified in each of the calls will be included. + + @param keys The keys to include in the result. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)selectKeys:(NSArray *)keys; + +/*! + @abstract Add a constraint that requires a particular key exists. + + @param key The key that should exist. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKeyExists:(NSString *)key; + +/*! + @abstract Add a constraint that requires a key not exist. + + @param key The key that should not exist. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKeyDoesNotExist:(NSString *)key; + +/*! + @abstract Add a constraint to the query that requires a particular key's object to be equal to the provided object. + + @param key The key to be constrained. + @param object The object that must be equalled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key equalTo:(id)object; + +/*! + @abstract Add a constraint to the query that requires a particular key's object to be less than the provided object. + + @param key The key to be constrained. + @param object The object that provides an upper bound. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key lessThan:(id)object; + +/*! + @abstract Add a constraint to the query that requires a particular key's object + to be less than or equal to the provided object. + + @param key The key to be constrained. + @param object The object that must be equalled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key lessThanOrEqualTo:(id)object; + +/*! + @abstract Add a constraint to the query that requires a particular key's object + to be greater than the provided object. + + @param key The key to be constrained. + @param object The object that must be equalled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key greaterThan:(id)object; + +/*! + @abstract Add a constraint to the query that requires a particular key's + object to be greater than or equal to the provided object. + + @param key The key to be constrained. + @param object The object that must be equalled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key greaterThanOrEqualTo:(id)object; + +/*! + @abstract Add a constraint to the query that requires a particular key's object + to be not equal to the provided object. + + @param key The key to be constrained. + @param object The object that must not be equalled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key notEqualTo:(id)object; + +/*! + @abstract Add a constraint to the query that requires a particular key's object + to be contained in the provided array. + + @param key The key to be constrained. + @param array The possible values for the key's object. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key containedIn:(NSArray *)array; + +/*! + @abstract Add a constraint to the query that requires a particular key's object + not be contained in the provided array. + + @param key The key to be constrained. + @param array The list of values the key's object should not be. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key notContainedIn:(NSArray *)array; + +/*! + @abstract Add a constraint to the query that requires a particular key's array + contains every element of the provided array. + + @param key The key to be constrained. + @param array The array of values to search for. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key containsAllObjectsInArray:(NSArray *)array; + +///-------------------------------------- +/// @name Adding Location Constraints +///-------------------------------------- + +/*! + @abstract Add a constraint to the query that requires a particular key's coordinates (specified via ) + be near a reference point. + + @discussion Distance is calculated based on angular distance on a sphere. Results will be sorted by distance + from reference point. + + @param key The key to be constrained. + @param geopoint The reference point represented as a . + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key nearGeoPoint:(PFGeoPoint *)geopoint; + +/*! + @abstract Add a constraint to the query that requires a particular key's coordinates (specified via ) + be near a reference point and within the maximum distance specified (in miles). + + @discussion Distance is calculated based on a spherical coordinate system. + Results will be sorted by distance (nearest to farthest) from the reference point. + + @param key The key to be constrained. + @param geopoint The reference point represented as a . + @param maxDistance Maximum distance in miles. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key + nearGeoPoint:(PFGeoPoint *)geopoint + withinMiles:(double)maxDistance; + +/*! + @abstract Add a constraint to the query that requires a particular key's coordinates (specified via ) + be near a reference point and within the maximum distance specified (in kilometers). + + @discussion Distance is calculated based on a spherical coordinate system. + Results will be sorted by distance (nearest to farthest) from the reference point. + + @param key The key to be constrained. + @param geopoint The reference point represented as a . + @param maxDistance Maximum distance in kilometers. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key + nearGeoPoint:(PFGeoPoint *)geopoint + withinKilometers:(double)maxDistance; + +/*! + Add a constraint to the query that requires a particular key's coordinates (specified via ) be near + a reference point and within the maximum distance specified (in radians). Distance is calculated based on + angular distance on a sphere. Results will be sorted by distance (nearest to farthest) from the reference point. + + @param key The key to be constrained. + @param geopoint The reference point as a . + @param maxDistance Maximum distance in radians. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key + nearGeoPoint:(PFGeoPoint *)geopoint + withinRadians:(double)maxDistance; + +/*! + @abstract Add a constraint to the query that requires a particular key's coordinates (specified via ) be + contained within a given rectangular geographic bounding box. + + @param key The key to be constrained. + @param southwest The lower-left inclusive corner of the box. + @param northeast The upper-right inclusive corner of the box. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key withinGeoBoxFromSouthwest:(PFGeoPoint *)southwest toNortheast:(PFGeoPoint *)northeast; + +///-------------------------------------- +/// @name Adding String Constraints +///-------------------------------------- + +/*! + @abstract Add a regular expression constraint for finding string values that match the provided regular expression. + + @warning This may be slow for large datasets. + + @param key The key that the string to match is stored in. + @param regex The regular expression pattern to match. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key matchesRegex:(NSString *)regex; + +/*! + @abstract Add a regular expression constraint for finding string values that match the provided regular expression. + + @warning This may be slow for large datasets. + + @param key The key that the string to match is stored in. + @param regex The regular expression pattern to match. + @param modifiers Any of the following supported PCRE modifiers: + - `i` - Case insensitive search + - `m` - Search across multiple lines of input + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key + matchesRegex:(NSString *)regex + modifiers:(PF_NULLABLE NSString *)modifiers; + +/*! + @abstract Add a constraint for finding string values that contain a provided substring. + + @warning This will be slow for large datasets. + + @param key The key that the string to match is stored in. + @param substring The substring that the value must contain. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key containsString:(PF_NULLABLE NSString *)substring; + +/*! + @abstract Add a constraint for finding string values that start with a provided prefix. + + @discussion This will use smart indexing, so it will be fast for large datasets. + + @param key The key that the string to match is stored in. + @param prefix The substring that the value must start with. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key hasPrefix:(PF_NULLABLE NSString *)prefix; + +/*! + @abstract Add a constraint for finding string values that end with a provided suffix. + + @warning This will be slow for large datasets. + + @param key The key that the string to match is stored in. + @param suffix The substring that the value must end with. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key hasSuffix:(PF_NULLABLE NSString *)suffix; + +///-------------------------------------- +/// @name Adding Subqueries +///-------------------------------------- + +/*! + Returns a `PFQuery` that is the `or` of the passed in queries. + + @param queries The list of queries to or together. + + @returns An instance of `PFQuery` that is the `or` of the passed in queries. + */ ++ (instancetype)orQueryWithSubqueries:(NSArray *)queries; + +/*! + @abstract Adds a constraint that requires that a key's value matches a value in another key + in objects returned by a sub query. + + @param key The key that the value is stored. + @param otherKey The key in objects in the returned by the sub query whose value should match. + @param query The query to run. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key + matchesKey:(NSString *)otherKey + inQuery:(PFQuery *)query; + +/*! + @abstract Adds a constraint that requires that a key's value `NOT` match a value in another key + in objects returned by a sub query. + + @param key The key that the value is stored. + @param otherKey The key in objects in the returned by the sub query whose value should match. + @param query The query to run. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key + doesNotMatchKey:(NSString *)otherKey + inQuery:(PFQuery *)query; + +/*! + @abstract Add a constraint that requires that a key's value matches a `PFQuery` constraint. + + @warning This only works where the key's values are s or arrays of s. + + @param key The key that the value is stored in + @param query The query the value should match + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key matchesQuery:(PFQuery *)query; + +/*! + @abstract Add a constraint that requires that a key's value to not match a `PFQuery` constraint. + + @warning This only works where the key's values are s or arrays of s. + + @param key The key that the value is stored in + @param query The query the value should not match + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)whereKey:(NSString *)key doesNotMatchQuery:(PFQuery *)query; + +///-------------------------------------- +/// @name Sorting +///-------------------------------------- + +/*! + @abstract Sort the results in *ascending* order with the given key. + + @param key The key to order by. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)orderByAscending:(NSString *)key; + +/*! + @abstract Additionally sort in *ascending* order by the given key. + + @discussion The previous keys provided will precedence over this key. + + @param key The key to order by. + */ +- (instancetype)addAscendingOrder:(NSString *)key; + +/*! + @abstract Sort the results in *descending* order with the given key. + + @param key The key to order by. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)orderByDescending:(NSString *)key; + +/*! + @abstract Additionally sort in *descending* order by the given key. + + @discussion The previous keys provided will precedence over this key. + + @param key The key to order by. + */ +- (instancetype)addDescendingOrder:(NSString *)key; + +/*! + @abstract Sort the results using a given sort descriptor. + + @warning If a `sortDescriptor` has custom `selector` or `comparator` - they aren't going to be used. + + @param sortDescriptor The `NSSortDescriptor` to use to sort the results of the query. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)orderBySortDescriptor:(NSSortDescriptor *)sortDescriptor; + +/*! + @abstract Sort the results using a given array of sort descriptors. + + @warning If a `sortDescriptor` has custom `selector` or `comparator` - they aren't going to be used. + + @param sortDescriptors An array of `NSSortDescriptor` objects to use to sort the results of the query. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)orderBySortDescriptors:(PF_NULLABLE NSArray *)sortDescriptors; + +///-------------------------------------- +/// @name Getting Objects by ID +///-------------------------------------- + +/*! + @abstract Returns a with a given class and id. + + @param objectClass The class name for the object that is being requested. + @param objectId The id of the object that is being requested. + + @returns The if found. Returns `nil` if the object isn't found, or if there was an error. + */ ++ (PF_NULLABLE PFObject *)getObjectOfClass:(NSString *)objectClass objectId:(NSString *)objectId; + +/*! + @abstract Returns a with a given class and id and sets an error if necessary. + + @param objectClass The class name for the object that is being requested. + @param objectId The id of the object that is being requested. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns The if found. Returns `nil` if the object isn't found, or if there was an `error`. + */ ++ (PF_NULLABLE PFObject *)getObjectOfClass:(NSString *)objectClass + objectId:(NSString *)objectId + error:(NSError **)error; + +/*! + @abstract Returns a with the given id. + + @warning This method mutates the query. + It will reset limit to `1`, skip to `0` and remove all conditions, leaving only `objectId`. + + @param objectId The id of the object that is being requested. + + @returns The if found. Returns nil if the object isn't found, or if there was an error. + */ +- (PF_NULLABLE PFObject *)getObjectWithId:(NSString *)objectId; + +/*! + @abstract Returns a with the given id and sets an error if necessary. + + @warning This method mutates the query. + It will reset limit to `1`, skip to `0` and remove all conditions, leaving only `objectId`. + + @param objectId The id of the object that is being requested. + @param error Pointer to an `NSError` that will be set if necessary. + + @returns The if found. Returns nil if the object isn't found, or if there was an error. + */ +- (PF_NULLABLE PFObject *)getObjectWithId:(NSString *)objectId error:(NSError **)error; + +/*! + @abstract Gets a asynchronously and calls the given block with the result. + + @warning This method mutates the query. + It will reset limit to `1`, skip to `0` and remove all conditions, leaving only `objectId`. + + @param objectId The id of the object that is being requested. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)getObjectInBackgroundWithId:(NSString *)objectId; + +/*! + @abstract Gets a asynchronously and calls the given block with the result. + + @warning This method mutates the query. + It will reset limit to `1`, skip to `0` and remove all conditions, leaving only `objectId`. + + @param objectId The id of the object that is being requested. + @param block The block to execute. + The block should have the following argument signature: `^(NSArray *object, NSError *error)` + */ +- (void)getObjectInBackgroundWithId:(NSString *)objectId + block:(PF_NULLABLE PFObjectResultBlock)block; + +/* + @abstract Gets a asynchronously. + + This mutates the PFQuery. It will reset limit to `1`, skip to `0` and remove all conditions, leaving only `objectId`. + + @param objectId The id of the object being requested. + @param target The target for the callback selector. + @param selector The selector for the callback. + It should have the following signature: `(void)callbackWithResult:(id)result error:(NSError *)error`. + Result will be `nil` if error is set and vice versa. + */ +- (void)getObjectInBackgroundWithId:(NSString *)objectId + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Getting User Objects +///-------------------------------------- + +/*! + @abstract Returns a with a given id. + + @param objectId The id of the object that is being requested. + + @returns The PFUser if found. Returns nil if the object isn't found, or if there was an error. + */ ++ (PF_NULLABLE PFUser *)getUserObjectWithId:(NSString *)objectId; + +/*! + Returns a PFUser with a given class and id and sets an error if necessary. + @param objectId The id of the object that is being requested. + @param error Pointer to an NSError that will be set if necessary. + @result The PFUser if found. Returns nil if the object isn't found, or if there was an error. + */ ++ (PF_NULLABLE PFUser *)getUserObjectWithId:(NSString *)objectId + error:(NSError **)error; + +/*! + @deprecated Please use [PFUser query] instead. + */ ++ (instancetype)queryForUser PARSE_DEPRECATED("Use [PFUser query] instead."); + +///-------------------------------------- +/// @name Getting all Matches for a Query +///-------------------------------------- + +/*! + @abstract Finds objects *synchronously* based on the constructed query. + + @returns Returns an array of objects that were found. + */ +- (PF_NULLABLE NSArray *)findObjects; + +/*! + @abstract Finds objects *synchronously* based on the constructed query and sets an error if there was one. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns an array of objects that were found. + */ +- (PF_NULLABLE NSArray *)findObjects:(NSError **)error; + +/*! + @abstract Finds objects *asynchronously* and sets the `NSArray` of objects as a result of the task. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)findObjectsInBackground; + +/*! + @abstract Finds objects *asynchronously* and calls the given block with the results. + + @param block The block to execute. + It should have the following argument signature: `^(NSArray *objects, NSError *error)` + */ +- (void)findObjectsInBackgroundWithBlock:(PF_NULLABLE PFArrayResultBlock)block; + +/* + @abstract Finds objects *asynchronously* and calls the given callback with the results. + + @param target The object to call the selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(id)result error:(NSError *)error`. + Result will be `nil` if error is set and vice versa. + */ +- (void)findObjectsInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Getting the First Match in a Query +///-------------------------------------- + +/*! + @abstract Gets an object *synchronously* based on the constructed query. + + @warning This method mutates the query. It will reset the limit to `1`. + + @returns Returns a , or `nil` if none was found. + */ +- (PF_NULLABLE PFObject *)getFirstObject; + +/*! + @abstract Gets an object *synchronously* based on the constructed query and sets an error if any occurred. + + @warning This method mutates the query. It will reset the limit to `1`. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns a , or `nil` if none was found. + */ +- (PF_NULLABLE PFObject *)getFirstObject:(NSError **)error; + +/*! + @abstract Gets an object *asynchronously* and sets it as a result of the task. + + @warning This method mutates the query. It will reset the limit to `1`. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)getFirstObjectInBackground; + +/*! + @abstract Gets an object *asynchronously* and calls the given block with the result. + + @warning This method mutates the query. It will reset the limit to `1`. + + @param block The block to execute. + It should have the following argument signature: `^(PFObject *object, NSError *error)`. + `result` will be `nil` if `error` is set OR no object was found matching the query. + `error` will be `nil` if `result` is set OR if the query succeeded, but found no results. + */ +- (void)getFirstObjectInBackgroundWithBlock:(PF_NULLABLE PFObjectResultBlock)block; + +/* + @abstract Gets an object *asynchronously* and calls the given callback with the results. + + @warning This method mutates the query. It will reset the limit to `1`. + + @param target The object to call the selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(PFObject *)result error:(NSError *)error`. + `result` will be `nil` if `error` is set OR no object was found matching the query. + `error` will be `nil` if `result` is set OR if the query succeeded, but found no results. + */ +- (void)getFirstObjectInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Counting the Matches in a Query +///-------------------------------------- + +/*! + @abstract Counts objects *synchronously* based on the constructed query. + + @returns Returns the number of objects that match the query, or `-1` if there is an error. + */ +- (NSInteger)countObjects; + +/*! + @abstract Counts objects *synchronously* based on the constructed query and sets an error if there was one. + + @param error Pointer to an `NSError` that will be set if necessary. + + @returns Returns the number of objects that match the query, or `-1` if there is an error. + */ +- (NSInteger)countObjects:(NSError **)error; + +/*! + @abstract Counts objects *asynchronously* and sets `NSNumber` with count as a result of the task. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)countObjectsInBackground; + +/*! + @abstract Counts objects *asynchronously* and calls the given block with the counts. + + @param block The block to execute. + It should have the following argument signature: `^(int count, NSError *error)` + */ +- (void)countObjectsInBackgroundWithBlock:(PF_NULLABLE PFIntegerResultBlock)block; + +/* + @abstract Counts objects *asynchronously* and calls the given callback with the count. + + @param target The object to call the selector on. + @param selector The selector to call. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + */ +- (void)countObjectsInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Cancelling a Query +///-------------------------------------- + +/*! + @abstract Cancels the current network request (if any). Ensures that callbacks won't be called. + */ +- (void)cancel; + +///-------------------------------------- +/// @name Paginating Results +///-------------------------------------- + +/*! + @abstract A limit on the number of objects to return. The default limit is `100`, with a + maximum of 1000 results being returned at a time. + + @warning If you are calling `findObjects` with `limit = 1`, you may find it easier to use `getFirst` instead. + */ +@property (nonatomic, assign) NSInteger limit; + +/*! + @abstract The number of objects to skip before returning any. + */ +@property (nonatomic, assign) NSInteger skip; + +///-------------------------------------- +/// @name Controlling Caching Behavior +///-------------------------------------- + +/*! + @abstract The cache policy to use for requests. + + Not allowed when Pinning is enabled. + + @see fromLocalDatastore + @see fromPin + @see fromPinWithName: + */ +@property (assign, readwrite) PFCachePolicy cachePolicy; + +/*! + @abstract The age after which a cached value will be ignored + */ +@property (assign, readwrite) NSTimeInterval maxCacheAge; + +/*! + @abstract Returns whether there is a cached result for this query. + + @result `YES` if there is a cached result for this query, otherwise `NO`. + */ +- (BOOL)hasCachedResult; + +/*! + @abstract Clears the cached result for this query. If there is no cached result, this is a noop. + */ +- (void)clearCachedResult; + +/*! + @abstract Clears the cached results for all queries. + */ ++ (void)clearAllCachedResults; + +///-------------------------------------- +/// @name Query Source +///-------------------------------------- + +/*! + @abstract Change the source of this query to all pinned objects. + + @warning Requires Local Datastore to be enabled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + + @see cachePolicy + */ +- (instancetype)fromLocalDatastore; + +/*! + @abstract Change the source of this query to the default group of pinned objects. + + @warning Requires Local Datastore to be enabled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + + @see PFObjectDefaultPin + @see cachePolicy + */ +- (instancetype)fromPin; + +/*! + @abstract Change the source of this query to a specific group of pinned objects. + + @warning Requires Local Datastore to be enabled. + + @param name The pinned group. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + + @see PFObjectDefaultPin + @see cachePolicy + */ +- (instancetype)fromPinWithName:(PF_NULLABLE NSString *)name; + +/*! + @abstract Ignore ACLs when querying from the Local Datastore. + + @discussion This is particularly useful when querying for objects with Role based ACLs set on them. + + @warning Requires Local Datastore to be enabled. + + @returns The same instance of `PFQuery` as the receiver. This allows method chaining. + */ +- (instancetype)ignoreACLs; + +///-------------------------------------- +/// @name Advanced Settings +///-------------------------------------- + +/*! + @abstract Whether or not performance tracing should be done on the query. + + @warning This should not be set to `YES` in most cases. + */ +@property (nonatomic, assign) BOOL trace; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFQuery.m b/Parse/PFQuery.m new file mode 100644 index 000000000..0c1d9436e --- /dev/null +++ b/Parse/PFQuery.m @@ -0,0 +1,1133 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFQuery.h" + +#import +#import + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCoreManager.h" +#import "PFCurrentUserController.h" +#import "PFGeoPointPrivate.h" +#import "PFInternalUtils.h" +#import "PFKeyValueCache.h" +#import "PFMutableQueryState.h" +#import "PFObject.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFPin.h" +#import "PFQueryController.h" +#import "PFQueryUtilities.h" +#import "PFRESTQueryCommand.h" +#import "PFUserPrivate.h" +#import "ParseInternal.h" +#import "Parse_Private.h" + +NSString *const PFQueryKeyNotEqualTo = @"$ne"; +NSString *const PFQueryKeyLessThan = @"$lt"; +NSString *const PFQueryKeyLessThanEqualTo = @"$lte"; +NSString *const PFQueryKeyGreaterThan = @"$gt"; +NSString *const PFQueryKeyGreaterThanOrEqualTo = @"$gte"; +NSString *const PFQueryKeyContainedIn = @"$in"; +NSString *const PFQueryKeyNotContainedIn = @"$nin"; +NSString *const PFQueryKeyContainsAll = @"$all"; +NSString *const PFQueryKeyNearSphere = @"$nearSphere"; +NSString *const PFQueryKeyWithin = @"$within"; +NSString *const PFQueryKeyRegex = @"$regex"; +NSString *const PFQueryKeyExists = @"$exists"; +NSString *const PFQueryKeyInQuery = @"$inQuery"; +NSString *const PFQueryKeyNotInQuery = @"$notInQuery"; +NSString *const PFQueryKeySelect = @"$select"; +NSString *const PFQueryKeyDontSelect = @"$dontSelect"; +NSString *const PFQueryKeyRelatedTo = @"$relatedTo"; +NSString *const PFQueryKeyOr = @"$or"; +NSString *const PFQueryKeyQuery = @"query"; +NSString *const PFQueryKeyKey = @"key"; +NSString *const PFQueryKeyObject = @"object"; + +NSString *const PFQueryOptionKeyMaxDistance = @"$maxDistance"; +NSString *const PFQueryOptionKeyBox = @"$box"; +NSString *const PFQueryOptionKeyRegexOptions = @"$options"; + +/*! + Checks if an object can be used as value for query equality clauses. + */ +static void PFQueryAssertValidEqualityClauseClass(id object) { + static NSArray *classes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + classes = @[ [NSString class], [NSNumber class], [NSDate class], [NSNull class], + [PFObject class], [PFGeoPoint class] ]; + }); + + for (Class class in classes) { + if ([object isKindOfClass:class]) { + return; + } + } + + PFParameterAssert(NO, @"Cannot do a comparison query for type: %@", [object class]); +} + +/*! + Checks if an object can be used as value for query ordering clauses. + */ +static void PFQueryAssertValidOrderingClauseClass(id object) { + static NSArray *classes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + classes = @[ [NSString class], [NSNumber class], [NSDate class] ]; + }); + + for (Class class in classes) { + if ([object isKindOfClass:class]) { + return; + } + } + + PFParameterAssert(NO, @"Cannot do a query that requires ordering for type: %@", [object class]); +} + +@interface PFQuery () { + BFCancellationTokenSource *_cancellationTokenSource; +} + +@property (nonatomic, strong, readwrite) PFMutableQueryState *state; + +@end + +@implementation PFQuery + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithState:(PFQueryState *)state { + self = [super init]; + if (!self) return nil; + + _state = [state mutableCopy]; + + return self; +} + +- (instancetype)initWithClassName:(NSString *)className { + self = [super init]; + if (!self) return nil; + + _state = [PFMutableQueryState stateWithParseClassName:className]; + + return self; +} + +///-------------------------------------- +#pragma mark - Public Accessors +///-------------------------------------- + +#pragma mark Basic + +- (NSString *)parseClassName { + return self.state.parseClassName; +} + +- (void)setParseClassName:(NSString *)parseClassName { + [self checkIfCommandIsRunning]; + self.state.parseClassName = parseClassName; +} + +#pragma mark Limit + +- (void)setLimit:(NSInteger)limit { + self.state.limit = limit; +} + +- (NSInteger)limit { + return self.state.limit; +} + +#pragma mark Skip + +- (void)setSkip:(NSInteger)skip { + self.state.skip = skip; +} + +- (NSInteger)skip { + return self.state.skip; +} + +#pragma mark Cache Policy + +- (void)setCachePolicy:(PFCachePolicy)cachePolicy { + [self _checkPinningEnabled:NO]; + [self checkIfCommandIsRunning]; + + self.state.cachePolicy = cachePolicy; +} + +- (PFCachePolicy)cachePolicy { + [self _checkPinningEnabled:NO]; + [self checkIfCommandIsRunning]; + + return self.state.cachePolicy; +} + +#pragma mark Cache Policy + +- (void)setMaxCacheAge:(NSTimeInterval)maxCacheAge { + self.state.maxCacheAge = maxCacheAge; +} + +- (NSTimeInterval)maxCacheAge { + return self.state.maxCacheAge; +} + +#pragma mark Trace + +- (void)setTrace:(BOOL)trace { + self.state.trace = trace; +} + +- (BOOL)trace { + return self.state.trace; +} + +///-------------------------------------- +#pragma mark - Order +///-------------------------------------- + +- (instancetype)orderByAscending:(NSString *)key { + [self checkIfCommandIsRunning]; + [self.state sortByKey:key ascending:YES]; + return self; +} + +- (instancetype)addAscendingOrder:(NSString *)key { + [self checkIfCommandIsRunning]; + [self.state addSortKey:key ascending:YES]; + return self; +} + +- (instancetype)orderByDescending:(NSString *)key { + [self checkIfCommandIsRunning]; + [self.state sortByKey:key ascending:NO]; + return self; +} + +- (instancetype)addDescendingOrder:(NSString *)key { + [self checkIfCommandIsRunning]; + [self.state addSortKey:key ascending:NO]; + return self; +} + +- (instancetype)orderBySortDescriptor:(NSSortDescriptor *)sortDescriptor { + NSString *key = sortDescriptor.key; + if (key) { + if (sortDescriptor.ascending) { + [self orderByAscending:key]; + } else { + [self orderByDescending:key]; + } + } + return self; +} + +- (instancetype)orderBySortDescriptors:(NSArray *)sortDescriptors { + [self.state addSortKeysFromSortDescriptors:sortDescriptors]; + return self; +} + +///-------------------------------------- +#pragma mark - Conditions +///-------------------------------------- + +// Helper for condition queries. +- (instancetype)whereKey:(NSString *)key condition:(NSString *)condition object:(id)object { + [self checkIfCommandIsRunning]; + [self.state setConditionType:condition withObject:object forKey:key]; + return self; +} + +- (instancetype)whereKey:(NSString *)key equalTo:(id)object { + [self checkIfCommandIsRunning]; + PFQueryAssertValidEqualityClauseClass(object); + [self.state setEqualityConditionWithObject:object forKey:key]; + return self; +} + +- (instancetype)whereKey:(NSString *)key greaterThan:(id)object { + PFQueryAssertValidOrderingClauseClass(object); + return [self whereKey:key condition:PFQueryKeyGreaterThan object:object]; +} + +- (instancetype)whereKey:(NSString *)key greaterThanOrEqualTo:(id)object { + PFQueryAssertValidOrderingClauseClass(object); + return [self whereKey:key condition:PFQueryKeyGreaterThanOrEqualTo object:object]; +} + +- (instancetype)whereKey:(NSString *)key lessThan:(id)object { + PFQueryAssertValidOrderingClauseClass(object); + return [self whereKey:key condition:PFQueryKeyLessThan object:object]; +} + +- (instancetype)whereKey:(NSString *)key lessThanOrEqualTo:(id)object { + PFQueryAssertValidOrderingClauseClass(object); + return [self whereKey:key condition:PFQueryKeyLessThanEqualTo object:object]; +} + +- (instancetype)whereKey:(NSString *)key notEqualTo:(id)object { + PFQueryAssertValidEqualityClauseClass(object); + return [self whereKey:key condition:PFQueryKeyNotEqualTo object:object]; +} + +- (instancetype)whereKey:(NSString *)key containedIn:(NSArray *)inArray { + return [self whereKey:key condition:PFQueryKeyContainedIn object:inArray]; +} + +- (instancetype)whereKey:(NSString *)key notContainedIn:(NSArray *)inArray { + return [self whereKey:key condition:PFQueryKeyNotContainedIn object:inArray]; +} + +- (instancetype)whereKey:(NSString *)key containsAllObjectsInArray:(NSArray *)array { + return [self whereKey:key condition:PFQueryKeyContainsAll object:array]; +} + +- (instancetype)whereKey:(NSString *)key nearGeoPoint:(PFGeoPoint *)geopoint { + return [self whereKey:key condition:PFQueryKeyNearSphere object:geopoint]; +} + +- (instancetype)whereKey:(NSString *)key nearGeoPoint:(PFGeoPoint *)geopoint withinRadians:(double)maxDistance { + return [[self whereKey:key condition:PFQueryKeyNearSphere object:geopoint] + whereKey:key condition:PFQueryOptionKeyMaxDistance object:@(maxDistance)]; +} + +- (instancetype)whereKey:(NSString *)key nearGeoPoint:(PFGeoPoint *)geopoint withinMiles:(double)maxDistance { + return [self whereKey:key nearGeoPoint:geopoint withinRadians:(maxDistance/EARTH_RADIUS_MILES)]; +} + +- (instancetype)whereKey:(NSString *)key nearGeoPoint:(PFGeoPoint *)geopoint withinKilometers:(double)maxDistance { + return [self whereKey:key nearGeoPoint:geopoint withinRadians:(maxDistance/EARTH_RADIUS_KILOMETERS)]; +} + +- (instancetype)whereKey:(NSString *)key withinGeoBoxFromSouthwest:(PFGeoPoint *)southwest toNortheast:(PFGeoPoint *)northeast { + NSArray *array = @[ southwest, northeast ]; + NSDictionary *dictionary = @{ PFQueryOptionKeyBox : array }; + return [self whereKey:key condition:PFQueryKeyWithin object:dictionary]; +} + +- (instancetype)whereKey:(NSString *)key matchesRegex:(NSString *)regex { + return [self whereKey:key condition:PFQueryKeyRegex object:regex]; +} + +- (instancetype)whereKey:(NSString *)key matchesRegex:(NSString *)regex modifiers:(NSString *)modifiers { + [self checkIfCommandIsRunning]; + NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:2]; + dictionary[PFQueryKeyRegex] = regex; + if ([modifiers length]) { + dictionary[PFQueryOptionKeyRegexOptions] = modifiers; + } + [self.state setEqualityConditionWithObject:dictionary forKey:key]; + return self; +} + +- (instancetype)whereKey:(NSString *)key containsString:(NSString *)substring { + NSString *regex = [PFQueryUtilities regexStringForString:substring]; + return [self whereKey:key matchesRegex:regex]; +} + +- (instancetype)whereKey:(NSString *)key hasPrefix:(NSString *)prefix { + NSString *regex = [NSString stringWithFormat:@"^%@", [PFQueryUtilities regexStringForString:prefix]]; + return [self whereKey:key matchesRegex:regex]; +} + +- (instancetype)whereKey:(NSString *)key hasSuffix:(NSString *)suffix { + NSString *regex = [NSString stringWithFormat:@"%@$", [PFQueryUtilities regexStringForString:suffix]]; + return [self whereKey:key matchesRegex:regex]; +} + +- (instancetype)whereKeyExists:(NSString *)key { + return [self whereKey:key condition:PFQueryKeyExists object:@YES]; +} + +- (instancetype)whereKeyDoesNotExist:(NSString *)key { + return [self whereKey:key condition:PFQueryKeyExists object:@NO]; +} + +- (instancetype)whereKey:(NSString *)key matchesQuery:(PFQuery *)query { + return [self whereKey:key condition:PFQueryKeyInQuery object:query]; +} + +- (instancetype)whereKey:(NSString *)key doesNotMatchQuery:(PFQuery *)query { + return [self whereKey:key condition:PFQueryKeyNotInQuery object:query]; +} + +- (instancetype)whereKey:(NSString *)key matchesKey:(NSString *)otherKey inQuery:(PFQuery *)query { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2]; + dict[PFQueryKeyQuery] = query; + dict[PFQueryKeyKey] = otherKey; + return [self whereKey:key condition:PFQueryKeySelect object:dict]; +} + +- (instancetype)whereKey:(NSString *)key doesNotMatchKey:(NSString *)otherKey inQuery:(PFQuery *)query { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2]; + dict[PFQueryKeyQuery] = query; + dict[PFQueryKeyKey] = otherKey; + return [self whereKey:key condition:PFQueryKeyDontSelect object:dict]; +} + +- (instancetype)whereRelatedToObject:(PFObject *)parent fromKey:(NSString *)key { + [self.state setRelationConditionWithObject:parent forKey:key]; + return self; +} + +- (void)redirectClassNameForKey:(NSString *)key { + [self.state redirectClassNameForKey:key]; +} + +///-------------------------------------- +#pragma mark - Include +///-------------------------------------- + +- (instancetype)includeKey:(NSString *)key { + [self checkIfCommandIsRunning]; + [self.state includeKey:key]; + return self; +} + +///-------------------------------------- +#pragma mark - Select +///-------------------------------------- + +- (instancetype)selectKeys:(NSArray *)keys { + [self checkIfCommandIsRunning]; + [self.state selectKeys:keys]; + return self; +} + +///-------------------------------------- +#pragma mark - NSPredicate helper methods +///-------------------------------------- + ++ (void)assertKeyPathConstant:(NSComparisonPredicate *)predicate { + PFConsistencyAssert(predicate.leftExpression.expressionType == NSKeyPathExpressionType && + predicate.rightExpression.expressionType == NSConstantValueExpressionType, + @"This predicate must have a key path and a constant. %@", predicate); +} + +// Adds the conditions from an NSComparisonPredicate to a PFQuery. +- (void)whereComparisonPredicate:(NSComparisonPredicate *)predicate { + NSExpression *left = predicate.leftExpression; + NSExpression *right = predicate.rightExpression; + + switch (predicate.predicateOperatorType) { + case NSEqualToPredicateOperatorType: { + [[self class] assertKeyPathConstant:predicate]; + [self whereKey:left.keyPath equalTo:(right.constantValue ?: [NSNull null])]; + return; + } + case NSNotEqualToPredicateOperatorType: { + [[self class] assertKeyPathConstant:predicate]; + [self whereKey:left.keyPath notEqualTo:(right.constantValue ?: [NSNull null])]; + return; + } + case NSLessThanPredicateOperatorType: { + [[self class] assertKeyPathConstant:predicate]; + [self whereKey:left.keyPath lessThan:right.constantValue]; + return; + } + case NSLessThanOrEqualToPredicateOperatorType: { + [[self class] assertKeyPathConstant:predicate]; + [self whereKey:left.keyPath lessThanOrEqualTo:right.constantValue]; + return; + } + case NSGreaterThanPredicateOperatorType: { + [[self class] assertKeyPathConstant:predicate]; + [self whereKey:left.keyPath greaterThan:right.constantValue]; + return; + } + case NSGreaterThanOrEqualToPredicateOperatorType: { + [[self class] assertKeyPathConstant:predicate]; + [self whereKey:left.keyPath greaterThanOrEqualTo:right.constantValue]; + return; + } + case NSInPredicateOperatorType: { + if (left.expressionType == NSKeyPathExpressionType && + right.expressionType == NSConstantValueExpressionType) { + if ([right.constantValue isKindOfClass:[PFQuery class]]) { + // Like "value IN subquery + [self whereKey:left.keyPath matchesQuery:right.constantValue]; + } else { + // Like "value IN %@", @{@1, @2, @3, @4} + [self whereKey:left.keyPath containedIn:right.constantValue]; + } + } else if (left.expressionType == NSKeyPathExpressionType && + right.expressionType == NSAggregateExpressionType && + [right.constantValue isKindOfClass:[NSArray class]]) { + // Like "value IN {1, 2, 3, 4}" + NSArray *constants = right.constantValue; + NSMutableArray *values = [NSMutableArray arrayWithCapacity:constants.count]; + for (NSExpression *expression in constants) { + [values addObject:expression.constantValue]; + } + [self whereKey:left.keyPath containedIn:values]; + } else if (right.expressionType == NSEvaluatedObjectExpressionType && + left.expressionType == NSKeyPathExpressionType) { + // Like "value IN SELF" + [self whereKeyExists:left.keyPath]; + } else { + [NSException raise:NSInternalInconsistencyException + format:@"An IN predicate must have a key path and a constant."]; + } + return; + } + case NSCustomSelectorPredicateOperatorType: { + if (predicate.customSelector != NSSelectorFromString(@"notContainedIn:")) { + [NSException raise:NSInternalInconsistencyException + format:@"Predicates with custom selectors are not supported."]; + } + + if (right.expressionType == NSConstantValueExpressionType && + left.expressionType == NSKeyPathExpressionType) { + if ([right.constantValue isKindOfClass:[PFQuery class]]) { + // Like "NOT (value IN subquery)" + [self whereKey:left.keyPath doesNotMatchQuery:right.constantValue]; + } else { + // Like "NOT (value in %@)", @{@1, @2, @3} + [self whereKey:left.keyPath notContainedIn:right.constantValue]; + } + } else if (left.expressionType == NSKeyPathExpressionType && + right.expressionType == NSAggregateExpressionType && + [right.constantValue isKindOfClass:[NSArray class]]) { + // Like "NOT (value IN {1, 2, 3, 4})" + NSArray *constants = right.constantValue; + NSMutableArray *values = [NSMutableArray arrayWithCapacity:constants.count]; + for (NSExpression *expression in constants) { + [values addObject:expression.constantValue]; + } + [self whereKey:left.keyPath notContainedIn:values]; + } else if (right.expressionType == NSEvaluatedObjectExpressionType && + left.expressionType == NSKeyPathExpressionType) { + // Like "NOT (value IN SELF)" + [self whereKeyDoesNotExist:left.keyPath]; + } else { + [NSException raise:NSInternalInconsistencyException + format:@"A NOT IN predicate must have a key path and a constant array."]; + } + return; + } + case NSBeginsWithPredicateOperatorType: { + [[self class] assertKeyPathConstant:predicate]; + [self whereKey:left.keyPath hasPrefix:right.constantValue]; + return; + } + case NSContainsPredicateOperatorType: { + [NSException raise:NSInternalInconsistencyException + format:@"Regex queries are not supported with " + "[PFQuery queryWithClassName:predicate:]. Please try to structure your " + "data so that you can use an equalTo or containedIn query."]; + } + case NSEndsWithPredicateOperatorType: { + [NSException raise:NSInternalInconsistencyException + format:@"Regex queries are not supported with " + "[PFQuery queryWithClassName:predicate:]. Please try to structure your " + "data so that you can use an equalTo or containedIn query."]; + } + case NSMatchesPredicateOperatorType: { + [NSException raise:NSInternalInconsistencyException + format:@"Regex queries are not supported with " + "[PFQuery queryWithClassName:predicate:]. Please try to structure your " + "data so that you can use an equalTo or containedIn query."]; + } + case NSLikePredicateOperatorType: { + [NSException raise:NSInternalInconsistencyException + format:@"LIKE is not supported by PFQuery."]; + } + default: { + [NSException raise:NSInternalInconsistencyException + format:@"This comparison predicate is not supported. (%zd)", predicate.predicateOperatorType]; + } + } +} + +/*! + Creates a PFQuery with the constraints given by predicate. + This method assumes the predicate has already been normalized. + */ ++ (instancetype)queryWithClassName:(NSString *)className normalizedPredicate:(NSPredicate *)predicate { + if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { + PFQuery *query = [self queryWithClassName:className]; + [query whereComparisonPredicate:(NSComparisonPredicate *)predicate]; + return query; + } else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { + NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; + switch (compound.compoundPredicateType) { + case NSAndPredicateType: { + PFQuery *query = nil; + NSMutableArray *subpredicates = [NSMutableArray array]; + // If there's an OR query in here, we'll start with it. + for (NSPredicate *subpredicate in compound.subpredicates) { + if ([subpredicate isKindOfClass:[NSCompoundPredicate class]] && + ((NSCompoundPredicate *)subpredicate).compoundPredicateType == NSOrPredicateType) { + if (query) { + [NSException raise:NSInternalInconsistencyException + format:@"A query had 2 ORs in an AND after normalization. %@", + predicate]; + } + query = [self queryWithClassName:className normalizedPredicate:subpredicate]; + } else { + [subpredicates addObject:subpredicate]; + } + } + // If there was no OR query, then start with an empty query. + if (!query) { + query = [self queryWithClassName:className]; + } + for (NSPredicate *subpredicate in subpredicates) { + if (![subpredicate isKindOfClass:[NSComparisonPredicate class]]) { + // This should never happen. + [NSException raise:NSInternalInconsistencyException + format:@"A predicate had a non-comparison predicate inside an AND " + "after normalization. %@", predicate]; + } + NSComparisonPredicate *comparison = (NSComparisonPredicate *)subpredicate; + [query whereComparisonPredicate:comparison]; + } + return query; + } + case NSOrPredicateType: { + NSMutableArray *subqueries = [NSMutableArray arrayWithCapacity:compound.subpredicates.count]; + if (compound.subpredicates.count > 4) { + [NSException raise:NSInternalInconsistencyException + format:@"This query is too complex. It had an OR with >4 subpredicates " + "after normalization."]; + } + for (NSPredicate *subpredicate in compound.subpredicates) { + [subqueries addObject:[self queryWithClassName:className normalizedPredicate:subpredicate]]; + } + return [self orQueryWithSubqueries:subqueries]; + } + default: { + // This should never happen. + [NSException raise:NSInternalInconsistencyException + format:@"A predicate had a NOT after normalization. %@", predicate]; + return nil; + } + } + } else { + [NSException raise:NSInternalInconsistencyException format:@"Unknown predicate type."]; + return nil; + } +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (void)checkIfCommandIsRunning { + @synchronized (self) { + if (_cancellationTokenSource) { + [NSException raise:NSInternalInconsistencyException + format:@"This query has an outstanding network connection. You have to wait until it's done."]; + } + } +} + +- (void)markAsRunning:(BFCancellationTokenSource *)source { + [self checkIfCommandIsRunning]; + @synchronized (self) { + _cancellationTokenSource = source; + } +} + +///-------------------------------------- +#pragma mark - Constructors +///-------------------------------------- + ++ (instancetype)queryWithClassName:(NSString *)className { + return [[self alloc] initWithClassName:className]; +} + ++ (instancetype)queryWithClassName:(NSString *)className predicate:(NSPredicate *)predicate { + if (!predicate) { + return [self queryWithClassName:className]; + } + + NSPredicate *normalizedPredicate = [PFQueryUtilities predicateByNormalizingPredicate:predicate]; + return [self queryWithClassName:className normalizedPredicate:normalizedPredicate]; +} + ++ (instancetype)orQueryWithSubqueries:(NSArray *)queries { + NSMutableArray *array = [NSMutableArray array]; + NSString *className = nil; + for (id object in queries) { + PFParameterAssert([object isKindOfClass:[PFQuery class]], + @"All elements should be instances of `PFQuery` class."); + + PFQuery *query = (PFQuery *)object; + if (!className) { + className = query.parseClassName; + } else { + PFParameterAssert([query.parseClassName isEqualToString:className], + @"All sub queries of an `or` query should be on the same class."); + } + + [array addObject:query]; + } + PFQuery *query = [self queryWithClassName:className]; + [query.state setEqualityConditionWithObject:array forKey:PFQueryKeyOr]; + return query; +} + +///-------------------------------------- +#pragma mark - Get with objectId +///-------------------------------------- + ++ (PFObject *)getObjectOfClass:(NSString *)objectClass objectId:(NSString *)objectId { + return [self getObjectOfClass:objectClass objectId:objectId error:nil]; +} + ++ (PFObject *)getObjectOfClass:(NSString *)objectClass + objectId:(NSString *)objectId + error:(NSError **)error { + PFQuery *query = [self queryWithClassName:objectClass]; + return [query getObjectWithId:objectId error:error]; +} + +// TODO (hallucinogen): we may want to remove this in 2.0 since we can just use the static counterpart +- (PFObject *)getObjectWithId:(NSString *)objectId { + return [self getObjectWithId:objectId error:nil]; +} + +- (PFObject *)getObjectWithId:(NSString *)objectId error:(NSError **)error { + return [[self getObjectInBackgroundWithId:objectId] waitForResult:error]; +} + +- (BFTask *)getObjectInBackgroundWithId:(NSString *)objectId { + if ([objectId length] == 0) { + return [BFTask taskWithResult:nil]; + } + + PFConsistencyAssert(self.state.cachePolicy != kPFCachePolicyCacheThenNetwork, + @"kPFCachePolicyCacheThenNetwork can only be used with methods that have a callback."); + return [self _getObjectWithIdAsync:objectId cachePolicy:self.state.cachePolicy after:nil]; +} + +- (void)getObjectInBackgroundWithId:(NSString *)objectId block:(PFObjectResultBlock)block { + @synchronized (self) { + if (!self.state.queriesLocalDatastore && self.state.cachePolicy == kPFCachePolicyCacheThenNetwork) { + BFTask *cacheTask = [[self _getObjectWithIdAsync:objectId + cachePolicy:kPFCachePolicyCacheOnly + after:nil] thenCallBackOnMainThreadAsync:block]; + [[self _getObjectWithIdAsync:objectId + cachePolicy:kPFCachePolicyNetworkOnly + after:cacheTask] thenCallBackOnMainThreadAsync:block]; + } else { + [[self getObjectInBackgroundWithId:objectId] thenCallBackOnMainThreadAsync:block]; + } + } +} + +- (void)getObjectInBackgroundWithId:(NSString *)objectId target:(id)target selector:(SEL)selector { + [self getObjectInBackgroundWithId:objectId block:^(PFObject *object, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:object object:error]; + }]; +} + +- (BFTask *)_getObjectWithIdAsync:(NSString *)objectId cachePolicy:(PFCachePolicy)cachePolicy after:(BFTask *)task { + self.limit = 1; + self.skip = 0; + [self.state removeAllConditions]; + [self.state setEqualityConditionWithObject:objectId forKey:@"objectId"]; + + PFQueryState *state = [self _queryStateCopyWithCachePolicy:cachePolicy]; + return [[self _findObjectsAsyncForQueryState:state + after:task] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *objects = task.result; + if (objects.count == 0) { + return [BFTask taskWithError:[PFQueryUtilities objectNotFoundError]]; + } + + return [BFTask taskWithResult:objects.lastObject]; + }]; +} + +///-------------------------------------- +#pragma mark - Get Users (Deprecated) +///-------------------------------------- + ++ (PFUser *)getUserObjectWithId:(NSString *)objectId { + return [self getUserObjectWithId:objectId error:nil]; +} + ++ (PFUser *)getUserObjectWithId:(NSString *)objectId error:(NSError **)error { + PFQuery *query = [PFUser query]; + PFUser *object = (PFUser *)[query getObjectWithId:objectId error:error]; + + return object; +} + ++ (instancetype)queryForUser { + return [PFUser query]; +} + +///-------------------------------------- +#pragma mark - Find Objects +///-------------------------------------- + +- (NSArray *)findObjects { + return [self findObjects:nil]; +} + +- (NSArray *)findObjects:(NSError **)error { + return [[self findObjectsInBackground] waitForResult:error]; +} + +- (BFTask *)findObjectsInBackground { + PFQueryState *state = [self _queryStateCopy]; + + PFConsistencyAssert(state.cachePolicy != kPFCachePolicyCacheThenNetwork, + @"kPFCachePolicyCacheThenNetwork can only be used with methods that have a callback."); + return [self _findObjectsAsyncForQueryState:state after:nil]; +} + +- (void)findObjectsInBackgroundWithBlock:(PFArrayResultBlock)block { + @synchronized (self) { + if (!self.state.queriesLocalDatastore && self.state.cachePolicy == kPFCachePolicyCacheThenNetwork) { + PFQueryState *cacheQueryState = [self _queryStateCopyWithCachePolicy:kPFCachePolicyCacheOnly]; + BFTask *cacheTask = [[self _findObjectsAsyncForQueryState:cacheQueryState + after:nil] thenCallBackOnMainThreadAsync:block]; + + PFQueryState *remoteQueryState = [self _queryStateCopyWithCachePolicy:kPFCachePolicyNetworkOnly]; + [[self _findObjectsAsyncForQueryState:remoteQueryState + after:cacheTask] thenCallBackOnMainThreadAsync:block]; + } else { + [[self findObjectsInBackground] thenCallBackOnMainThreadAsync:block]; + } + } +} + +- (void)findObjectsInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:objects object:error]; + }]; +} + +- (BFTask *)_findObjectsAsyncForQueryState:(PFQueryState *)queryState after:(BFTask *)previous { + BFCancellationTokenSource *cancellationTokenSource = _cancellationTokenSource; + if (!previous) { + cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [self markAsRunning:cancellationTokenSource]; + } + + BFTask *start = (previous ?: [BFTask taskWithResult:nil]); + + [self _validateQueryState]; + @weakify(self); + return [[[start continueWithBlock:^id(BFTask *task) { + @strongify(self); + return [[self class] _getCurrentUserForQueryState:queryState]; + }] continueWithBlock:^id(BFTask *task) { + @strongify(self); + PFUser *user = task.result; + return [[[self class] queryController] findObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationTokenSource.token + user:user]; + }] continueWithBlock:^id(BFTask *task) { + @strongify(self); + @synchronized (self) { + if (_cancellationTokenSource == cancellationTokenSource) { + _cancellationTokenSource = nil; + } + } + return task; + }]; +} + +///-------------------------------------- +#pragma mark - Get Object +///-------------------------------------- + +- (PFObject *)getFirstObject { + return [self getFirstObject:nil]; +} + +- (PFObject *)getFirstObject:(NSError **)error { + return [[self getFirstObjectInBackground] waitForResult:error]; +} + +- (BFTask *)getFirstObjectInBackground { + PFConsistencyAssert(self.state.cachePolicy != kPFCachePolicyCacheThenNetwork, + @"kPFCachePolicyCacheThenNetwork can only be used with methods that have a callback."); + return [self _getFirstObjectAsyncWithCachePolicy:self.state.cachePolicy after:nil]; +} + +- (void)getFirstObjectInBackgroundWithBlock:(PFObjectResultBlock)block { + @synchronized (self) { + if (!self.state.queriesLocalDatastore && self.state.cachePolicy == kPFCachePolicyCacheThenNetwork) { + BFTask *cacheTask = [[self _getFirstObjectAsyncWithCachePolicy:kPFCachePolicyCacheOnly + after:nil] thenCallBackOnMainThreadAsync:block]; + [[self _getFirstObjectAsyncWithCachePolicy:kPFCachePolicyNetworkOnly + after:cacheTask] thenCallBackOnMainThreadAsync:block]; + } else { + [[self getFirstObjectInBackground] thenCallBackOnMainThreadAsync:block]; + } + } +} + +- (void)getFirstObjectInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self getFirstObjectInBackgroundWithBlock:^(PFObject *result, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:result object:error]; + }]; +} + +- (BFTask *)_getFirstObjectAsyncWithCachePolicy:(PFCachePolicy)cachePolicy after:(BFTask *)task { + self.limit = 1; + + PFQueryState *state = [self _queryStateCopyWithCachePolicy:cachePolicy]; + return [[self _findObjectsAsyncForQueryState:state after:task] continueWithSuccessBlock:^id(BFTask *task) { + NSArray *objects = task.result; + if (objects.count == 0) { + return [BFTask taskWithError:[PFQueryUtilities objectNotFoundError]]; + } + + return [BFTask taskWithResult:objects.lastObject]; + }]; +} + +///-------------------------------------- +#pragma mark - Count Objects +///-------------------------------------- + +- (NSInteger)countObjects { + return [self countObjects:nil]; +} + +- (NSInteger)countObjects:(NSError **)error { + NSNumber *count = [[self countObjectsInBackground] waitForResult:error]; + if (!count) { + // TODO: (nlutsenko) It's really weird that we are inconsistent in sync vs async methods. + // Leaving for now since some devs might be relying on this. + return -1; + } + + return [count integerValue]; +} + +- (BFTask *)countObjectsInBackground { + PFConsistencyAssert(self.state.cachePolicy != kPFCachePolicyCacheThenNetwork, + @"kPFCachePolicyCacheThenNetwork can only be used with methods that have a callback."); + return [self _countObjectsAsyncForQueryState:[self _queryStateCopy] after:nil]; +} + +- (void)countObjectsInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self countObjectsInBackgroundWithBlock:^(int number, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(number) object:error]; + }]; +} + +- (void)countObjectsInBackgroundWithBlock:(PFIntegerResultBlock)block { + PFIdResultBlock callback = nil; + if (block) { + callback = ^(id result, NSError *error) { + block([result intValue], error); + }; + } + + @synchronized (self) { + if (!self.state.queriesLocalDatastore && self.state.cachePolicy == kPFCachePolicyCacheThenNetwork) { + PFQueryState *cacheQueryState = [self _queryStateCopyWithCachePolicy:kPFCachePolicyCacheOnly]; + BFTask *cacheTask = [[self _countObjectsAsyncForQueryState:cacheQueryState + after:nil] thenCallBackOnMainThreadAsync:callback]; + + PFQueryState *remoteQueryState = [self _queryStateCopyWithCachePolicy:kPFCachePolicyNetworkOnly]; + [[self _countObjectsAsyncForQueryState:remoteQueryState + after:cacheTask] thenCallBackOnMainThreadAsync:callback]; + } else { + [[self countObjectsInBackground] thenCallBackOnMainThreadAsync:callback]; + } + } +} + +- (BFTask *)_countObjectsAsyncForQueryState:(PFQueryState *)queryState after:(BFTask *)previousTask { + BFCancellationTokenSource *cancellationTokenSource = _cancellationTokenSource; + if (!previousTask) { + cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [self markAsRunning:cancellationTokenSource]; + } + + BFTask *start = (previousTask ?: [BFTask taskWithResult:nil]); + + [self _validateQueryState]; + @weakify(self); + return [[[start continueWithBlock:^id(BFTask *task) { + return [[self class] _getCurrentUserForQueryState:queryState]; + }] continueWithBlock:^id(BFTask *task) { + @strongify(self); + PFUser *user = task.result; + return [[[self class] queryController] countObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationTokenSource.token + user:user]; + }] continueWithBlock:^id(BFTask *task) { + @synchronized(self) { + if (_cancellationTokenSource == cancellationTokenSource) { + _cancellationTokenSource = nil; + } + } + return task; + }]; +} + +///-------------------------------------- +#pragma mark - Cancel +///-------------------------------------- + +- (void)cancel { + @synchronized (self) { + if (_cancellationTokenSource) { + [_cancellationTokenSource cancel]; + _cancellationTokenSource = nil; + } + } +} + +///-------------------------------------- +#pragma mark - NSCopying +///-------------------------------------- + +- (instancetype)copyWithZone:(NSZone *)zone { + return [[[self class] allocWithZone:zone] initWithState:self.state]; +} + +///-------------------------------------- +#pragma mark NSObject +///-------------------------------------- + +- (NSUInteger)hash { + return [self.state hash]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[PFQuery class]]) { + return NO; + } + + return [self.state isEqual:((PFQuery *)object).state]; +} + +///-------------------------------------- +#pragma mark - Caching +///-------------------------------------- + +- (BOOL)hasCachedResult { + return [[[self class] queryController] hasCachedResultForQueryState:self.state + sessionToken:[PFUser currentSessionToken]]; +} + +- (void)clearCachedResult { + [[[self class] queryController] clearCachedResultForQueryState:self.state + sessionToken:[PFUser currentSessionToken]]; +} + ++ (void)clearAllCachedResults { + [[self queryController] clearAllCachedResults]; +} + +///-------------------------------------- +#pragma mark - Check Pinning Status +///-------------------------------------- + +/*! + If `enabled` is YES, raise an exception if OfflineStore is not enabled. If `enabled` is NO, raise + an exception if OfflineStore is enabled. + */ +- (void)_checkPinningEnabled:(BOOL)enabled { + BOOL loaded = [Parse _currentManager].offlineStoreLoaded; + if (enabled) { + PFConsistencyAssert(loaded, @"Method requires Pinning enabled."); + } else { + PFConsistencyAssert(!loaded, @"Method not allowed when Pinning is enabled."); + } +} + +///-------------------------------------- +#pragma mark - Query Source +///-------------------------------------- + +- (instancetype)fromLocalDatastore { + return [self fromPinWithName:nil]; +} + +- (instancetype)fromPin { + return [self fromPinWithName:PFObjectDefaultPin]; +} + +- (instancetype)fromPinWithName:(NSString *)name { + [self _checkPinningEnabled:YES]; + [self checkIfCommandIsRunning]; + + self.state.queriesLocalDatastore = YES; + self.state.localDatastorePinName = [name copy]; + + return self; +} + +- (instancetype)ignoreACLs { + [self _checkPinningEnabled:YES]; + [self checkIfCommandIsRunning]; + + self.state.shouldIgnoreACLs = YES; + + return self; +} + +///-------------------------------------- +#pragma mark - Query State +///-------------------------------------- + +- (PFQueryState *)_queryStateCopy { + return [self.state copy]; +} + +- (PFQueryState *)_queryStateCopyWithCachePolicy:(PFCachePolicy)cachePolicy { + PFMutableQueryState *state = [self.state mutableCopy]; + state.cachePolicy = cachePolicy; + return state; +} + +- (void)_validateQueryState { + PFConsistencyAssert(self.state.queriesLocalDatastore || !self.state.shouldIgnoreACLs, + @"`ignoreACLs` can only be used with Local Datastore queries."); +} + +///-------------------------------------- +#pragma mark - Query Controller +///-------------------------------------- + ++ (PFQueryController *)queryController { + return [Parse _currentManager].coreManager.queryController; +} + +///-------------------------------------- +#pragma mark - User +///-------------------------------------- + ++ (BFTask *)_getCurrentUserForQueryState:(PFQueryState *)state { + if (state.shouldIgnoreACLs) { + return [BFTask taskWithResult:nil]; + } + return [[Parse _currentManager].coreManager.currentUserController getCurrentObjectAsync]; +} + +@end diff --git a/Parse/PFRelation.h b/Parse/PFRelation.h new file mode 100644 index 000000000..7a7b90286 --- /dev/null +++ b/Parse/PFRelation.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +/*! + The `PFRelation` class that is used to access all of the children of a many-to-many relationship. + Each instance of `PFRelation` is associated with a particular parent object and key. + */ +@interface PFRelation : NSObject + +/*! + @abstract The name of the class of the target child objects. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, copy) NSString *targetClass; + +///-------------------------------------- +/// @name Accessing Objects +///-------------------------------------- + +/*! + @abstract Returns a object that can be used to get objects in this relation. + */ +- (PF_NULLABLE PFQuery *)query; + +///-------------------------------------- +/// @name Modifying Relations +///-------------------------------------- + +/*! + @abstract Adds a relation to the passed in object. + + @param object A object to add relation to. + */ +- (void)addObject:(PFObject *)object; + +/*! + @abstract Removes a relation to the passed in object. + + @param object A object to add relation to. + */ +- (void)removeObject:(PFObject *)object; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFRelation.m b/Parse/PFRelation.m new file mode 100644 index 000000000..a6fdb68e2 --- /dev/null +++ b/Parse/PFRelation.m @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRelation.h" +#import "PFRelationPrivate.h" + +#import + +#import "PFAssert.h" +#import "PFFieldOperation.h" +#import "PFInternalUtils.h" +#import "PFMacros.h" +#import "PFMutableRelationState.h" +#import "PFObjectPrivate.h" +#import "PFQueryPrivate.h" + +NSString *const PFRelationKeyClassName = @"className"; +NSString *const PFRelationKeyType = @"__type"; +NSString *const PFRelationKeyObjects = @"objects"; + +@interface PFRelation () { + // + // Use this queue as follows: + // Because state is defined as an atomic property, there's no need to use the queue if you're only reading from + // self.state once during the method. + // + // If you ever need to use self.state more than once, either take a copy at the top of the function, or use a + // dispatch_sync block. + // + // If you are ever changing the state variable, you should use dispatch_sync. + // + dispatch_queue_t _stateAccessQueue; +} + +@property (atomic, copy) PFMutableRelationState *state; + +@end + +@implementation PFRelation + +@dynamic targetClass; + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _stateAccessQueue = dispatch_queue_create("com.parse.relation.state.access", DISPATCH_QUEUE_SERIAL); + _state = [[PFMutableRelationState alloc] init]; + + return self; +} + +- (instancetype)initWithParent:(PFObject *)newParent key:(NSString *)newKey { + self = [self init]; + if (!self) return nil; + + _state.parent = newParent; + _state.key = newKey; + + return self; +} + +- (instancetype)initWithTargetClass:(NSString *)newTargetClass { + self = [self init]; + if (!self) return nil; + + _state.targetClass = newTargetClass; + + return self; +} + +- (instancetype)initFromDictionary:(NSDictionary *)dictionary withDecoder:(PFDecoder *)decoder { + self = [self init]; + if (!self) return nil; + + NSArray *array = dictionary[PFRelationKeyObjects]; + NSMutableSet *known = [[NSMutableSet alloc] initWithCapacity:array.count]; + + // Decode the result + for (id encodedObject in array) { + [known addObject:[decoder decodeObject:encodedObject]]; + } + + _state.targetClass = dictionary[PFRelationKeyClassName]; + [_state.knownObjects setSet:known]; + + return self; +} + ++ (PFRelation *)relationForObject:(PFObject *)parent forKey:(NSString *)key { + return [[PFRelation alloc] initWithParent:parent key:key]; +} + ++ (PFRelation *)relationWithTargetClass:(NSString *)targetClass { + return [[PFRelation alloc] initWithTargetClass:targetClass]; +} + ++ (PFRelation *)relationFromDictionary:(NSDictionary *)dictionary withDecoder:(PFDecoder *)decoder { + return [[PFRelation alloc] initFromDictionary:dictionary withDecoder:decoder]; +} + +- (void)ensureParentIs:(PFObject *)someParent andKeyIs:(NSString *)someKey { + pf_sync_with_throw(_stateAccessQueue, ^{ + __strong PFObject *sparent = self.state.parent; + + if (!sparent) { + sparent = self.state.parent = someParent; + } + + if (!self.state.key) { + self.state.key = someKey; + } + + PFConsistencyAssert(sparent == someParent, + @"Internal error. One PFRelation retrieved from two different PFObjects."); + + PFConsistencyAssert([self.state.key isEqualToString:someKey], + @"Internal error. One PFRelation retrieved from two different keys."); + }); +} + +- (NSString *)description { + PFRelationState *state = [self.state copy]; + + return [NSString stringWithFormat:@"<%@: %p, %p.%@ -> %@>", + [self class], + self, + state.parent, + state.key, + state.targetClass]; +} + +- (PFQuery *)query { + PFRelationState *state = [self.state copy]; + __strong PFObject *sparent = state.parent; + + PFQuery *query = nil; + if (state.targetClass) { + query = [PFQuery queryWithClassName:state.targetClass]; + } else { + query = [PFQuery queryWithClassName:state.parentClassName]; + [query redirectClassNameForKey:state.key]; + } + if (sparent) { + [query whereRelatedToObject:sparent fromKey:state.key]; + } else if (state.parentClassName) { + PFObject *object = [PFObject objectWithoutDataWithClassName:state.parentClassName + objectId:state.parentObjectId]; + [query whereRelatedToObject:object fromKey:state.key]; + } + + return query; +} + +- (NSString *)targetClass { + return self.state.targetClass; +} + +- (void)setTargetClass:(NSString *)targetClass { + dispatch_sync(_stateAccessQueue, ^{ + self.state.targetClass = targetClass; + }); +} + +- (void)addObject:(PFObject *)object { + pf_sync_with_throw(_stateAccessQueue, ^{ + PFRelationState *state = self.state; + + PFRelationOperation *op = [PFRelationOperation addRelationToObjects:@[ object ]]; + [state.parent performOperation:op forKey:state.key]; + + self.state.targetClass = op.targetClass; + [self.state.knownObjects addObject:object]; + }); +} + +- (void)removeObject:(PFObject *)object { + pf_sync_with_throw(_stateAccessQueue, ^{ + PFRelationState *state = self.state; + + PFRelationOperation *op = [PFRelationOperation removeRelationToObjects:@[ object ]]; + [state.parent performOperation:op forKey:state.key]; + + self.state.targetClass = op.targetClass; + [self.state.knownObjects removeObject:object]; + }); +} + +- (NSDictionary *)encodeIntoDictionary { + PFRelationState *state = [self.state copy]; + NSMutableArray *encodedObjects = [NSMutableArray arrayWithCapacity:state.knownObjects.count]; + + for (PFObject *knownObject in state.knownObjects) { + [encodedObjects addObject:[[PFPointerObjectEncoder objectEncoder] encodeObject:knownObject]]; + } + + return @{ + PFRelationKeyType : @"Relation", + PFRelationKeyClassName : state.targetClass, + PFRelationKeyObjects : encodedObjects + }; +} + +/*! + Returns true if and only if this object was ever known to be in the relation. + This is used for offline caching. + */ +- (BOOL)_hasKnownObject:(PFObject *)object { + __block BOOL results = NO; + + dispatch_sync(_stateAccessQueue, ^{ + results = [self.state.knownObjects containsObject:object]; + }); + + return results; +} + +- (void)_addKnownObject:(PFObject *)object { + dispatch_sync(_stateAccessQueue, ^{ + [self.state.knownObjects addObject:object]; + }); +} + +- (void)_removeKnownObject:(PFObject *)object { + dispatch_sync(_stateAccessQueue, ^{ + [self.state.knownObjects removeObject:object]; + }); +} + +@end diff --git a/Parse/PFRole.h b/Parse/PFRole.h new file mode 100644 index 000000000..d188990b8 --- /dev/null +++ b/Parse/PFRole.h @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +/*! + The `PFRole` class represents a Role on the Parse server. + `PFRoles` represent groupings of objects for the purposes of granting permissions + (e.g. specifying a for a ). + Roles are specified by their sets of child users and child roles, + all of which are granted any permissions that the parent role has. + + Roles must have a name (which cannot be changed after creation of the role), and must specify an ACL. + */ +@interface PFRole : PFObject + +///-------------------------------------- +/// @name Creating a New Role +///-------------------------------------- + +/*! + @abstract Constructs a new `PFRole` with the given name. + If no default ACL has been specified, you must provide an ACL for the role. + + @param name The name of the Role to create. + */ +- (instancetype)initWithName:(NSString *)name; + +/*! + @abstract Constructs a new `PFRole` with the given name. + + @param name The name of the Role to create. + @param acl The ACL for this role. Roles must have an ACL. + */ +- (instancetype)initWithName:(NSString *)name acl:(PF_NULLABLE PFACL *)acl; + +/*! + @abstract Constructs a new `PFRole` with the given name. + + @discussion If no default ACL has been specified, you must provide an ACL for the role. + + @param name The name of the Role to create. + */ ++ (instancetype)roleWithName:(NSString *)name; + +/*! + @abstract Constructs a new `PFRole` with the given name. + + @param name The name of the Role to create. + @param acl The ACL for this role. Roles must have an ACL. + */ ++ (instancetype)roleWithName:(NSString *)name acl:(PF_NULLABLE PFACL *)acl; + +///-------------------------------------- +/// @name Role-specific Properties +///-------------------------------------- + +/*! + @abstract Gets or sets the name for a role. + + @discussion This value must be set before the role has been saved to the server, + and cannot be set once the role has been saved. + + @warning A role's name can only contain alphanumeric characters, `_`, `-`, and spaces. + */ +@property (nonatomic, copy) NSString *name; + +/*! + @abstract Gets the for the objects that are direct children of this role. + + @discussion These users are granted any privileges that this role has been granted + (e.g. read or write access through ACLs). You can add or remove users from + the role through this relation. + */ +@property (nonatomic, strong, readonly) PFRelation *users; + +/*! + @abstract Gets the for the `PFRole` objects that are direct children of this role. + + @discussion These roles' users are granted any privileges that this role has been granted + (e.g. read or write access through ACLs). You can add or remove child roles + from this role through this relation. + */ +@property (nonatomic, strong, readonly) PFRelation *roles; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFRole.m b/Parse/PFRole.m new file mode 100644 index 000000000..bc797ca88 --- /dev/null +++ b/Parse/PFRole.m @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFRole.h" + +#import + +#import "PFAssert.h" +#import "PFObject+Subclass.h" +#import "PFObjectPrivate.h" +#import "PFQuery.h" + +@implementation PFRole + +#pragma mark Creating a New Role + +- (instancetype)initWithName:(NSString *)name { + return [self initWithName:name acl:nil]; +} + +- (instancetype)initWithName:(NSString *)name acl:(PFACL *)acl { + self = [super init]; + if (!self) return nil; + + self.name = name; + self.ACL = acl; + + return self; +} + ++ (instancetype)roleWithName:(NSString *)name { + return [[self alloc] initWithName:name]; +} + ++ (instancetype)roleWithName:(NSString *)name acl:(PFACL *)acl { + return [[self alloc] initWithName:name acl:acl]; +} + +///-------------------------------------- +#pragma mark - Role-specific Properties +///-------------------------------------- + +@dynamic name; + +// Dynamic synthesizers would use objectForKey, not relationForKey +- (PFRelation *)roles { + return [self relationForKey:@"roles"]; +} + +- (PFRelation *)users { + return [self relationForKey:@"users"]; +} + +///-------------------------------------- +#pragma mark - PFObject Overrides +///-------------------------------------- + +- (void)setObject:(id)object forKey:(NSString *)key { + if ([@"name" isEqualToString:key]) { + if (self.objectId) { + [NSException raise:NSInternalInconsistencyException + format:@"A role's name can only be set before it has been saved."]; + } + if (![object isKindOfClass:[NSString class]]) { + [NSException raise:NSInvalidArgumentException + format:@"A role's name must be an NSString."]; + } + if ([object rangeOfString:@"^[0-9a-zA-Z_\\- ]+$" options:NSRegularExpressionSearch].location == NSNotFound) { + [NSException raise:NSInvalidArgumentException + format:@"A role's name can only contain alphanumeric characters, _, -, and spaces."]; + } + } + [super setObject:object forKey:key]; +} + +- (BFTask *)saveInBackground { + if (!self.objectId && !self.name) { + [NSException raise:NSInternalInconsistencyException + format:@"New roles must specify a name."]; + } + return [super saveInBackground]; +} + +// Validates a class name. We override this to only allow the role class name. ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert([className isEqualToString:[self parseClassName]], + @"Cannot initialize a PFRole with a custom class name."); +} + ++ (NSString *)parseClassName { + return @"_Role"; +} + +@end diff --git a/Parse/PFSession.h b/Parse/PFSession.h new file mode 100644 index 000000000..f666b607c --- /dev/null +++ b/Parse/PFSession.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#else +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +@class PFSession; + +typedef void(^PFSessionResultBlock)(PFSession *PF_NULLABLE_S session, NSError *PF_NULLABLE_S error); + +/*! + `PFSession` is a local representation of a session. + This class is a subclass of a , + and retains the same functionality as any other subclass of . + */ +@interface PFSession : PFObject + +/*! + @abstract The session token string for this session. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, copy, readonly) NSString *sessionToken; + +/*! + *Asynchronously* fetches a `PFSession` object related to the current user. + + @returns A task that is `completed` with an instance of `PFSession` class or is `faulted` if the operation fails. + */ ++ (BFTask *)getCurrentSessionInBackground; + +/*! + *Asynchronously* fetches a `PFSession` object related to the current user. + + @param block The block to execute when the operation completes. + It should have the following argument signature: `^(PFSession *session, NSError *error)`. + */ ++ (void)getCurrentSessionInBackgroundWithBlock:(PF_NULLABLE PFSessionResultBlock)block; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFSession.m b/Parse/PFSession.m new file mode 100644 index 000000000..a4af8e814 --- /dev/null +++ b/Parse/PFSession.m @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSession.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFCoreManager.h" +#import "PFCurrentUserController.h" +#import "PFObject+Subclass.h" +#import "PFObjectPrivate.h" +#import "PFSessionController.h" +#import "PFUserPrivate.h" +#import "Parse_Private.h" + +static BOOL _PFSessionIsWritablePropertyForKey(NSString *key) { + static NSSet *protectedKeys; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + protectedKeys = [NSSet setWithObjects: + @"sessionToken", + @"restricted", + @"createdWith", + @"installationId", + @"user", + @"expiresAt", nil]; + }); + return ![protectedKeys containsObject:key]; +} + +@implementation PFSession + +@dynamic sessionToken; + +///-------------------------------------- +#pragma mark - PFSubclassing +///-------------------------------------- + ++ (NSString *)parseClassName { + return @"_Session"; +} + +- (BOOL)needsDefaultACL { + return NO; +} + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert([className isEqualToString:[PFSession parseClassName]], + @"Cannot initialize a PFSession with a custom class name."); +} + +#pragma mark Get Current Session + ++ (BFTask *)getCurrentSessionInBackground { + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller getCurrentUserSessionTokenAsync] continueWithBlock:^id(BFTask *task) { + NSString *sessionToken = task.result; + return [[self sessionController] getCurrentSessionAsyncWithSessionToken:sessionToken]; + }]; +} + ++ (void)getCurrentSessionInBackgroundWithBlock:(PFSessionResultBlock)block { + [[self getCurrentSessionInBackground] thenCallBackOnMainThreadAsync:block]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)setObject:(id)object forKey:(NSString *)key { + PFParameterAssert(_PFSessionIsWritablePropertyForKey(key), + @"Can't change the '%@' field of a PFSession.", key); + [super setObject:object forKey:key]; +} + +- (void)removeObjectForKey:(NSString *)key { + PFParameterAssert(_PFSessionIsWritablePropertyForKey(key), + @"Can't remove the '%@' field of a PFSession.", key); + [super removeObjectForKey:key]; +} + +- (void)removeObjectsInArray:(NSArray *)objects forKey:(NSString *)key { + PFParameterAssert(_PFSessionIsWritablePropertyForKey(key), + @"Can't remove any object from '%@' field of a PFSession.", key); + [super removeObjectsInArray:objects forKey:key]; +} + +///-------------------------------------- +#pragma mark - Session Controller +///-------------------------------------- + ++ (PFSessionController *)sessionController { + return [Parse _currentManager].coreManager.sessionController; +} + +@end diff --git a/Parse/PFSubclassing.h b/Parse/PFSubclassing.h new file mode 100644 index 000000000..2b2bee016 --- /dev/null +++ b/Parse/PFSubclassing.h @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +@class PFQuery; + +PF_ASSUME_NONNULL_BEGIN + +/*! + If a subclass of conforms to `PFSubclassing` and calls , + Parse framework will be able to use that class as the native class for a Parse cloud object. + + Classes conforming to this protocol should subclass and + include `PFObject+Subclass.h` in their implementation file. + This ensures the methods in the Subclass category of are exposed in its subclasses only. + */ +@protocol PFSubclassing + +/*! + @abstract Constructs an object of the most specific class known to implement . + + @discussion This method takes care to help subclasses be subclassed themselves. + For example, `[PFUser object]` returns a by default but will return an + object of a registered subclass instead if one is known. + A default implementation is provided by which should always be sufficient. + + @returns Returns the object that is instantiated. + */ ++ (instancetype)object; + +/*! + @abstract Creates a reference to an existing PFObject for use in creating associations between PFObjects. + + @discussion Calling <[PFObject isDataAvailable]> on this object will return `NO` + until <[PFObject fetchIfNeeded]> has been called. No network request will be made. + A default implementation is provided by PFObject which should always be sufficient. + + @param objectId The object id for the referenced object. + + @returns A new without data. + */ ++ (instancetype)objectWithoutDataWithObjectId:(PF_NULLABLE NSString *)objectId; + +/*! + @abstract The name of the class as seen in the REST API. + */ ++ (NSString *)parseClassName; + +/*! + @abstract Create a query which returns objects of this type. + + @discussion A default implementation is provided by which should always be sufficient. + */ ++ (PF_NULLABLE PFQuery *)query; + +/*! + @abstract Returns a query for objects of this type with a given predicate. + + @discussion A default implementation is provided by which should always be sufficient. + + @param predicate The predicate to create conditions from. + + @returns An instance of . + + @see [PFQuery queryWithClassName:predicate:] + */ ++ (PF_NULLABLE PFQuery *)queryWithPredicate:(PF_NULLABLE NSPredicate *)predicate; + +/*! + @abstract Lets Parse know this class should be used to instantiate all objects with class type . + + @warning This method must be called before <[Parse setApplicationId:clientKey:]> + */ ++ (void)registerSubclass; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFUser.h b/Parse/PFUser.h new file mode 100644 index 000000000..8fae8528f --- /dev/null +++ b/Parse/PFUser.h @@ -0,0 +1,459 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#import +#endif + +PF_ASSUME_NONNULL_BEGIN + +typedef void(^PFUserSessionUpgradeResultBlock)(NSError *PF_NULLABLE_S error); +typedef void(^PFUserLogoutResultBlock)(NSError *PF_NULLABLE_S error); + + +@class PFQuery; + +/*! + The `PFUser` class is a local representation of a user persisted to the Parse Data. + This class is a subclass of a , and retains the same functionality of a , + but also extends it with various user specific methods, like authentication, signing up, and validation uniqueness. + + Many APIs responsible for linking a `PFUser` with Facebook or Twitter have been deprecated in favor of dedicated + utilities for each social network. See , and for more information. + */ + +@interface PFUser : PFObject + +///-------------------------------------- +/// @name Accessing the Current User +///-------------------------------------- + +/*! + @abstract Gets the currently logged in user from disk and returns an instance of it. + + @returns Returns a `PFUser` that is the currently logged in user. If there is none, returns `nil`. + */ ++ (PF_NULLABLE instancetype)currentUser; + +/*! + @abstract The session token for the `PFUser`. + + @discussion This is set by the server upon successful authentication. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, copy, readonly) NSString *sessionToken; + +/*! + @abstract Whether the `PFUser` was just created from a request. + + @discussion This is only set after a Facebook or Twitter login. + */ +@property (assign, readonly) BOOL isNew; + +/*! + @abstract Whether the user is an authenticated object for the device. + + @discussion An authenticated `PFUser` is one that is obtained via a or method. + An authenticated object is required in order to save (with altered values) or delete it. + + @returns Returns whether the user is authenticated. + */ +- (BOOL)isAuthenticated; + +///-------------------------------------- +/// @name Creating a New User +///-------------------------------------- + +/*! + @abstract Creates a new `PFUser` object. + + @returns Returns a new `PFUser` object. + */ ++ (PFUser *)user; + +/*! + @abstract Enables automatic creation of anonymous users. + + @discussion After calling this method, will always have a value. + The user will only be created on the server once the user has been saved, + or once an object with a relation to that user or an ACL that refers to the user has been saved. + + @warning <[PFObject saveEventually]> will not work on if an item being saved has a relation + to an automatic user that has never been saved. + */ ++ (void)enableAutomaticUser; + +/*! + @abstract The username for the `PFUser`. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSString *username; + +/**! + @abstract The password for the `PFUser`. + + @discussion This will not be filled in from the server with the password. + It is only meant to be set. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSString *password; + +/*! + @abstract The email for the `PFUser`. + */ +@property (PF_NULLABLE_PROPERTY nonatomic, strong) NSString *email; + +/*! + @abstract Signs up the user *synchronously*. + + @discussion This will also enforce that the username isn't already taken. + + @warning Make sure that password and username are set before calling this method. + + @returns Returns `YES` if the sign up was successful, otherwise `NO`. + */ +- (BOOL)signUp; + +/*! + @abstract Signs up the user *synchronously*. + + @discussion This will also enforce that the username isn't already taken. + + @warning Make sure that password and username are set before calling this method. + + @param error Error object to set on error. + + @returns Returns whether the sign up was successful. + */ +- (BOOL)signUp:(NSError **)error; + +/*! + @abstract Signs up the user *asynchronously*. + + @discussion This will also enforce that the username isn't already taken. + + @warning Make sure that password and username are set before calling this method. + + @returns The task, that encapsulates the work being done. + */ +- (BFTask *)signUpInBackground; + +/*! + @abstract Signs up the user *asynchronously*. + + @discussion This will also enforce that the username isn't already taken. + + @warning Make sure that password and username are set before calling this method. + + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ +- (void)signUpInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract Signs up the user *asynchronously*. + + @discussion This will also enforce that the username isn't already taken. + + @warning Make sure that password and username are set before calling this method. + + @param target Target object for the selector. + @param selector The selector that will be called when the asynchrounous request is complete. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ +- (void)signUpInBackgroundWithTarget:(PF_NULLABLE_S id)target selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Logging In +///-------------------------------------- + +/*! + @abstract Makes a *synchronous* request to login a user with specified credentials. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to will use the latest logged in user. + + @param username The username of the user. + @param password The password of the user. + + @returns Returns an instance of the `PFUser` on success. + If login failed for either wrong password or wrong username, returns `nil`. + */ ++ (PF_NULLABLE instancetype)logInWithUsername:(NSString *)username + password:(NSString *)password; + +/*! + @abstract Makes a *synchronous* request to login a user with specified credentials. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to will use the latest logged in user. + + @param username The username of the user. + @param password The password of the user. + @param error The error object to set on error. + + @returns Returns an instance of the `PFUser` on success. + If login failed for either wrong password or wrong username, returns `nil`. + */ ++ (PF_NULLABLE instancetype)logInWithUsername:(NSString *)username + password:(NSString *)password + error:(NSError **)error; + +/*! + @abstract Makes an *asynchronous* request to login a user with specified credentials. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to will use the latest logged in user. + + @param username The username of the user. + @param password The password of the user. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password; + +/*! + @abstract Makes an *asynchronous* request to login a user with specified credentials. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to will use the latest logged in user. + + @param username The username of the user. + @param password The password of the user. + @param target Target object for the selector. + @param selector The selector that will be called when the asynchrounous request is complete. + It should have the following signature: `(void)callbackWithUser:(PFUser *)user error:(NSError *)error`. + */ ++ (void)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +/*! + @abstract Makes an *asynchronous* request to log in a user with specified credentials. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to will use the latest logged in user. + + @param username The username of the user. + @param password The password of the user. + @param block The block to execute. + It should have the following argument signature: `^(PFUser *user, NSError *error)`. + */ ++ (void)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + block:(PF_NULLABLE PFUserResultBlock)block; + +///-------------------------------------- +/// @name Becoming a User +///-------------------------------------- + +/*! + @abstract Makes a *synchronous* request to become a user with the given session token. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to will use the latest logged in user. + + @param sessionToken The session token for the user. + + @returns Returns an instance of the `PFUser` on success. + If becoming a user fails due to incorrect token, it returns `nil`. + */ ++ (PF_NULLABLE instancetype)become:(NSString *)sessionToken; + +/*! + @abstract Makes a *synchronous* request to become a user with the given session token. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This will also cache the user locally so that calls to will use the latest logged in user. + + @param sessionToken The session token for the user. + @param error The error object to set on error. + + @returns Returns an instance of the `PFUser` on success. + If becoming a user fails due to incorrect token, it returns `nil`. + */ ++ (PF_NULLABLE instancetype)become:(NSString *)sessionToken error:(NSError **)error; + +/*! + @abstract Makes an *asynchronous* request to become a user with the given session token. + + @discussion Returns an instance of the successfully logged in `PFUser`. + This also caches the user locally so that calls to will use the latest logged in user. + + @param sessionToken The session token for the user. + + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)becomeInBackground:(NSString *)sessionToken; + +/*! + @abstract Makes an *asynchronous* request to become a user with the given session token. + + @discussion Returns an instance of the successfully logged in `PFUser`. This also caches the user locally + so that calls to will use the latest logged in user. + + @param sessionToken The session token for the user. + @param block The block to execute. + The block should have the following argument signature: `^(PFUser *user, NSError *error)`. + */ ++ (void)becomeInBackground:(NSString *)sessionToken block:(PF_NULLABLE PFUserResultBlock)block; + +/*! + @abstract Makes an *asynchronous* request to become a user with the given session token. + + @discussion Returns an instance of the successfully logged in `PFUser`. This also caches the user locally + so that calls to will use the latest logged in user. + + @param sessionToken The session token for the user. + @param target Target object for the selector. + @param selector The selector that will be called when the asynchrounous request is complete. + It should have the following signature: `(void)callbackWithUser:(PFUser *)user error:(NSError *)error`. + */ ++ (void)becomeInBackground:(NSString *)sessionToken + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +///-------------------------------------- +/// @name Revocable Session +///-------------------------------------- + +/*! + @abstract Enables revocable sessions and migrates the currentUser session token to use revocable session if needed. + + @discussion This method is required if you want to use APIs + and you application's 'Require Revocable Session' setting is turned off on `http://parse.com` app settings. + After returned `BFTask` completes - class and APIs will be available for use. + + @returns An instance of `BFTask` that is completed when + revocable sessions are enabled and currentUser token is migrated. + */ ++ (BFTask *)enableRevocableSessionInBackground; + +/*! + @abstract Enables revocable sessions and upgrades the currentUser session token to use revocable session if needed. + + @discussion This method is required if you want to use APIs + and legacy sessions are enabled in your application settings on `http://parse.com/`. + After returned `BFTask` completes - class and APIs will be available for use. + + @param block Block that will be called when revocable sessions are enabled and currentUser token is migrated. + */ ++ (void)enableRevocableSessionInBackgroundWithBlock:(PF_NULLABLE PFUserSessionUpgradeResultBlock)block; + +///-------------------------------------- +/// @name Logging Out +///-------------------------------------- + +/*! + @abstract *Synchronously* logs out the currently logged in user on disk. + */ ++ (void)logOut; + +/*! + @abstract *Asynchronously* logs out the currently logged in user. + + @discussion This will also remove the session from disk, log out of linked services + and all future calls to will return `nil`. This is preferrable to using , + unless your code is already running from a background thread. + + @returns An instance of `BFTask`, that is resolved with `nil` result when logging out completes. + */ ++ (BFTask *)logOutInBackground; + +/*! + @abstract *Asynchronously* logs out the currently logged in user. + + @discussion This will also remove the session from disk, log out of linked services + and all future calls to will return `nil`. This is preferrable to using , + unless your code is already running from a background thread. + + @param block A block that will be called when logging out completes or fails. + */ ++ (void)logOutInBackgroundWithBlock:(PF_NULLABLE PFUserLogoutResultBlock)block; + +///-------------------------------------- +/// @name Requesting a Password Reset +///-------------------------------------- + +/*! + @abstract *Synchronously* Send a password reset request for a specified email. + + @discussion If a user account exists with that email, an email will be sent to that address + with instructions on how to reset their password. + + @param email Email of the account to send a reset password request. + + @returns Returns `YES` if the reset email request is successful. `NO` - if no account was found for the email address. + */ ++ (BOOL)requestPasswordResetForEmail:(NSString *)email; + +/*! + @abstract *Synchronously* send a password reset request for a specified email and sets an error object. + + @discussion If a user account exists with that email, an email will be sent to that address + with instructions on how to reset their password. + + @param email Email of the account to send a reset password request. + @param error Error object to set on error. + @returns Returns `YES` if the reset email request is successful. `NO` - if no account was found for the email address. + */ ++ (BOOL)requestPasswordResetForEmail:(NSString *)email + error:(NSError **)error; + +/*! + @abstract Send a password reset request asynchronously for a specified email and sets an + error object. If a user account exists with that email, an email will be sent to + that address with instructions on how to reset their password. + @param email Email of the account to send a reset password request. + @returns The task, that encapsulates the work being done. + */ ++ (BFTask *)requestPasswordResetForEmailInBackground:(NSString *)email; + +/*! + @abstract Send a password reset request *asynchronously* for a specified email. + + @discussion If a user account exists with that email, an email will be sent to that address + with instructions on how to reset their password. + + @param email Email of the account to send a reset password request. + @param block The block to execute. + It should have the following argument signature: `^(BOOL succeeded, NSError *error)`. + */ ++ (void)requestPasswordResetForEmailInBackground:(NSString *)email + block:(PF_NULLABLE PFBooleanResultBlock)block; + +/*! + @abstract Send a password reset request *asynchronously* for a specified email and sets an error object. + + @discussion If a user account exists with that email, an email will be sent to that address + with instructions on how to reset their password. + + @param email Email of the account to send a reset password request. + @param target Target object for the selector. + @param selector The selector that will be called when the asynchronous request is complete. + It should have the following signature: `(void)callbackWithResult:(NSNumber *)result error:(NSError *)error`. + `error` will be `nil` on success and set if there was an error. + `[result boolValue]` will tell you whether the call succeeded or not. + */ ++ (void)requestPasswordResetForEmailInBackground:(NSString *)email + target:(PF_NULLABLE_S id)target + selector:(PF_NULLABLE_S SEL)selector; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/PFUser.m b/Parse/PFUser.m new file mode 100644 index 000000000..7d88ecaa4 --- /dev/null +++ b/Parse/PFUser.m @@ -0,0 +1,1225 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUser.h" +#import "PFUserPrivate.h" + +#import +#import + +#import "BFTask+Private.h" +#import "PFACLPrivate.h" +#import "PFAnonymousAuthenticationProvider.h" +#import "PFAnonymousUtils_Private.h" +#import "PFAssert.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFCoreManager.h" +#import "PFCurrentUserController.h" +#import "PFDecoder.h" +#import "PFErrorUtilities.h" +#import "PFFileManager.h" +#import "PFKeychainStore.h" +#import "PFMultiProcessFileLockController.h" +#import "PFMutableUserState.h" +#import "PFObject+Subclass.h" +#import "PFObjectConstants.h" +#import "PFObjectFilePersistenceController.h" +#import "PFObjectPrivate.h" +#import "PFOfflineStore.h" +#import "PFOperationSet.h" +#import "PFQueryPrivate.h" +#import "PFRESTUserCommand.h" +#import "PFSessionUtilities.h" +#import "PFTaskQueue.h" +#import "PFUserAuthenticationController.h" +#import "PFUserConstants.h" +#import "PFUserController.h" +#import "PFUserFileCodingLogic.h" +#import "Parse_Private.h" + +NSString *const PFUserCurrentUserFileName = @"currentUser"; +NSString *const PFUserCurrentUserPinName = @"_currentUser"; +NSString *const PFUserCurrentUserKeychainItemName = @"currentUser"; + +static BOOL _PFUserIsWritablePropertyForKey(NSString *key) { + return ![PFUserSessionTokenRESTKey isEqualToString:key]; +} + +static BOOL _PFUserIsRemovablePropertyForKey(NSString *key) { + return _PFUserIsWritablePropertyForKey(key) && ![PFUserUsernameRESTKey isEqualToString:key]; +} + +@interface PFUser () + +@property (nonatomic, copy) PFUserState *_state; + +@end + +@implementation PFUser (Private) + +static BOOL revocableSessionEnabled_; + +- (void)setDefaultValues { + [super setDefaultValues]; + self.isCurrentUser = NO; +} + +- (BOOL)needsDefaultACL { + return NO; +} + +///-------------------------------------- +#pragma mark - Current User +///-------------------------------------- + +// Returns the session token for the current user. ++ (NSString *)currentSessionToken { + return [[self _getCurrentUserSessionTokenAsync] waitForResult:nil withMainThreadWarning:NO]; +} + ++ (BFTask *)_getCurrentUserSessionTokenAsync { + return [[self currentUserController] getCurrentUserSessionTokenAsync]; +} + +///-------------------------------------- +#pragma mark - PFObject +///-------------------------------------- + +// Check security on delete +- (void)checkDeleteParams { + if (![self isAuthenticated]) { + [NSException raise:NSInternalInconsistencyException + format:@"User cannot be deleted unless they have been authenticated via logIn or signUp", nil]; + } + + [super checkDeleteParams]; +} + +- (NSString *)displayClassName { + return @"PFUser"; +} + +// Validates a class name. We override this to only allow the user class name. ++ (void)_assertValidInstanceClassName:(NSString *)className { + PFParameterAssert([className isEqualToString:[PFUser parseClassName]], + @"Cannot initialize a PFUser with a custom class name."); +} + +// Checks the properties on the object before saving. +- (void)_checkSaveParametersWithCurrentUser:(PFUser *)currentUser { + @synchronized ([self lock]) { + if (!self.objectId && !self.isLazy) { + [NSException raise:NSInternalInconsistencyException + format:@"User cannot be saved unless they are already signed up. Call signUp first.", nil]; + } + + if (![self _isAuthenticatedWithCurrentUser:currentUser] + && ![self.objectId isEqualToString:currentUser.objectId]) { + [NSException raise:NSInternalInconsistencyException + format:@"User cannot be saved unless they have been authenticated via logIn or signUp", nil]; + } + } +} + +// Checks the properties on the object before signUp. +- (void)checkSignUpParams { + @synchronized ([self lock]) { + if (self.username == nil) { + [NSException raise:NSInternalInconsistencyException format:@"Cannot sign up without a username."]; + } + + if (self.password == nil) { + [NSException raise:NSInternalInconsistencyException format:@"Cannot sign up without a password."]; + } + + if (![self isDirty:NO] || self.objectId) { + [NSException raise:NSInternalInconsistencyException format:@"Cannot sign up an existing user."]; + } + } +} + +- (NSMutableDictionary *)_convertToDictionaryForSaving:(PFOperationSet *)changes + withObjectEncoder:(PFEncoder *)encoder { + @synchronized ([self lock]) { + NSMutableDictionary *serialized = [super _convertToDictionaryForSaving:changes withObjectEncoder:encoder]; + if ([self.authData count] > 0) { + serialized[PFUserAuthDataRESTKey] = [self.authData copy]; + } + return serialized; + } +} + +- (BFTask *)handleSaveResultAsync:(NSDictionary *)result { + return [[super handleSaveResultAsync:result] continueWithSuccessBlock:^id(BFTask *saveTask) { + if (self.isCurrentUser) { + [self cleanUpAuthData]; + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller saveCurrentObjectAsync:self] continueWithBlock:^id(BFTask *task) { + return saveTask.result; + }]; + } + return saveTask; + }]; +} + +///-------------------------------------- +#pragma mark - Sign Up +///-------------------------------------- + +- (PFRESTCommand *)_currentSignUpCommandForChanges:(PFOperationSet *)changes { + @synchronized ([self lock]) { + NSDictionary *parameters = [self _convertToDictionaryForSaving:changes + withObjectEncoder:[PFPointerObjectEncoder objectEncoder]]; + return [PFRESTUserCommand signUpUserCommandWithParameters:parameters + revocableSession:[[self class] _isRevocableSessionEnabled] + sessionToken:self.sessionToken]; + } +} + +///-------------------------------------- +#pragma mark - Service Login +///-------------------------------------- + +// Constructs the command for user_signup_or_login. This is used for Facebook, Twitter, and other linking services. +- (PFRESTCommand *)_currentServiceLoginCommandForChanges:(PFOperationSet *)changes { + @synchronized ([self lock]) { + NSDictionary *parameters = [self _convertToDictionaryForSaving:changes + withObjectEncoder:[PFPointerObjectEncoder objectEncoder]]; + return [PFRESTUserCommand serviceLoginUserCommandWithParameters:parameters + revocableSession:[[self class] _isRevocableSessionEnabled] + sessionToken:self.sessionToken]; + } +} + +- (BFTask *)_handleServiceLoginCommandResult:(PFCommandResult *)result { + return [BFTask taskFromExecutor:[BFExecutor defaultExecutor] withBlock:^id{ + NSDictionary *resultDictionary = result.result; + return [[self handleSaveResultAsync:resultDictionary] continueWithBlock:^id(BFTask *task) { + BOOL new = (result.httpResponse.statusCode == 201); // 201 means Created + @synchronized (self.lock) { + if (self._state.isNew != new) { + PFMutableUserState *state = [self._state mutableCopy]; + state.isNew = new; + self._state = state; + } + if (resultDictionary) { + self.isLazy = NO; + + // Serialize the object to disk so we can later access it via currentUser + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller saveCurrentObjectAsync:self] continueAsyncWithBlock:^id(BFTask *task) { + [self.saveDelegate invoke:self error:nil]; + return self; + }]; + } + return [BFTask taskWithResult:self]; + } + }]; + }]; +} + +// Override the save result handling with custom user functionality +- (BFTask *)handleSignUpResultAsync:(BFTask *)task { + @synchronized ([self lock]) { + PFCommandResult *commandResult = task.result; + NSDictionary *result = commandResult.result; + BFTask *signUpTask = task; + + // Bail-out early, but still make sure that super class handled the result + if (task.error || task.cancelled || task.exception) { + return [[super handleSaveResultAsync:nil] continueWithBlock:^id(BFTask *task) { + return signUpTask; + }]; + } + __block BOOL saveResult = NO; + return [[[super handleSaveResultAsync:result] continueWithBlock:^id(BFTask *task) { + saveResult = [task.result boolValue]; + if (saveResult) { + @synchronized (self.lock) { + // Save the session information + PFMutableUserState *state = [self._state mutableCopy]; + state.sessionToken = result[PFUserSessionTokenRESTKey]; + state.isNew = YES; + self._state = state; + self.isLazy = NO; + } + } + return signUpTask; + }] continueWithBlock:^id(BFTask *task) { + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller saveCurrentObjectAsync:self] continueWithResult:@(saveResult)]; + }]; + } +} + +- (void)cleanUpAuthData { + @synchronized ([self lock]) { + for (NSString *key in [self.authData copy]) { + id linkData = [self.authData objectForKey:key]; + if (!linkData || linkData == [NSNull null]) { + [self.authData removeObjectForKey:key]; + [self.linkedServiceNames removeObject:key]; + + [[[self class] authenticationController] restoreAuthenticationWithAuthData:nil + withProviderForAuthType:key]; + } + } + } +} + +/*! + Copies special PFUser fields from another user. + */ +- (PFObject *)mergeFromObject:(PFUser *)other { + @synchronized ([self lock]) { + [super mergeFromObject:other]; + + if (self == other) { + // If they point to the same instance, then don't merge. + return self; + } + + PFMutableUserState *state = [self._state mutableCopy]; + state.sessionToken = other.sessionToken; + state.isNew = other._state.isNew; + self._state = state; + + [self.authData removeAllObjects]; + [self.authData addEntriesFromDictionary:other.authData]; + + [self.linkedServiceNames removeAllObjects]; + [self.linkedServiceNames unionSet:other.linkedServiceNames]; + + return self; + } +} + +/* + Merges custom fields from JSON associated with a PFUser: + { + "session_token": string, + "is_new": boolean, + "auth_data": { + "facebook": { + "id": string, + "access_token": string, + "expiration_date": string (represents date) + } + } + } + */ +- (void)_mergeFromServerWithResult:(NSDictionary *)result decoder:(PFDecoder *)decoder completeData:(BOOL)completeData { + @synchronized ([self lock]) { + // save the session token + + PFMutableUserState *state = [self._state mutableCopy]; + + NSString *newSessionToken = result[PFUserSessionTokenRESTKey]; + if (newSessionToken) { + // Save the session token + state.sessionToken = newSessionToken; + } + + self._state = state; + + // Merge the linked service metadata + NSDictionary *newAuthData = [decoder decodeObject:result[PFUserAuthDataRESTKey]]; + if (newAuthData) { + [self.authData removeAllObjects]; + [self.linkedServiceNames removeAllObjects]; + [newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id linkData, BOOL *stop) { + if (linkData != [NSNull null]) { + [self.authData setObject:linkData forKey:key]; + [self.linkedServiceNames addObject:key]; + [self synchronizeAuthDataWithAuthType:key]; + } else { + [self.authData removeObjectForKey:key]; + [self.linkedServiceNames removeObject:key]; + [self synchronizeAuthDataWithAuthType:key]; + } + }]; + } + + // Strip authData and sessionToken from the data, as those keys are saved in a custom way + NSMutableDictionary *serverData = [result mutableCopy]; + [serverData removeObjectForKey:PFUserSessionTokenRESTKey]; + [serverData removeObjectForKey:PFUserAuthDataRESTKey]; + + // The public fields are handled by the regular mergeFromServer + [super _mergeFromServerWithResult:serverData decoder:decoder completeData:completeData]; + } +} + +- (void)synchronizeAuthDataWithAuthType:(NSString *)authType { + @synchronized ([self lock]) { + if (!self.isCurrentUser) { + return; + } + + NSDictionary *data = self.authData[authType]; + BOOL authRestored = [[[self class] authenticationController] restoreAuthenticationWithAuthData:data + withProviderForAuthType:authType]; + if (!authRestored) { + [self _unlinkWithAuthTypeInBackground:authType]; + } + } +} + +- (void)synchronizeAllAuthData { + @synchronized ([self lock]) { + // Ensures that all auth providers have auth data (e.g. access tokens, etc.) that matches this user. + if (self.authData) { + [self.authData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [self synchronizeAuthDataWithAuthType:key]; + }]; + } + } +} + ++ (BFTask *)_logInWithAuthTypeInBackground:(NSString *)authType authData:(NSDictionary *)authData { + // Handle claiming of user. + PFUser *currentUser = [PFUser currentUser]; + if (currentUser && [PFAnonymousUtils isLinkedWithUser:currentUser]) { + if ([currentUser isLazy]) { + PFUser *user = currentUser; + BFTask *resolveLaziness = nil; + NSDictionary *oldAnonymousData = nil; + @synchronized ([user lock]) { + oldAnonymousData = user.authData[[PFAnonymousAuthenticationProvider authType]]; + + // Replace any anonymity with the new linked authData + [user stripAnonymity]; + + [user.authData setObject:authData forKey:authType]; + [user.linkedServiceNames addObject:authType]; + + resolveLaziness = [user resolveLazinessAsync:[BFTask taskWithResult:nil]]; + } + + return [resolveLaziness continueAsyncWithBlock:^id(BFTask *task) { + if (task.isCancelled || task.exception || task.error) { + [user.authData removeObjectForKey:authType]; + [user.linkedServiceNames removeObject:authType]; + [user restoreAnonymity:oldAnonymousData]; + return task; + } + return task.result; + }]; + } else { + return [[currentUser _linkWithAuthTypeInBackground:authType + authData:authData] continueAsyncWithBlock:^id(BFTask *task) { + NSError *error = task.error; + if (error) { + if (error.code == kPFErrorAccountAlreadyLinked) { + // An account that's linked to the given authData already exists, + // so log in instead of trying to claim. + return [[self userController] logInCurrentUserAsyncWithAuthType:authType + authData:authData + revocableSession:[self _isRevocableSessionEnabled]]; + } else { + return task; + } + } + + return [BFTask taskWithResult:currentUser]; + }]; + } + } + return [[self userController] logInCurrentUserAsyncWithAuthType:authType + authData:authData + revocableSession:[self _isRevocableSessionEnabled]]; +} + +- (BFTask *)resolveLazinessAsync:(BFTask *)toAwait { + @synchronized ([self lock]) { + if (!self.isLazy) { + return [BFTask taskWithResult:self]; + } + if (self.linkedServiceNames.count == 0) { + // If there are no linked services, treat this like a sign-up. + return [[self signUpAsync:toAwait] continueAsyncWithSuccessBlock:^id(BFTask *task) { + self.isLazy = NO; + return self; + }]; + } + + // Otherwise, treat this as a SignUpOrLogIn + PFRESTCommand *command = [self _currentServiceLoginCommandForChanges:[self unsavedChanges]]; + [self startSave]; + + return [[toAwait continueAsyncWithBlock:^id(BFTask *task) { + return [[Parse _currentManager].commandRunner runCommandAsync:command withOptions:0]; + }] continueAsyncWithBlock:^id(BFTask *task) { + PFCommandResult *result = task.result; + + if (task.error || task.cancelled) { + // If there was an error, we want to roll forward the save changes, but return the original task. + return [[self _handleServiceLoginCommandResult:result] continueAsyncWithBlock:^id(BFTask *unused) { + // Return the original task, instead of the new one (in order to have a proper error) + return task; + }]; + } + + if ([result.httpResponse statusCode] == 201) { + return [self _handleServiceLoginCommandResult:result]; + } else { + // Otherwise, treat this as a fresh login, and switch the current user to the new user. + PFUser *newUser = [[self class] _objectFromDictionary:result.result + defaultClassName:[self parseClassName] + completeData:YES]; + @synchronized ([newUser lock]) { + [newUser startSave]; + return [newUser _handleServiceLoginCommandResult:result]; + } + } + }]; + } +} + +- (BFTask *)_linkWithAuthTypeInBackground:(NSString *)authType authData:(NSDictionary *)newAuthData { + @weakify(self); + return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueWithBlock:^id(BFTask *task) { + @strongify(self); + + NSDictionary *oldAnonymousData = nil; + + @synchronized (self.lock) { + self.authData[authType] = newAuthData; + [self.linkedServiceNames addObject:authType]; + + oldAnonymousData = self.authData[[PFAnonymousAuthenticationProvider authType]]; + [self stripAnonymity]; + + dirty = YES; + } + + return [[self saveAsync:nil] continueAsyncWithBlock:^id(BFTask *task) { + if (task.result) { + [self synchronizeAuthDataWithAuthType:authType]; + } else { + @synchronized (self.lock) { + [self.authData removeObjectForKey:authType]; + [self.linkedServiceNames removeObject:authType]; + [self restoreAnonymity:oldAnonymousData]; + } + } + return task; + }]; + }]; + }]; +} + +- (BFTask *)_logOutAsyncWithAuthType:(NSString *)authType { + return [[[self class] authenticationController] deauthenticateAsyncWithProviderForAuthType:authType]; +} + +- (BFTask *)_unlinkWithAuthTypeInBackground:(NSString *)authType { + BFTask *save = nil; + @synchronized ([self lock]) { + if (!self.authData[authType]) { + save = [BFTask taskWithResult:@YES]; + } else { + self.authData[authType] = [NSNull null]; + dirty = YES; + save = [self saveInBackground]; + } + } + return save; +} + ++ (instancetype)logInLazyUserWithAuthType:(NSString *)authType authData:(NSDictionary *)authData { + PFUser *user = [PFUser user]; + @synchronized ([user lock]) { + [user setIsCurrentUser:YES]; + user.isLazy = YES; + [user.authData setObject:authData forKey:authType]; + [user.linkedServiceNames addObject:authType]; + } + return user; +} + +- (BFTask *)signUpAsync:(BFTask *)toAwait { + PFUser *currentUser = [PFUser currentUser]; + NSString *token = currentUser.sessionToken; + @synchronized ([self lock]) { + if (self.objectId) { + // For anonymous users, there may be an objectId. Setting the userName + // will have removed the anonymous link and set the value in the authData + // object to [NSNull null], so we can just treat it like a save operation. + if (self.authData[[PFAnonymousAuthenticationProvider authType]] == [NSNull null]) { + return [self saveAsync:toAwait]; + } + + // Otherwise, return an error + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorUsernameTaken + message:@"Cannot sign up a user that has already signed up."]; + return [BFTask taskWithError:error]; + } + + // If the operationSetQueue is has operation sets in it, then a save or signUp is in progress. + // If there is a signUp or save already in progress, don't allow another one to start. + if ([self _hasOutstandingOperations]) { + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorUsernameTaken + message:@"Cannot sign up a user that is already signing up."]; + return [BFTask taskWithError:error]; + } + + return [BFTask taskFromExecutor:[BFExecutor immediateExecutor] withBlock:^id{ + [self checkSignUpParams]; + if (currentUser && [PFAnonymousUtils isLinkedWithUser:currentUser]) { + // self doesn't have any outstanding saves, so we can safely merge its operations + // into the current user. + + PFConsistencyAssert(!isCurrentUser, @"Attempt to merge currentUser with itself."); + + [self checkForChangesToMutableContainers]; + @synchronized ([currentUser lock]) { + NSString *oldUsername = [currentUser.username copy]; + NSString *oldPassword = [currentUser.password copy]; + NSArray *oldAnonymousData = currentUser.authData[[PFAnonymousAuthenticationProvider authType]]; + + [currentUser checkForChangesToMutableContainers]; + + // Move the changes to this object over to the currentUser object. + PFOperationSet *selfOperations = operationSetQueue[0]; + [operationSetQueue removeAllObjects]; + [operationSetQueue addObject:[[PFOperationSet alloc] init]]; + for (NSString *key in selfOperations) { + [currentUser setObject:[selfOperations objectForKey:key] forKey:key]; + } + + currentUser->dirty = YES; + currentUser.password = self.password; + currentUser.username = self.username; + + [self rebuildEstimatedData]; + [currentUser rebuildEstimatedData]; + + return [[[[currentUser saveInBackground] continueWithBlock:^id(BFTask *task) { + if (task.error || task.cancelled || task.exception) { + @synchronized ([currentUser lock]) { + if (oldUsername) { + currentUser.username = oldUsername; + } + currentUser.password = oldPassword; + [currentUser restoreAnonymity:oldAnonymousData]; + } + + @synchronized(self.lock) { + [operationSetQueue replaceObjectAtIndex:0 withObject:selfOperations]; + [self rebuildEstimatedData]; + } + } + return task; + }] continueWithSuccessBlock:^id(BFTask *task) { + if ([Parse _currentManager].offlineStoreLoaded) { + return [[Parse _currentManager].offlineStore deleteDataForObjectAsync:currentUser]; + } + return nil; + }] continueWithSuccessBlock:^id(BFTask *task) { + [self mergeFromObject:currentUser]; + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller saveCurrentObjectAsync:self] continueWithResult:@YES]; + }]; + } + } + // Use a nil session token for objects saved during a signup. + BFTask *saveChildren = [self _saveChildrenInBackgroundWithCurrentUser:currentUser sessionToken:token]; + PFOperationSet *changes = [self unsavedChanges]; + [self startSave]; + + return [[[toAwait continueWithBlock:^id(BFTask *task) { + return saveChildren; + }] continueWithSuccessBlock:^id(BFTask *task) { + // We need to construct the signup command lazily, because saving the children + // may change the way the object itself is serialized. + PFRESTCommand *command = [self _currentSignUpCommandForChanges:changes]; + return [[Parse _currentManager].commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed]; + }] continueWithBlock:^id(BFTask *task) { + return [self handleSignUpResultAsync:task]; + }]; + }]; + } +} + +- (void)stripAnonymity { + @synchronized ([self lock]) { + if ([PFAnonymousUtils isLinkedWithUser:self]) { + NSString *authType = [PFAnonymousAuthenticationProvider authType]; + + [self.linkedServiceNames removeObject:authType]; + + if (self.objectId) { + self.authData[authType] = [NSNull null]; + } else { + [self.authData removeObjectForKey:authType]; + } + dirty = YES; + } + } +} + +- (void)restoreAnonymity:(id)anonymousData { + @synchronized ([self lock]) { + if (anonymousData && anonymousData != [NSNull null]) { + NSString *authType = [PFAnonymousAuthenticationProvider authType]; + [self.linkedServiceNames addObject:authType]; + self.authData[authType] = anonymousData; + } + } +} + +///-------------------------------------- +#pragma mark - Saving +///-------------------------------------- + +- (PFRESTCommand *)_constructSaveCommandForChanges:(PFOperationSet *)changes + sessionToken:(NSString *)token + objectEncoder:(PFEncoder *)encoder { + // If we are curent user - use the latest available session token, as it might have been changed since + // this command was enqueued. + if ([self isCurrentUser]) { + token = self.sessionToken; + } + return [super _constructSaveCommandForChanges:changes + sessionToken:token + objectEncoder:encoder]; +} + +///-------------------------------------- +#pragma mark - REST operations +///-------------------------------------- + +- (void)mergeFromRESTDictionary:(NSDictionary *)object withDecoder:(PFDecoder *)decoder { + @synchronized ([self lock]) { + NSMutableDictionary *restDictionary = [object mutableCopy]; + + PFMutableUserState *state = [self._state mutableCopy]; + if (object[PFUserSessionTokenRESTKey] != nil) { + state.sessionToken = object[PFUserSessionTokenRESTKey]; + [restDictionary removeObjectForKey:PFUserSessionTokenRESTKey]; + } + + if (object[PFUserAuthDataRESTKey] != nil) { + NSDictionary *newAuthData = object[PFUserAuthDataRESTKey]; + [newAuthData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + self.authData[key] = obj; + if (obj != nil) { + [self.linkedServiceNames addObject:key]; + } + [self synchronizeAuthDataWithAuthType:key]; + }]; + + [restDictionary removeObjectForKey:PFUserAuthDataRESTKey]; + } + + self._state = state; + + [super mergeFromRESTDictionary:restDictionary withDecoder:decoder]; + } +} + +- (NSDictionary *)RESTDictionaryWithObjectEncoder:(PFEncoder *)objectEncoder + operationSetUUIDs:(NSArray **)operationSetUUIDs + state:(PFObjectState *)state + operationSetQueue:(NSArray *)queue { + @synchronized (self.lock) { + NSMutableArray *cleanQueue = [queue mutableCopy]; + [queue enumerateObjectsUsingBlock:^(PFOperationSet *operationSet, NSUInteger idx, BOOL *stop) { + // Remove operations for `password` field, to not let it persist to LDS. + if (operationSet[PFUserPasswordRESTKey]) { + operationSet = [operationSet copy]; + [operationSet removeObjectForKey:PFUserPasswordRESTKey]; + + cleanQueue[idx] = operationSet; + } + }]; + return [super RESTDictionaryWithObjectEncoder:objectEncoder + operationSetUUIDs:operationSetUUIDs + state:state + operationSetQueue:cleanQueue]; + } +} + +///-------------------------------------- +#pragma mark - Revocable Session +///-------------------------------------- + ++ (dispatch_queue_t)_revocableSessionSynchronizationQueue { + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.parse.user.revocableSession", DISPATCH_QUEUE_CONCURRENT); + }); + return queue; +} + ++ (BOOL)_isRevocableSessionEnabled { + __block BOOL value = NO; + dispatch_sync([self _revocableSessionSynchronizationQueue], ^{ + value = revocableSessionEnabled_; + }); + return value; +} + ++ (void)_setRevocableSessionEnabled:(BOOL)enabled { + dispatch_barrier_sync([self _revocableSessionSynchronizationQueue], ^{ + revocableSessionEnabled_ = enabled; + }); +} + ++ (BFTask *)_upgradeToRevocableSessionInBackground { + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller getCurrentUserAsyncWithOptions:0] continueWithSuccessBlock:^id(BFTask *task) { + PFUser *currentUser = task.result; + NSString *sessionToken = currentUser.sessionToken; + + // Bail-out early if session token is already revocable. + if ([PFSessionUtilities isSessionTokenRevocable:sessionToken]) { + return [BFTask taskWithResult:currentUser]; + } + return [currentUser _upgradeToRevocableSessionInBackground]; + }]; +} + +- (BFTask *)_upgradeToRevocableSessionInBackground { + @weakify(self); + return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + @strongify(self); + + NSString *token = nil; + @synchronized(self.lock) { + token = self.sessionToken; + } + + // Check session token here as well, to make sure we didn't upgrade the token in between. + if ([PFSessionUtilities isSessionTokenRevocable:token]) { + return [BFTask taskWithResult:self]; + } + + PFRESTCommand *command = [PFRESTUserCommand upgradeToRevocableSessionCommandWithSessionToken:token]; + return [[[Parse _currentManager].commandRunner runCommandAsync:command + withOptions:0] continueWithSuccessBlock:^id(BFTask *task) { + NSDictionary *dictionary = [task.result result]; + PFSession *session = [PFSession _objectFromDictionary:dictionary + defaultClassName:[PFSession parseClassName] + completeData:YES]; + @synchronized(self.lock) { + PFMutableUserState *state = [self._state mutableCopy]; + state.sessionToken = session.sessionToken; + self._state = state; + } + PFCurrentUserController *controller = [[self class] currentUserController]; + return [controller saveCurrentObjectAsync:self]; + }]; + }]; + }]; +} + +///-------------------------------------- +#pragma mark - Data Source +///-------------------------------------- + ++ (PFObjectFileCodingLogic *)objectFileCodingLogic { + return [PFUserFileCodingLogic codingLogic]; +} + ++ (PFUserAuthenticationController *)authenticationController { + return [Parse _currentManager].coreManager.userAuthenticationController; +} + ++ (PFUserController *)userController { + return [Parse _currentManager].coreManager.userController; +} + +@end + +@implementation PFUser + +@dynamic _state; + +// PFUser: +@dynamic username; +@dynamic email; +@dynamic password; + +// PFUser (Private): +@dynamic authData; +@dynamic linkedServiceNames; +@dynamic isLazy; + ++ (NSString *)parseClassName { + return @"_User"; +} + ++ (instancetype)currentUser { + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller getCurrentObjectAsync] waitForResult:nil withMainThreadWarning:NO]; +} + +- (BOOL)isCurrentUser { + @synchronized (self.lock) { + return isCurrentUser; + } +} + +- (void)setIsCurrentUser:(BOOL)aBool { + @synchronized (self.lock) { + isCurrentUser = aBool; + } +} + +///-------------------------------------- +#pragma mark - Log In +///-------------------------------------- + ++ (instancetype)logInWithUsername:(NSString *)username password:(NSString *)password { + return [self logInWithUsername:username password:password error:nil]; +} + ++ (instancetype)logInWithUsername:(NSString *)username password:(NSString *)password error:(NSError **)error { + return [[self logInWithUsernameInBackground:username password:password] waitForResult:error]; +} + ++ (BFTask *)logInWithUsernameInBackground:(NSString *)username password:(NSString *)password { + return [[self userController] logInCurrentUserAsyncWithUsername:username + password:password + revocableSession:[self _isRevocableSessionEnabled]]; +} + ++ (void)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + block:(PFUserResultBlock)block { + [[self logInWithUsernameInBackground:username password:password] thenCallBackOnMainThreadAsync:block]; +} + ++ (void)logInWithUsernameInBackground:(NSString *)username + password:(NSString *)password + target:(id)target + selector:(SEL)selector { + [self logInWithUsernameInBackground:username password:password block:^(PFUser *user, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:user object:error]; + }]; +} + +///-------------------------------------- +#pragma mark - Become +///-------------------------------------- + ++ (instancetype)become:(NSString *)sessionToken { + return [self become:sessionToken error:nil]; +} + ++ (instancetype)become:(NSString *)sessionToken error:(NSError **)error { + return [[self becomeInBackground:sessionToken] waitForResult:error]; +} + ++ (BFTask *)becomeInBackground:(NSString *)sessionToken { + PFParameterAssert(sessionToken, @"Session Token must be provided for login."); + return [[self userController] logInCurrentUserAsyncWithSessionToken:sessionToken]; +} + ++ (void)becomeInBackground:(NSString *)sessionToken block:(PFUserResultBlock)block { + [[self becomeInBackground:sessionToken] thenCallBackOnMainThreadAsync:block]; +} + ++ (void)becomeInBackground:(NSString *)sessionToken target:(id)target selector:(SEL)selector { + [self becomeInBackground:sessionToken block:^(PFUser *user, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:user object:error]; + }]; +} + +///-------------------------------------- +#pragma mark - Revocable SEssions +///-------------------------------------- + ++ (BFTask *)enableRevocableSessionInBackground { + if ([self _isRevocableSessionEnabled]) { + return [BFTask taskWithResult:nil]; + } + [self _setRevocableSessionEnabled:YES]; + return [self _upgradeToRevocableSessionInBackground]; +} + ++ (void)enableRevocableSessionInBackgroundWithBlock:(PFUserSessionUpgradeResultBlock)block { + [[self enableRevocableSessionInBackground] continueWithBlock:^id(BFTask *task) { + block(task.error); + return nil; + }]; +} + +///-------------------------------------- +#pragma mark - Request Password Reset +///-------------------------------------- + ++ (BOOL)requestPasswordResetForEmail:(NSString *)email { + return [self requestPasswordResetForEmail:email error:nil]; +} + ++ (BOOL)requestPasswordResetForEmail:(NSString *)email error:(NSError **)error { + return [[[self requestPasswordResetForEmailInBackground:email] waitForResult:error] boolValue]; +} + ++ (BFTask *)requestPasswordResetForEmailInBackground:(NSString *)email { + PFParameterAssert(email, @"Email should be provided to request password reset."); + return [[[self userController] requestPasswordResetAsyncForEmail:email] continueWithSuccessResult:@YES]; +} + ++ (void)requestPasswordResetForEmailInBackground:(NSString *)email block:(PFBooleanResultBlock)block { + [[self requestPasswordResetForEmailInBackground:email] thenCallBackOnMainThreadWithBoolValueAsync:block]; +} + ++ (void)requestPasswordResetForEmailInBackground:(NSString *)email target:(id)target selector:(SEL)selector { + [self requestPasswordResetForEmailInBackground:email block:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +///-------------------------------------- +#pragma mark - Logging out +///-------------------------------------- + ++ (void)logOut { + [[self logOutInBackground] waitForResult:nil withMainThreadWarning:NO]; +} + ++ (BFTask *)logOutInBackground { + PFCurrentUserController *controller = [[self class] currentUserController]; + return [controller logOutCurrentUserAsync]; +} + ++ (void)logOutInBackgroundWithBlock:(PFUserLogoutResultBlock)block { + [[self logOutInBackground] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id(BFTask *task) { + block(task.error); + return nil; + }]; +} + +- (BFTask *)_logOutAsync { + //TODO: (nlutsenko) Maybe add this to `taskQueue`? + + NSString *token = nil; + NSMutableArray *tasks = [NSMutableArray array]; + @synchronized(self.lock) { + [self.authData enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + BFTask *task = [self _logOutAsyncWithAuthType:key]; + [tasks addObject:task]; + }]; + + self.isCurrentUser = NO; + + token = [self.sessionToken copy]; + + PFMutableUserState *state = [self._state mutableCopy]; + state.sessionToken = nil; + self._state = state; + } + + BFTask *task = [BFTask taskForCompletionOfAllTasks:tasks]; + + if ([PFSessionUtilities isSessionTokenRevocable:token]) { + return [task continueWithExecutor:[BFExecutor defaultExecutor] withBlock:^id(BFTask *task) { + return [[[self class] userController] logOutUserAsyncWithSessionToken:token]; + }]; + } + return task; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)setObject:(id)object forKey:(NSString *)key { + PFParameterAssert(_PFUserIsWritablePropertyForKey(key), + @"Can't remove the '%@' field of a PFUser.", key); + if ([key isEqualToString:PFUserUsernameRESTKey]) { + [self stripAnonymity]; + } + [super setObject:object forKey:key]; +} + +- (void)removeObjectForKey:(NSString *)key { + PFParameterAssert(_PFUserIsRemovablePropertyForKey(key), + @"Can't remove the '%@' field of a PFUser.", key); + [super removeObjectForKey:key]; +} + +- (NSMutableDictionary *)authData { + @synchronized ([self lock]) { + if (!authData) { + authData = [[NSMutableDictionary alloc] init]; + } + } + return authData; +} + +- (NSMutableSet *)linkedServiceNames { + @synchronized ([self lock]) { + if (!linkedServiceNames) { + linkedServiceNames = [[NSMutableSet alloc] init]; + } + } + return linkedServiceNames; +} + ++ (instancetype)user { + return (PFUser *)[PFUser object]; +} + +- (BFTask *)saveAsync:(BFTask *)toAwait { + if (!toAwait) { + toAwait = [BFTask taskWithResult:nil]; + } + + // This breaks a rare deadlock scenario where on one thread, user.lock is acquired before taskQueue.lock sometimes, + // but not always. Using continueAsyncWithBlock unlocks from the taskQueue, and solves the proplem. + return [toAwait continueAsyncWithBlock:^id(BFTask *task) { + @synchronized ([self lock]) { + if (self.isLazy) { + return [[self resolveLazinessAsync:toAwait] continueAsyncWithSuccessBlock:^id(BFTask *task) { + return @(!!task.result); + }]; + } + } + + return [super saveAsync:toAwait]; + }]; +} + +- (BFTask *)fetchAsync:(BFTask *)toAwait { + if ([self isLazy]) { + return [BFTask taskWithResult:@YES]; + } + + return [[super fetchAsync:toAwait] continueAsyncWithSuccessBlock:^id(BFTask *fetchAsyncTask) { + if ([self isCurrentUser]) { + [self cleanUpAuthData]; + PFCurrentUserController *controller = [[self class] currentUserController]; + return [[controller saveCurrentObjectAsync:self] continueAsyncWithBlock:^id(BFTask *task) { + return fetchAsyncTask.result; + }]; + } + return fetchAsyncTask.result; + }]; +} + +- (void)fetch:(NSError **)error { + if (self.isLazy) { + return; + } + [super fetch:error]; +} + +- (void)fetchInBackgroundWithBlock:(PFObjectResultBlock)block { + if (self.isLazy) { + if (block) { + block(self, nil); + return; + } + } + [super fetchInBackgroundWithBlock:^(PFObject *result, NSError *error) { + if (block) { + block(result, error); + } + }]; +} + +- (BOOL)signUp { + return [self signUp:nil]; +} + +- (BOOL)signUp:(NSError **)error { + return [[[self signUpInBackground] waitForResult:error] boolValue]; +} + +- (BFTask *)signUpInBackground { + return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) { + return [self signUpAsync:toAwait]; + }]; +} + +- (void)signUpInBackgroundWithTarget:(id)target selector:(SEL)selector { + [self signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + [PFInternalUtils safePerformSelector:selector withTarget:target object:@(succeeded) object:error]; + }]; +} + +- (BOOL)isAuthenticated { + PFUser *currentUser = [PFUser currentUser]; + return [self _isAuthenticatedWithCurrentUser:currentUser]; +} + +- (BOOL)_isAuthenticatedWithCurrentUser:(PFUser *)currentUser { + @synchronized ([self lock]) { + BOOL authenticated = self.isLazy || self.sessionToken; + if (!authenticated && currentUser != nil) { + authenticated = [self.objectId isEqualToString:currentUser.objectId]; + } else { + authenticated = self.isCurrentUser; + } + return authenticated; + } +} + +- (BOOL)isNew { + return self._state.isNew; +} + +- (NSString *)sessionToken { + return self._state.sessionToken; +} + +- (void)signUpInBackgroundWithBlock:(PFBooleanResultBlock)block { + @synchronized ([self lock]) { + if (self.objectId) { + // For anonymous users, there may be an objectId. Setting the userName + // will have removed the anonymous link and set the value in the authData + // object to [NSNull null], so we can just treat it like a save operation. + if (authData[[PFAnonymousAuthenticationProvider authType]] == [NSNull null]) { + [self saveInBackgroundWithBlock:block]; + return; + } + } + [self checkSignUpParams]; + [[self signUpInBackground] thenCallBackOnMainThreadWithBoolValueAsync:block]; + } +} + ++ (void)enableAutomaticUser { + [Parse _currentManager].coreManager.currentUserController.automaticUsersEnabled = YES; +} + +///-------------------------------------- +#pragma mark - PFObjectPrivateSubclass +///-------------------------------------- + +#pragma mark State + ++ (PFObjectState *)_newObjectStateWithParseClassName:(NSString *)className + objectId:(NSString *)objectId + isComplete:(BOOL)complete { + return [PFUserState stateWithParseClassName:className objectId:objectId isComplete:complete]; +} + +#pragma mark Validation + +- (BFTask *)_validateSaveEventuallyAsync { + if ([self isDirtyForKey:PFUserPasswordRESTKey]) { + NSError *error = [PFErrorUtilities errorWithCode:kPFErrorOperationForbidden + message:@"Unable to saveEventually a PFUser with dirty password."]; + return [BFTask taskWithError:error]; + } + return [BFTask taskWithResult:nil]; +} + +@end diff --git a/Parse/Parse.h b/Parse/Parse.h new file mode 100644 index 000000000..2e632fcf2 --- /dev/null +++ b/Parse/Parse.h @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#else + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#endif + +PF_ASSUME_NONNULL_BEGIN + +/*! + The `Parse` class contains static functions that handle global configuration for the Parse framework. + */ +@interface Parse : NSObject + +///-------------------------------------- +/// @name Connecting to Parse +///-------------------------------------- + +/*! + @abstract Sets the applicationId and clientKey of your application. + + @param applicationId The application id of your Parse application. + @param clientKey The client key of your Parse application. + */ ++ (void)setApplicationId:(NSString *)applicationId clientKey:(NSString *)clientKey; + +/*! + @abstract The current application id that was used to configure Parse framework. + */ ++ (NSString *)getApplicationId; + +/*! + @abstract The current client key that was used to configure Parse framework. + */ ++ (NSString *)getClientKey; + +///-------------------------------------- +/// @name Enabling Local Datastore +///-------------------------------------- + +/*! + @abstract Enable pinning in your application. This must be called before your application can use + pinning. The recommended way is to call this method before `setApplicationId:clientKey:`. + */ ++ (void)enableLocalDatastore; + +/*! + @abstract Flag that indicates whether Local Datastore is enabled. + + @returns `YES` if Local Datastore is enabled, otherwise `NO`. + */ ++ (BOOL)isLocalDatastoreEnabled; + +///-------------------------------------- +/// @name Enabling Extensions Data Sharing +///-------------------------------------- + +/*! + @abstract Enables data sharing with an application group identifier. + + @discussion After enabling - Local Datastore, `currentUser`, `currentInstallation` and all eventually commands + are going to be available to every application/extension in a group that have the same Parse applicationId. + + @warning This method is required to be called before . + + @param groupIdentifier Application Group Identifier to share data with. + */ ++ (void)enableDataSharingWithApplicationGroupIdentifier:(NSString *)groupIdentifier PF_EXTENSION_UNAVAILABLE("Use `enableDataSharingWithApplicationGroupIdentifier:containingApplication:`."); + +/*! + @abstract Enables data sharing with an application group identifier. + + @discussion After enabling - Local Datastore, `currentUser`, `currentInstallation` and all eventually commands + are going to be available to every application/extension in a group that have the same Parse applicationId. + + @warning This method is required to be called before . + This method can only be used by application extensions. + + @param groupIdentifier Application Group Identifier to share data with. + @param bundleIdentifier Bundle identifier of the containing application. + */ ++ (void)enableDataSharingWithApplicationGroupIdentifier:(NSString *)groupIdentifier + containingApplication:(NSString *)bundleIdentifier; + +/*! + @abstract Application Group Identifier for Data Sharing + + @returns `NSString` value if data sharing is enabled, otherwise `nil`. + */ ++ (NSString *)applicationGroupIdentifierForDataSharing; + +/*! + @abstract Containing application bundle identifier. + + @returns `NSString` value if data sharing is enabled, otherwise `nil`. + */ ++ (NSString *)containingApplicationBundleIdentifierForDataSharing; + +#if PARSE_IOS_ONLY + +///-------------------------------------- +/// @name Configuring UI Settings +///-------------------------------------- + +/*! + @abstract Set whether to show offline messages when using a Parse view or view controller related classes. + + @param enabled Whether a `UIAlertView` should be shown when the device is offline + and network access is required from a view or view controller. + + @deprecated This method has no effect. + */ ++ (void)offlineMessagesEnabled:(BOOL)enabled PARSE_DEPRECATED("This method is deprecated and has no effect."); + +/*! + @abstract Set whether to show an error message when using a Parse view or view controller related classes + and a Parse error was generated via a query. + + @param enabled Whether a `UIAlertView` should be shown when an error occurs. + + @deprecated This method has no effect. + */ ++ (void)errorMessagesEnabled:(BOOL)enabled PARSE_DEPRECATED("This method is deprecated and has no effect."); + +#endif + +///-------------------------------------- +/// @name Logging +///-------------------------------------- + +/*! + @abstract Sets the level of logging to display. + + @discussion By default: + - If running inside an app that was downloaded from iOS App Store - it is set to + - All other cases - it is set to + + @param logLevel Log level to set. + @see PFLogLevel + */ ++ (void)setLogLevel:(PFLogLevel)logLevel; + +/*! + @abstract Log level that will be displayed. + + @discussion By default: + - If running inside an app that was downloaded from iOS App Store - it is set to + - All other cases - it is set to + + @returns A value. + @see PFLogLevel + */ ++ (PFLogLevel)logLevel; + +@end + +PF_ASSUME_NONNULL_END diff --git a/Parse/Parse.m b/Parse/Parse.m new file mode 100644 index 000000000..bd2b535af --- /dev/null +++ b/Parse/Parse.m @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "BFTask+Private.h" +#import "Parse.h" +#import "ParseInternal.h" +#import "ParseManager.h" +#import "PFEventuallyPin.h" +#import "PFObject+Subclass.h" +#import "PFOfflineStore.h" +#import "PFPin.h" +#import "PFPinningEventuallyQueue.h" +#import "PFUserPrivate.h" +#import "PFLogger.h" +#import "PFSession.h" +#import "PFFileManager.h" +#import "PFApplication.h" +#import "PFKeychainStore.h" +#import "PFLogging.h" +#import "PFInstallationPrivate.h" +#import "PFObjectSubclassingController.h" + +#if PARSE_IOS_ONLY +#import "PFProduct+Private.h" +#endif + +#import "PFCategoryLoader.h" + +@implementation Parse + +static ParseManager *currentParseManager_; + +static BOOL shouldEnableLocalDatastore_; + +static NSString *applicationGroupIdentifier_; +static NSString *containingApplicationBundleIdentifier_; + ++ (void)initialize { + if (self == [Parse class]) { + // Load all private categories, that we have... + // Without this call - private categories - will require `-ObjC` in linker flags. + // By explicitly calling empty method - we can avoid that. + [PFCategoryLoader loadPrivateCategories]; + } +} + +///-------------------------------------- +#pragma mark - Connect +///-------------------------------------- + ++ (void)setApplicationId:(NSString *)applicationId clientKey:(NSString *)clientKey { + // TODO: (nlutsenko) Add assert and unit test here that checks applicationId, clientKey not being nil. + + // Setup new manager first, so it's 100% ready whenever someone sends a request for anything. + ParseManager *manager = [[ParseManager alloc] initWithApplicationId:applicationId clientKey:clientKey]; + [manager configureWithApplicationGroupIdentifier:applicationGroupIdentifier_ + containingApplicationIdentifier:containingApplicationBundleIdentifier_ + enabledLocalDataStore:shouldEnableLocalDatastore_]; + currentParseManager_ = manager; + + shouldEnableLocalDatastore_ = NO; + + PFObjectSubclassingController *subclassingController = [PFObjectSubclassingController defaultController]; + // Register built-in subclasses of PFObject so they get used. + // We're forced to register subclasses directly this way, in order to prevent a deadlock. + // If we ever switch to bundle scanning, this code can go away. + [subclassingController registerSubclass:[PFUser class]]; + [subclassingController registerSubclass:[PFInstallation class]]; + [subclassingController registerSubclass:[PFSession class]]; + [subclassingController registerSubclass:[PFRole class]]; + [subclassingController registerSubclass:[PFPin class]]; + [subclassingController registerSubclass:[PFEventuallyPin class]]; +#if TARGET_OS_IPHONE + [subclassingController registerSubclass:[PFProduct class]]; +#endif + + [currentParseManager_ preloadDiskObjectsToMemoryAsync]; + + [[self parseModulesCollection] parseDidInitializeWithApplicationId:applicationId clientKey:clientKey]; +} + ++ (NSString *)getApplicationId { + PFConsistencyAssert(currentParseManager_, + @"You have to call setApplicationId:clientKey: on Parse to configure Parse."); + return currentParseManager_.applicationId; +} + ++ (NSString *)getClientKey { + PFConsistencyAssert(currentParseManager_, + @"You have to call setApplicationId:clientKey: on Parse to configure Parse."); + return currentParseManager_.clientKey; +} + +///-------------------------------------- +#pragma mark - Extensions Data Sharing +///-------------------------------------- + ++ (void)enableDataSharingWithApplicationGroupIdentifier:(NSString *)groupIdentifier { + PFConsistencyAssert(!currentParseManager_, + @"'enableDataSharingWithApplicationGroupIdentifier:' must be called before 'setApplicationId:clientKey'"); + PFParameterAssert([groupIdentifier length], @"'groupIdentifier' should not be nil."); + PFConsistencyAssert(![PFApplication currentApplication].extensionEnvironment, @"This method cannot be used in application extensions."); + PFConsistencyAssert([PFFileManager isApplicationGroupContainerReachableForGroupIdentifier:groupIdentifier], + @"ApplicationGroupContainer is unreachable. Please double check your Xcode project settings."); + applicationGroupIdentifier_ = [groupIdentifier copy]; +} + ++ (void)enableDataSharingWithApplicationGroupIdentifier:(NSString *)groupIdentifier + containingApplication:(NSString *)bundleIdentifier { + PFConsistencyAssert(!currentParseManager_, + @"'enableDataSharingWithApplicationGroupIdentifier:containingApplication:' must be called before 'setApplicationId:clientKey'"); + PFParameterAssert([groupIdentifier length], @"'groupIdentifier' should not be nil."); + PFParameterAssert([bundleIdentifier length], @"Containing application bundle identifier should not be nil."); + PFConsistencyAssert([PFApplication currentApplication].extensionEnvironment, @"This method can only be used in application extensions."); + PFConsistencyAssert([PFFileManager isApplicationGroupContainerReachableForGroupIdentifier:groupIdentifier], + @"ApplicationGroupContainer is unreachable. Please double check your Xcode project settings."); + + applicationGroupIdentifier_ = groupIdentifier; + containingApplicationBundleIdentifier_ = bundleIdentifier; +} + ++ (NSString *)applicationGroupIdentifierForDataSharing { + return applicationGroupIdentifier_; +} + ++ (NSString *)containingApplicationBundleIdentifierForDataSharing { + return containingApplicationBundleIdentifier_; +} + ++ (void)_resetDataSharingIdentifiers { + applicationGroupIdentifier_ = nil; + containingApplicationBundleIdentifier_ = nil; +} + +///-------------------------------------- +#pragma mark - Local Datastore +///-------------------------------------- + ++ (void)enableLocalDatastore { + PFConsistencyAssert(!currentParseManager_, + @"'enableLocalDataStore' must be called before 'setApplicationId:clientKey:'"); + + // Lazily enableLocalDatastore after init. We can't use ParseModule because + // ParseModule isn't processed in main thread and may cause race condition. + shouldEnableLocalDatastore_ = YES; +} + ++ (BOOL)isLocalDatastoreEnabled { + if (!currentParseManager_) { + return shouldEnableLocalDatastore_; + } + return currentParseManager_.offlineStoreLoaded; +} + +///-------------------------------------- +#pragma mark - User Interface +///-------------------------------------- + +#if PARSE_IOS_ONLY + ++ (void)offlineMessagesEnabled:(BOOL)enabled { + // Deprecated method - shouldn't do anything. +} + ++ (void)errorMessagesEnabled:(BOOL)enabled { + // Deprecated method - shouldn't do anything. +} + +#endif + +///-------------------------------------- +#pragma mark - Logging +///-------------------------------------- + ++ (void)setLogLevel:(PFLogLevel)logLevel { + [PFLogger sharedLogger].logLevel = logLevel; +} + ++ (PFLogLevel)logLevel { + return [PFLogger sharedLogger].logLevel; +} + +///-------------------------------------- +#pragma mark - Private +///-------------------------------------- + ++ (ParseManager *)_currentManager { + return currentParseManager_; +} + ++ (void)_clearCurrentManager { + currentParseManager_ = nil; +} + +///-------------------------------------- +#pragma mark - Modules +///-------------------------------------- + ++ (void)enableParseModule:(id)module { + [[self parseModulesCollection] addParseModule:module]; +} + ++ (void)disableParseModule:(id)module { + [[self parseModulesCollection] removeParseModule:module]; +} + ++ (BOOL)isModuleEnabled:(id)module { + return [[self parseModulesCollection] containsModule:module]; +} + ++ (ParseModuleCollection *)parseModulesCollection { + static ParseModuleCollection *collection; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + collection = [[ParseModuleCollection alloc] init]; + }); + return collection; +} + +@end diff --git a/Parse/Resources/Framework.plist b/Parse/Resources/Framework.plist new file mode 100644 index 000000000..67febd37f --- /dev/null +++ b/Parse/Resources/Framework.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + Parse + CFBundleIdentifier + com.parse.Parse + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.8.0 + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + iPhoneSimulator + iPhoneOS + + CFBundleVersion + 1.8.0 + MinimumOSVersion + 6.0 + + diff --git a/Parse/Resources/FrameworkOSX.plist b/Parse/Resources/FrameworkOSX.plist new file mode 100644 index 000000000..f07e09b02 --- /dev/null +++ b/Parse/Resources/FrameworkOSX.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ParseOSX + CFBundleIdentifier + com.parse.ParseOSX + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.8.0 + CFBundleSignature + ???? + CFBundleVersion + 1.8.0 + + diff --git a/Parse/Resources/Localizable.strings b/Parse/Resources/Localizable.strings new file mode 100644 index 0000000000000000000000000000000000000000..8e8378c7ec4fb63e95474d0922dab2059b126ffa GIT binary patch literal 112 zcmXwvF$#b%5Co^rD-O#P<^f`7XW<)Sh#&+7iQiXOL5{t>x!wB;c#MI}Lgb`S#ZjI) kzB`FFlDH_D)v20YKa;Q0+igXjfTLkz)HZtQr%s9E0noS-F#rGn literal 0 HcmV?d00001 diff --git a/ParseStarterProject/.gitignore b/ParseStarterProject/.gitignore new file mode 100644 index 000000000..469a4ac17 --- /dev/null +++ b/ParseStarterProject/.gitignore @@ -0,0 +1 @@ +*.framework diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.pbxproj b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.pbxproj new file mode 100644 index 000000000..c2f6723fe --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.pbxproj @@ -0,0 +1,397 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + 81993FD51B69AC760077D6B9 /* Bootstrap */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 81993FD81B69AC760077D6B9 /* Build configuration list for PBXAggregateTarget "Bootstrap" */; + buildPhases = ( + 81993FD91B69AC7B0077D6B9 /* ShellScript */, + ); + dependencies = ( + ); + name = Bootstrap; + productName = Bootstrap; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 814C3AD21B69887B00E307BB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 814C3ACE1B69887B00E307BB /* MainMenu.xib */; }; + 814C3AD31B69887B00E307BB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AD01B69887B00E307BB /* Images.xcassets */; }; + 81CC85BE1A49F2E00076DE19 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CC85BD1A49F2E00076DE19 /* AppDelegate.swift */; }; + 81CC85DA1A49F3D70076DE19 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81CC85D81A49F3D70076DE19 /* Bolts.framework */; }; + 81CC85DB1A49F3D70076DE19 /* ParseOSX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81CC85D91A49F3D70076DE19 /* ParseOSX.framework */; }; + 81CC85DF1A49F53C0076DE19 /* Bolts.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 81CC85D81A49F3D70076DE19 /* Bolts.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 81CC85E01A49F53C0076DE19 /* ParseOSX.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 81CC85D91A49F3D70076DE19 /* ParseOSX.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 81993FDA1B69AC880077D6B9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 81CC85B01A49F2E00076DE19 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 81993FD51B69AC760077D6B9; + remoteInfo = Bootstrap; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 81CC85DE1A49F5340076DE19 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 81CC85DF1A49F53C0076DE19 /* Bolts.framework in CopyFiles */, + 81CC85E01A49F53C0076DE19 /* ParseOSX.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 814C3ACF1B69887B00E307BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 814C3AD01B69887B00E307BB /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 814C3AD11B69887B00E307BB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 81CC85B81A49F2E00076DE19 /* ParseOSXStarterProject-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ParseOSXStarterProject-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 81CC85BD1A49F2E00076DE19 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 81CC85D81A49F3D70076DE19 /* Bolts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Bolts.framework; sourceTree = ""; }; + 81CC85D91A49F3D70076DE19 /* ParseOSX.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ParseOSX.framework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 81CC85B51A49F2E00076DE19 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 81CC85DA1A49F3D70076DE19 /* Bolts.framework in Frameworks */, + 81CC85DB1A49F3D70076DE19 /* ParseOSX.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 814C3ACD1B69887B00E307BB /* Resources */ = { + isa = PBXGroup; + children = ( + 814C3AD01B69887B00E307BB /* Images.xcassets */, + 814C3AD11B69887B00E307BB /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 81CC85AF1A49F2E00076DE19 = { + isa = PBXGroup; + children = ( + 81CC85BA1A49F2E00076DE19 /* ParseOSXStarterProject */, + 814C3ACD1B69887B00E307BB /* Resources */, + 81CC85DC1A49F3DA0076DE19 /* Frameworks */, + 81CC85B91A49F2E00076DE19 /* Products */, + ); + sourceTree = ""; + }; + 81CC85B91A49F2E00076DE19 /* Products */ = { + isa = PBXGroup; + children = ( + 81CC85B81A49F2E00076DE19 /* ParseOSXStarterProject-Swift.app */, + ); + name = Products; + sourceTree = ""; + }; + 81CC85BA1A49F2E00076DE19 /* ParseOSXStarterProject */ = { + isa = PBXGroup; + children = ( + 81CC85BD1A49F2E00076DE19 /* AppDelegate.swift */, + 814C3ACE1B69887B00E307BB /* MainMenu.xib */, + ); + path = ParseOSXStarterProject; + sourceTree = ""; + }; + 81CC85DC1A49F3DA0076DE19 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 81CC85D81A49F3D70076DE19 /* Bolts.framework */, + 81CC85D91A49F3D70076DE19 /* ParseOSX.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 81CC85B71A49F2E00076DE19 /* ParseOSXStarterProject-Swift */ = { + isa = PBXNativeTarget; + buildConfigurationList = 81CC85D21A49F2E00076DE19 /* Build configuration list for PBXNativeTarget "ParseOSXStarterProject-Swift" */; + buildPhases = ( + 81CC85B41A49F2E00076DE19 /* Sources */, + 81CC85B51A49F2E00076DE19 /* Frameworks */, + 81CC85B61A49F2E00076DE19 /* Resources */, + 81CC85DE1A49F5340076DE19 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + 81993FDB1B69AC880077D6B9 /* PBXTargetDependency */, + ); + name = "ParseOSXStarterProject-Swift"; + productName = ParseOSXStarterProject; + productReference = 81CC85B81A49F2E00076DE19 /* ParseOSXStarterProject-Swift.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 81CC85B01A49F2E00076DE19 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Parse; + TargetAttributes = { + 81993FD51B69AC760077D6B9 = { + CreatedOnToolsVersion = 6.4; + }; + 81CC85B71A49F2E00076DE19 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 81CC85B31A49F2E00076DE19 /* Build configuration list for PBXProject "ParseOSXStarterProject-Swift" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 81CC85AF1A49F2E00076DE19; + productRefGroup = 81CC85B91A49F2E00076DE19 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 81CC85B71A49F2E00076DE19 /* ParseOSXStarterProject-Swift */, + 81993FD51B69AC760077D6B9 /* Bootstrap */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 81CC85B61A49F2E00076DE19 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 814C3AD21B69887B00E307BB /* MainMenu.xib in Resources */, + 814C3AD31B69887B00E307BB /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 81993FD91B69AC7B0077D6B9 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ ! -d \"Bolts.framework\" ]]; then\n cp -R ../../../Vendor/Bolts-ObjC/build/osx/Bolts.framework .\nfi\n\nrm -rf ParseOSX.framework\ncp -R $BUILT_PRODUCTS_DIR/ParseOSX.framework .\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 81CC85B41A49F2E00076DE19 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81CC85BE1A49F2E00076DE19 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 81993FDB1B69AC880077D6B9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 81993FD51B69AC760077D6B9 /* Bootstrap */; + targetProxy = 81993FDA1B69AC880077D6B9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 814C3ACE1B69887B00E307BB /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 814C3ACF1B69887B00E307BB /* Base */, + ); + name = MainMenu.xib; + path = ../Resources; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 81993FD61B69AC760077D6B9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 81993FD71B69AC760077D6B9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 81CC85D01A49F2E00076DE19 /* 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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = 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; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 81CC85D11A49F2E00076DE19 /* 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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + }; + name = Release; + }; + 81CC85D31A49F2E00076DE19 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + INFOPLIST_FILE = Resources/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_NAME = "ParseOSXStarterProject-Swift"; + }; + name = Debug; + }; + 81CC85D41A49F2E00076DE19 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + INFOPLIST_FILE = Resources/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_NAME = "ParseOSXStarterProject-Swift"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 81993FD81B69AC760077D6B9 /* Build configuration list for PBXAggregateTarget "Bootstrap" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81993FD61B69AC760077D6B9 /* Debug */, + 81993FD71B69AC760077D6B9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 81CC85B31A49F2E00076DE19 /* Build configuration list for PBXProject "ParseOSXStarterProject-Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81CC85D01A49F2E00076DE19 /* Debug */, + 81CC85D11A49F2E00076DE19 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 81CC85D21A49F2E00076DE19 /* Build configuration list for PBXNativeTarget "ParseOSXStarterProject-Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81CC85D31A49F2E00076DE19 /* Debug */, + 81CC85D41A49F2E00076DE19 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 81CC85B01A49F2E00076DE19 /* Project object */; +} diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..8713af992 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject-Swift.xcscheme b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject-Swift.xcscheme new file mode 100644 index 000000000..f148574b7 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject-Swift.xcscheme @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject/AppDelegate.swift b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject/AppDelegate.swift new file mode 100644 index 000000000..431ad15d1 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/ParseOSXStarterProject/AppDelegate.swift @@ -0,0 +1,73 @@ +/** +* Copyright (c) 2015-present, Parse, LLC. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +*/ + +import Cocoa + +import Bolts +import ParseOSX + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + @IBOutlet weak var window: NSWindow! + + func applicationDidFinishLaunching(aNotification: NSNotification) { + // Enable storing and querying data from Local Datastore. + // Remove this line if you don't want to use Local Datastore features or want to use cachePolicy. + Parse.enableLocalDatastore() + + // **************************************************************************** + // Uncomment and fill in with your Parse credentials: + // [Parse setApplicationId:@"your_application_id" clientKey:@"your_client_key"]; + // **************************************************************************** + + PFUser.enableAutomaticUser() + + let defaultACL: PFACL = PFACL() + // If you would like all objects to be private by default, remove this line. + defaultACL.setPublicReadAccess(true) + + PFACL.setDefaultACL(defaultACL, withAccessForCurrentUser: true) + + // **************************************************************************** + // Uncomment these lines to register for Push Notifications. + // + // let types = NSRemoteNotificationType.Alert | + // NSRemoteNotificationType.Badge | + // NSRemoteNotificationType.Sound; + // NSApplication.sharedApplication().registerForRemoteNotificationTypes(types) + // + // **************************************************************************** + + PFAnalytics.trackAppOpenedWithLaunchOptions(nil) + } + + func application(application: NSApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) { + let installation = PFInstallation.currentInstallation() + installation.setDeviceTokenFromData(deviceToken) + installation.saveInBackground() + + PFPush.subscribeToChannelInBackground("") { (succeeded: Bool, error: NSError?) in + if succeeded { + println("ParseStarterProject successfully subscribed to push notifications on the broadcast channel."); + } else { + println("ParseStarterProject failed to subscribe to push notifications on the broadcast channel with error = %@.", error) + } + } + } + + func application(application: NSApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) { + println("application:didFailToRegisterForRemoteNotificationsWithError: %@", error) + } + + func application(application: NSApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) { + PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo) + } + +} diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Base.lproj/MainMenu.xib b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..1cbeb9a8a --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Base.lproj/MainMenu.xib @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..2db2b1c7c --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Info.plist b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Info.plist new file mode 100644 index 000000000..7b8323b2e --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject-Swift/Resources/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + com.parse.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.8.0 + CFBundleSignature + ???? + CFBundleVersion + 1.8.0 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.pbxproj b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.pbxproj new file mode 100644 index 000000000..668860b4e --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.pbxproj @@ -0,0 +1,457 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + 81993FCE1B69ABA70077D6B9 /* Bootstrap */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 81993FD11B69ABA70077D6B9 /* Build configuration list for PBXAggregateTarget "Bootstrap" */; + buildPhases = ( + 81993FD21B69ABB00077D6B9 /* ShellScript */, + ); + dependencies = ( + ); + name = Bootstrap; + productName = Bootstrap; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 814C3ADD1B6988E300E307BB /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AD61B6988E300E307BB /* Credits.rtf */; }; + 814C3ADE1B6988E300E307BB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AD81B6988E300E307BB /* InfoPlist.strings */; }; + 814C3ADF1B6988E300E307BB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 814C3ADA1B6988E300E307BB /* MainMenu.xib */; }; + 815E9D8C196C6B0800F648E4 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 817AD67D196B026F0014C796 /* Bolts.framework */; }; + 815E9D8D196C6B1A00F648E4 /* Bolts.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 817AD67D196B026F0014C796 /* Bolts.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 974268D81651F09E00F2BC57 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D01651F09E00F2BC57 /* CFNetwork.framework */; }; + 974268D91651F09E00F2BC57 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D11651F09E00F2BC57 /* CoreGraphics.framework */; }; + 974268DA1651F09E00F2BC57 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D21651F09E00F2BC57 /* CoreLocation.framework */; }; + 974268DB1651F09E00F2BC57 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D31651F09E00F2BC57 /* libsqlite3.dylib */; }; + 974268DC1651F09E00F2BC57 /* libz.1.1.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D41651F09E00F2BC57 /* libz.1.1.3.dylib */; }; + 974268DD1651F09E00F2BC57 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D51651F09E00F2BC57 /* QuartzCore.framework */; }; + 974268DE1651F09E00F2BC57 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D61651F09E00F2BC57 /* Security.framework */; }; + 974268DF1651F09E00F2BC57 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974268D71651F09E00F2BC57 /* SystemConfiguration.framework */; }; + 978816C0163F1D6F00C613D2 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 978816BF163F1D6F00C613D2 /* Cocoa.framework */; }; + 978816CC163F1D6F00C613D2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 978816CB163F1D6F00C613D2 /* main.m */; }; + 978816D3163F1D6F00C613D2 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 978816D2163F1D6F00C613D2 /* AppDelegate.m */; }; + 97DFCF92166FF5FB0094BE60 /* ParseOSX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97DFCF91166FF5FB0094BE60 /* ParseOSX.framework */; }; + 97DFCF93166FF5FC0094BE60 /* ParseOSX.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 97DFCF91166FF5FB0094BE60 /* ParseOSX.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 97F7357D165B6AF000C4B72A /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97F7357C165B6AF000C4B72A /* ApplicationServices.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 81993FD31B69ABBC0077D6B9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 978816B2163F1D6F00C613D2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 81993FCE1B69ABA70077D6B9; + remoteInfo = Bootstrap; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 978818AB1641C2A800C613D2 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 97DFCF93166FF5FC0094BE60 /* ParseOSX.framework in CopyFiles */, + 815E9D8D196C6B1A00F648E4 /* Bolts.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 814C3AD71B6988E300E307BB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; + 814C3AD91B6988E300E307BB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 814C3ADB1B6988E300E307BB /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; + 814C3ADC1B6988E300E307BB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 817AD67D196B026F0014C796 /* Bolts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Bolts.framework; sourceTree = ""; }; + 974268D01651F09E00F2BC57 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 974268D11651F09E00F2BC57 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 974268D21651F09E00F2BC57 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; + 974268D31651F09E00F2BC57 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; + 974268D41651F09E00F2BC57 /* libz.1.1.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.1.1.3.dylib; path = usr/lib/libz.1.1.3.dylib; sourceTree = SDKROOT; }; + 974268D51651F09E00F2BC57 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 974268D61651F09E00F2BC57 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 974268D71651F09E00F2BC57 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 978816BB163F1D6F00C613D2 /* ParseOSXStarterProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ParseOSXStarterProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 978816BF163F1D6F00C613D2 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 978816C2163F1D6F00C613D2 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + 978816C3163F1D6F00C613D2 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; + 978816C4163F1D6F00C613D2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 978816CB163F1D6F00C613D2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 978816D1163F1D6F00C613D2 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 978816D2163F1D6F00C613D2 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 97DFCF91166FF5FB0094BE60 /* ParseOSX.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ParseOSX.framework; sourceTree = ""; }; + 97F7357C165B6AF000C4B72A /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 978816B8163F1D6F00C613D2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 97F7357D165B6AF000C4B72A /* ApplicationServices.framework in Frameworks */, + 974268D81651F09E00F2BC57 /* CFNetwork.framework in Frameworks */, + 974268D91651F09E00F2BC57 /* CoreGraphics.framework in Frameworks */, + 974268DA1651F09E00F2BC57 /* CoreLocation.framework in Frameworks */, + 974268DB1651F09E00F2BC57 /* libsqlite3.dylib in Frameworks */, + 974268DC1651F09E00F2BC57 /* libz.1.1.3.dylib in Frameworks */, + 974268DD1651F09E00F2BC57 /* QuartzCore.framework in Frameworks */, + 974268DE1651F09E00F2BC57 /* Security.framework in Frameworks */, + 974268DF1651F09E00F2BC57 /* SystemConfiguration.framework in Frameworks */, + 978816C0163F1D6F00C613D2 /* Cocoa.framework in Frameworks */, + 815E9D8C196C6B0800F648E4 /* Bolts.framework in Frameworks */, + 97DFCF92166FF5FB0094BE60 /* ParseOSX.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 814C3AD51B6988E300E307BB /* Resources */ = { + isa = PBXGroup; + children = ( + 814C3AD61B6988E300E307BB /* Credits.rtf */, + 814C3AD81B6988E300E307BB /* InfoPlist.strings */, + 814C3ADC1B6988E300E307BB /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 978816B0163F1D6F00C613D2 = { + isa = PBXGroup; + children = ( + 978816C5163F1D6F00C613D2 /* ParseOSXStarterProject */, + 814C3AD51B6988E300E307BB /* Resources */, + 978816BE163F1D6F00C613D2 /* Frameworks */, + 978816BC163F1D6F00C613D2 /* Products */, + ); + sourceTree = ""; + }; + 978816BC163F1D6F00C613D2 /* Products */ = { + isa = PBXGroup; + children = ( + 978816BB163F1D6F00C613D2 /* ParseOSXStarterProject.app */, + ); + name = Products; + sourceTree = ""; + }; + 978816BE163F1D6F00C613D2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 817AD67D196B026F0014C796 /* Bolts.framework */, + 97DFCF91166FF5FB0094BE60 /* ParseOSX.framework */, + 978816BF163F1D6F00C613D2 /* Cocoa.framework */, + 97F7357C165B6AF000C4B72A /* ApplicationServices.framework */, + 974268D01651F09E00F2BC57 /* CFNetwork.framework */, + 974268D11651F09E00F2BC57 /* CoreGraphics.framework */, + 974268D21651F09E00F2BC57 /* CoreLocation.framework */, + 974268D31651F09E00F2BC57 /* libsqlite3.dylib */, + 974268D41651F09E00F2BC57 /* libz.1.1.3.dylib */, + 974268D51651F09E00F2BC57 /* QuartzCore.framework */, + 974268D61651F09E00F2BC57 /* Security.framework */, + 974268D71651F09E00F2BC57 /* SystemConfiguration.framework */, + 978816C1163F1D6F00C613D2 /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + 978816C1163F1D6F00C613D2 /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 978816C2163F1D6F00C613D2 /* AppKit.framework */, + 978816C3163F1D6F00C613D2 /* CoreData.framework */, + 978816C4163F1D6F00C613D2 /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 978816C5163F1D6F00C613D2 /* ParseOSXStarterProject */ = { + isa = PBXGroup; + children = ( + 978816D1163F1D6F00C613D2 /* AppDelegate.h */, + 978816D2163F1D6F00C613D2 /* AppDelegate.m */, + 814C3ADA1B6988E300E307BB /* MainMenu.xib */, + 978816C6163F1D6F00C613D2 /* Supporting Files */, + ); + path = ParseOSXStarterProject; + sourceTree = ""; + }; + 978816C6163F1D6F00C613D2 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 978816CB163F1D6F00C613D2 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 978816BA163F1D6F00C613D2 /* ParseOSXStarterProject */ = { + isa = PBXNativeTarget; + buildConfigurationList = 978816D9163F1D6F00C613D2 /* Build configuration list for PBXNativeTarget "ParseOSXStarterProject" */; + buildPhases = ( + 978816B7163F1D6F00C613D2 /* Sources */, + 978816B8163F1D6F00C613D2 /* Frameworks */, + 978816B9163F1D6F00C613D2 /* Resources */, + 978818AB1641C2A800C613D2 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + 81993FD41B69ABBC0077D6B9 /* PBXTargetDependency */, + ); + name = ParseOSXStarterProject; + productName = ParseOSXStarterProject; + productReference = 978816BB163F1D6F00C613D2 /* ParseOSXStarterProject.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 978816B2163F1D6F00C613D2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Parse; + TargetAttributes = { + 81993FCE1B69ABA70077D6B9 = { + CreatedOnToolsVersion = 6.4; + }; + }; + }; + buildConfigurationList = 978816B5163F1D6F00C613D2 /* Build configuration list for PBXProject "ParseOSXStarterProject" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 978816B0163F1D6F00C613D2; + productRefGroup = 978816BC163F1D6F00C613D2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 978816BA163F1D6F00C613D2 /* ParseOSXStarterProject */, + 81993FCE1B69ABA70077D6B9 /* Bootstrap */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 978816B9163F1D6F00C613D2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 814C3ADD1B6988E300E307BB /* Credits.rtf in Resources */, + 814C3ADE1B6988E300E307BB /* InfoPlist.strings in Resources */, + 814C3ADF1B6988E300E307BB /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 81993FD21B69ABB00077D6B9 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ ! -d \"Bolts.framework\" ]]; then\n cp -R ../../../Vendor/Bolts-ObjC/build/osx/Bolts.framework .\nfi\n\nrm -rf ParseOSX.framework\ncp -R $BUILT_PRODUCTS_DIR/ParseOSX.framework .\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 978816B7163F1D6F00C613D2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978816CC163F1D6F00C613D2 /* main.m in Sources */, + 978816D3163F1D6F00C613D2 /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 81993FD41B69ABBC0077D6B9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 81993FCE1B69ABA70077D6B9 /* Bootstrap */; + targetProxy = 81993FD31B69ABBC0077D6B9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 814C3AD61B6988E300E307BB /* Credits.rtf */ = { + isa = PBXVariantGroup; + children = ( + 814C3AD71B6988E300E307BB /* en */, + ); + name = Credits.rtf; + sourceTree = ""; + }; + 814C3AD81B6988E300E307BB /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 814C3AD91B6988E300E307BB /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 814C3ADA1B6988E300E307BB /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 814C3ADB1B6988E300E307BB /* en */, + ); + name = MainMenu.xib; + path = ../Resources; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 81993FCF1B69ABA70077D6B9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 81993FD01B69ABA70077D6B9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 978816D7163F1D6F00C613D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 978816D8163F1D6F00C613D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + SDKROOT = macosx; + }; + name = Release; + }; + 978816DA163F1D6F00C613D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + "$(PROJECT_DIR)", + ); + INFOPLIST_FILE = Resources/Info.plist; + PRODUCT_NAME = ParseOSXStarterProject; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 978816DB163F1D6F00C613D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + "$(PROJECT_DIR)", + ); + INFOPLIST_FILE = Resources/Info.plist; + PRODUCT_NAME = ParseOSXStarterProject; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 81993FD11B69ABA70077D6B9 /* Build configuration list for PBXAggregateTarget "Bootstrap" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81993FCF1B69ABA70077D6B9 /* Debug */, + 81993FD01B69ABA70077D6B9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 978816B5163F1D6F00C613D2 /* Build configuration list for PBXProject "ParseOSXStarterProject" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 978816D7163F1D6F00C613D2 /* Debug */, + 978816D8163F1D6F00C613D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 978816D9163F1D6F00C613D2 /* Build configuration list for PBXNativeTarget "ParseOSXStarterProject" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 978816DA163F1D6F00C613D2 /* Debug */, + 978816DB163F1D6F00C613D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 978816B2163F1D6F00C613D2 /* Project object */; +} diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..f20f6ef7c --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject.xcscheme b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject.xcscheme new file mode 100644 index 000000000..7636cffda --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject.xcodeproj/xcshareddata/xcschemes/ParseOSXStarterProject.xcscheme @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.h b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.h new file mode 100644 index 000000000..d107fd7b0 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@interface AppDelegate : NSObject + +@property (assign) IBOutlet NSWindow *window; + +@end diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.m b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.m new file mode 100644 index 000000000..2e665c917 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/AppDelegate.m @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "AppDelegate.h" + +@implementation AppDelegate + +#pragma mark - +#pragma mark NSApplicationDelegate + +- (void)applicationDidFinishLaunching:(NSNotification *)notification { + // Enable storing and querying data from Local Datastore. + // Remove this line if you don't want to use Local Datastore features or want to use cachePolicy. + [Parse enableLocalDatastore]; + + // **************************************************************************** + // Uncomment and fill in with your Parse credentials: + // [Parse setApplicationId:@"your_application_id" clientKey:@"your_client_key"]; + // **************************************************************************** + + [PFUser enableAutomaticUser]; + + PFACL *defaultACL = [PFACL ACL]; + + // If you would like all objects to be private by default, remove this line. + [defaultACL setPublicReadAccess:YES]; + + [PFACL setDefaultACL:defaultACL withAccessForCurrentUser:YES]; + + // **************************************************************************** + // Uncomment these lines to register for Push Notifications. + // + // NSRemoteNotificationType types = (NSRemoteNotificationTypeAlert | + // NSRemoteNotificationTypeBadge | + // NSRemoteNotificationTypeSound); + // [[NSApplication sharedApplication] registerForRemoteNotificationTypes:types]; + // + // **************************************************************************** + + [PFAnalytics trackAppOpenedWithLaunchOptions:nil]; +} + +#pragma mark Push Notifications + +- (void)application:(NSApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + PFInstallation *currentInstallation = [PFInstallation currentInstallation]; + [currentInstallation setDeviceTokenFromData:deviceToken]; + [currentInstallation saveInBackground]; + + [PFPush subscribeToChannelInBackground:@"" block:^(BOOL succeeded, NSError *error) { + if (succeeded) { + NSLog(@"ParseStarterProject successfully subscribed to push notifications on the broadcast channel."); + } else { + NSLog(@"ParseStarterProject failed to subscribe to push notifications on the broadcast channel."); + } + }]; +} + +- (void)application:(NSApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { + // Show some alert or otherwise handle the failure to register. + NSLog(@"application:didFailToRegisterForRemoteNotificationsWithError: %@", error); +} + +- (void)application:(NSApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { + [PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo]; +} + +@end diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/main.m b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/main.m new file mode 100644 index 000000000..b997c4c71 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/ParseOSXStarterProject/main.m @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +int main(int argc, const char *argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/Info.plist b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/Info.plist new file mode 100644 index 000000000..e3f63d5ed --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.parse.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.8.0 + CFBundleSignature + ???? + CFBundleVersion + 1.8.0 + LSMinimumSystemVersion + ${MACOSX_DEPLOYMENT_TARGET} + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/Credits.rtf b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/Credits.rtf new file mode 100644 index 000000000..46576ef21 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/Credits.rtf @@ -0,0 +1,29 @@ +{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} +{\colortbl;\red255\green255\blue255;} +\paperw9840\paperh8400 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural + +\f0\b\fs24 \cf0 Engineering: +\b0 \ + Some people\ +\ + +\b Human Interface Design: +\b0 \ + Some other people\ +\ + +\b Testing: +\b0 \ + Hopefully not nobody\ +\ + +\b Documentation: +\b0 \ + Whoever\ +\ + +\b With special thanks to: +\b0 \ + Mom\ +} diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/InfoPlist.strings b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/InfoPlist.strings new file mode 100644 index 000000000..b92732c79 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* Localized versions of Info.plist keys */ diff --git a/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/MainMenu.xib b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/MainMenu.xib new file mode 100644 index 000000000..9528e6144 --- /dev/null +++ b/ParseStarterProject/OSX/ParseOSXStarterProject/Resources/en.lproj/MainMenu.xib @@ -0,0 +1,4666 @@ + + + + 1080 + 12A269 + 2840 + 1187 + 624.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 2840 + + + NSCustomObject + NSMenu + NSMenuItem + NSView + NSWindowTemplate + + + com.apple.InterfaceBuilder.CocoaPlugin + + + PluginDependencyRecalculationVersion + + + + + NSApplication + + + FirstResponder + + + NSApplication + + + AMainMenu + + + + ParseOSXStarterProject + + 1048576 + 2147483647 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + submenuAction: + + ParseOSXStarterProject + + + + About ParseOSXStarterProject + + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Preferences… + , + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Services + + 1048576 + 2147483647 + + + submenuAction: + + Services + + _NSServicesMenu + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Hide ParseOSXStarterProject + h + 1048576 + 2147483647 + + + + + + Hide Others + h + 1572864 + 2147483647 + + + + + + Show All + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Quit ParseOSXStarterProject + q + 1048576 + 2147483647 + + + + + _NSAppleMenu + + + + + File + + 1048576 + 2147483647 + + + submenuAction: + + File + + + + New + n + 1048576 + 2147483647 + + + + + + Open… + o + 1048576 + 2147483647 + + + + + + Open Recent + + 1048576 + 2147483647 + + + submenuAction: + + Open Recent + + + + Clear Menu + + 1048576 + 2147483647 + + + + + _NSRecentDocumentsMenu + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Close + w + 1048576 + 2147483647 + + + + + + Save… + s + 1048576 + 2147483647 + + + + + + Revert to Saved + + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Page Setup... + P + 1179648 + 2147483647 + + + + + + + Print… + p + 1048576 + 2147483647 + + + + + + + + + Edit + + 1048576 + 2147483647 + + + submenuAction: + + Edit + + + + Undo + z + 1048576 + 2147483647 + + + + + + Redo + Z + 1179648 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Cut + x + 1048576 + 2147483647 + + + + + + Copy + c + 1048576 + 2147483647 + + + + + + Paste + v + 1048576 + 2147483647 + + + + + + Paste and Match Style + V + 1572864 + 2147483647 + + + + + + Delete + + 1048576 + 2147483647 + + + + + + Select All + a + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Find + + 1048576 + 2147483647 + + + submenuAction: + + Find + + + + Find… + f + 1048576 + 2147483647 + + + 1 + + + + Find and Replace… + f + 1572864 + 2147483647 + + + 12 + + + + Find Next + g + 1048576 + 2147483647 + + + 2 + + + + Find Previous + G + 1179648 + 2147483647 + + + 3 + + + + Use Selection for Find + e + 1048576 + 2147483647 + + + 7 + + + + Jump to Selection + j + 1048576 + 2147483647 + + + + + + + + + Spelling and Grammar + + 1048576 + 2147483647 + + + submenuAction: + + Spelling and Grammar + + + + Show Spelling and Grammar + : + 1048576 + 2147483647 + + + + + + Check Document Now + ; + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Check Spelling While Typing + + 1048576 + 2147483647 + + + + + + Check Grammar With Spelling + + 1048576 + 2147483647 + + + + + + Correct Spelling Automatically + + 2147483647 + + + + + + + + + Substitutions + + 1048576 + 2147483647 + + + submenuAction: + + Substitutions + + + + Show Substitutions + + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Smart Copy/Paste + f + 1048576 + 2147483647 + + + 1 + + + + Smart Quotes + g + 1048576 + 2147483647 + + + 2 + + + + Smart Dashes + + 2147483647 + + + + + + Smart Links + G + 1179648 + 2147483647 + + + 3 + + + + Text Replacement + + 2147483647 + + + + + + + + + Transformations + + 2147483647 + + + submenuAction: + + Transformations + + + + Make Upper Case + + 2147483647 + + + + + + Make Lower Case + + 2147483647 + + + + + + Capitalize + + 2147483647 + + + + + + + + + Speech + + 1048576 + 2147483647 + + + submenuAction: + + Speech + + + + Start Speaking + + 1048576 + 2147483647 + + + + + + Stop Speaking + + 1048576 + 2147483647 + + + + + + + + + + + + Format + + 2147483647 + + + submenuAction: + + Format + + + + Font + + 2147483647 + + + submenuAction: + + Font + + + + Show Fonts + t + 1048576 + 2147483647 + + + + + + Bold + b + 1048576 + 2147483647 + + + 2 + + + + Italic + i + 1048576 + 2147483647 + + + 1 + + + + Underline + u + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Bigger + + + 1048576 + 2147483647 + + + 3 + + + + Smaller + - + 1048576 + 2147483647 + + + 4 + + + + YES + YES + + + 2147483647 + + + + + + Kern + + 2147483647 + + + submenuAction: + + Kern + + + + Use Default + + 2147483647 + + + + + + Use None + + 2147483647 + + + + + + Tighten + + 2147483647 + + + + + + Loosen + + 2147483647 + + + + + + + + + Ligatures + + 2147483647 + + + submenuAction: + + Ligatures + + + + Use Default + + 2147483647 + + + + + + Use None + + 2147483647 + + + + + + Use All + + 2147483647 + + + + + + + + + Baseline + + 2147483647 + + + submenuAction: + + Baseline + + + + Use Default + + 2147483647 + + + + + + Superscript + + 2147483647 + + + + + + Subscript + + 2147483647 + + + + + + Raise + + 2147483647 + + + + + + Lower + + 2147483647 + + + + + + + + + YES + YES + + + 2147483647 + + + + + + Show Colors + C + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Copy Style + c + 1572864 + 2147483647 + + + + + + Paste Style + v + 1572864 + 2147483647 + + + + + _NSFontMenu + + + + + Text + + 2147483647 + + + submenuAction: + + Text + + + + Align Left + { + 1048576 + 2147483647 + + + + + + Center + | + 1048576 + 2147483647 + + + + + + Justify + + 2147483647 + + + + + + Align Right + } + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Writing Direction + + 2147483647 + + + submenuAction: + + Writing Direction + + + + YES + Paragraph + + 2147483647 + + + + + + CURlZmF1bHQ + + 2147483647 + + + + + + CUxlZnQgdG8gUmlnaHQ + + 2147483647 + + + + + + CVJpZ2h0IHRvIExlZnQ + + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + YES + Selection + + 2147483647 + + + + + + CURlZmF1bHQ + + 2147483647 + + + + + + CUxlZnQgdG8gUmlnaHQ + + 2147483647 + + + + + + CVJpZ2h0IHRvIExlZnQ + + 2147483647 + + + + + + + + + YES + YES + + + 2147483647 + + + + + + Show Ruler + + 2147483647 + + + + + + Copy Ruler + c + 1310720 + 2147483647 + + + + + + Paste Ruler + v + 1310720 + 2147483647 + + + + + + + + + + + + View + + 1048576 + 2147483647 + + + submenuAction: + + View + + + + Show Toolbar + t + 1572864 + 2147483647 + + + + + + Customize Toolbar… + + 1048576 + 2147483647 + + + + + + + + + Window + + 1048576 + 2147483647 + + + submenuAction: + + Window + + + + Minimize + m + 1048576 + 2147483647 + + + + + + Zoom + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Bring All to Front + + 1048576 + 2147483647 + + + + + _NSWindowsMenu + + + + + Help + + 2147483647 + + + submenuAction: + + Help + + + + ParseOSXStarterProject Help + ? + 1048576 + 2147483647 + + + + + _NSHelpMenu + + + + _NSMainMenu + + + 15 + 2 + {{335, 390}, {480, 360}} + 1954021376 + ParseOSXStarterProject + NSWindow + + + + + 256 + {480, 360} + + {{0, 0}, {2560, 1418}} + {10000000000000, 10000000000000} + YES + + + AppDelegate + + + NSFontManager + + + + + + + terminate: + + + + 449 + + + + orderFrontStandardAboutPanel: + + + + 142 + + + + delegate + + + + 495 + + + + performMiniaturize: + + + + 37 + + + + arrangeInFront: + + + + 39 + + + + print: + + + + 86 + + + + runPageLayout: + + + + 87 + + + + clearRecentDocuments: + + + + 127 + + + + performClose: + + + + 193 + + + + toggleContinuousSpellChecking: + + + + 222 + + + + undo: + + + + 223 + + + + copy: + + + + 224 + + + + checkSpelling: + + + + 225 + + + + paste: + + + + 226 + + + + stopSpeaking: + + + + 227 + + + + cut: + + + + 228 + + + + showGuessPanel: + + + + 230 + + + + redo: + + + + 231 + + + + selectAll: + + + + 232 + + + + startSpeaking: + + + + 233 + + + + delete: + + + + 235 + + + + performZoom: + + + + 240 + + + + performFindPanelAction: + + + + 241 + + + + centerSelectionInVisibleArea: + + + + 245 + + + + toggleGrammarChecking: + + + + 347 + + + + toggleSmartInsertDelete: + + + + 355 + + + + toggleAutomaticQuoteSubstitution: + + + + 356 + + + + toggleAutomaticLinkDetection: + + + + 357 + + + + saveDocument: + + + + 362 + + + + revertDocumentToSaved: + + + + 364 + + + + runToolbarCustomizationPalette: + + + + 365 + + + + toggleToolbarShown: + + + + 366 + + + + hide: + + + + 367 + + + + hideOtherApplications: + + + + 368 + + + + unhideAllApplications: + + + + 370 + + + + newDocument: + + + + 373 + + + + openDocument: + + + + 374 + + + + raiseBaseline: + + + + 426 + + + + lowerBaseline: + + + + 427 + + + + copyFont: + + + + 428 + + + + subscript: + + + + 429 + + + + superscript: + + + + 430 + + + + tightenKerning: + + + + 431 + + + + underline: + + + + 432 + + + + orderFrontColorPanel: + + + + 433 + + + + useAllLigatures: + + + + 434 + + + + loosenKerning: + + + + 435 + + + + pasteFont: + + + + 436 + + + + unscript: + + + + 437 + + + + useStandardKerning: + + + + 438 + + + + useStandardLigatures: + + + + 439 + + + + turnOffLigatures: + + + + 440 + + + + turnOffKerning: + + + + 441 + + + + toggleAutomaticSpellingCorrection: + + + + 456 + + + + orderFrontSubstitutionsPanel: + + + + 458 + + + + toggleAutomaticDashSubstitution: + + + + 461 + + + + toggleAutomaticTextReplacement: + + + + 463 + + + + uppercaseWord: + + + + 464 + + + + capitalizeWord: + + + + 467 + + + + lowercaseWord: + + + + 468 + + + + pasteAsPlainText: + + + + 486 + + + + performFindPanelAction: + + + + 487 + + + + performFindPanelAction: + + + + 488 + + + + performFindPanelAction: + + + + 489 + + + + showHelp: + + + + 493 + + + + alignCenter: + + + + 518 + + + + pasteRuler: + + + + 519 + + + + toggleRuler: + + + + 520 + + + + alignRight: + + + + 521 + + + + copyRuler: + + + + 522 + + + + alignJustified: + + + + 523 + + + + alignLeft: + + + + 524 + + + + makeBaseWritingDirectionNatural: + + + + 525 + + + + makeBaseWritingDirectionLeftToRight: + + + + 526 + + + + makeBaseWritingDirectionRightToLeft: + + + + 527 + + + + makeTextWritingDirectionNatural: + + + + 528 + + + + makeTextWritingDirectionLeftToRight: + + + + 529 + + + + makeTextWritingDirectionRightToLeft: + + + + 530 + + + + performFindPanelAction: + + + + 535 + + + + addFontTrait: + + + + 421 + + + + addFontTrait: + + + + 422 + + + + modifyFont: + + + + 423 + + + + orderFrontFontPanel: + + + + 424 + + + + modifyFont: + + + + 425 + + + + window + + + + 532 + + + + + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 29 + + + + + + + + + + + + + + 19 + + + + + + + + 56 + + + + + + + + 217 + + + + + + + + 83 + + + + + + + + 81 + + + + + + + + + + + + + + + + + 75 + + + + + 78 + + + + + 72 + + + + + 82 + + + + + 124 + + + + + + + + 77 + + + + + 73 + + + + + 79 + + + + + 112 + + + + + 74 + + + + + 125 + + + + + + + + 126 + + + + + 205 + + + + + + + + + + + + + + + + + + + + + + 202 + + + + + 198 + + + + + 207 + + + + + 214 + + + + + 199 + + + + + 203 + + + + + 197 + + + + + 206 + + + + + 215 + + + + + 218 + + + + + + + + 216 + + + + + + + + 200 + + + + + + + + + + + + + 219 + + + + + 201 + + + + + 204 + + + + + 220 + + + + + + + + + + + + + 213 + + + + + 210 + + + + + 221 + + + + + 208 + + + + + 209 + + + + + 57 + + + + + + + + + + + + + + + + + + 58 + + + + + 134 + + + + + 150 + + + + + 136 + + + + + 144 + + + + + 129 + + + + + 143 + + + + + 236 + + + + + 131 + + + + + + + + 149 + + + + + 145 + + + + + 130 + + + + + 24 + + + + + + + + + + + 92 + + + + + 5 + + + + + 239 + + + + + 23 + + + + + 295 + + + + + + + + 296 + + + + + + + + + 297 + + + + + 298 + + + + + 211 + + + + + + + + 212 + + + + + + + + + 195 + + + + + 196 + + + + + 346 + + + + + 348 + + + + + + + + 349 + + + + + + + + + + + + + + 350 + + + + + 351 + + + + + 354 + + + + + 371 + + + + + + + + 372 + + + + + 375 + + + + + + + + 376 + + + + + + + + + 377 + + + + + + + + 388 + + + + + + + + + + + + + + + + + + + + + + + 389 + + + + + 390 + + + + + 391 + + + + + 392 + + + + + 393 + + + + + 394 + + + + + 395 + + + + + 396 + + + + + 397 + + + + + + + + 398 + + + + + + + + 399 + + + + + + + + 400 + + + + + 401 + + + + + 402 + + + + + 403 + + + + + 404 + + + + + 405 + + + + + + + + + + + + 406 + + + + + 407 + + + + + 408 + + + + + 409 + + + + + 410 + + + + + 411 + + + + + + + + + + 412 + + + + + 413 + + + + + 414 + + + + + 415 + + + + + + + + + + + 416 + + + + + 417 + + + + + 418 + + + + + 419 + + + + + 420 + + + + + 450 + + + + + + + + 451 + + + + + + + + + + 452 + + + + + 453 + + + + + 454 + + + + + 457 + + + + + 459 + + + + + 460 + + + + + 462 + + + + + 465 + + + + + 466 + + + + + 485 + + + + + 490 + + + + + + + + 491 + + + + + + + + 492 + + + + + 494 + + + + + 496 + + + + + + + + 497 + + + + + + + + + + + + + + + + + 498 + + + + + 499 + + + + + 500 + + + + + 501 + + + + + 502 + + + + + 503 + + + + + + + + 504 + + + + + 505 + + + + + 506 + + + + + 507 + + + + + 508 + + + + + + + + + + + + + + + + 509 + + + + + 510 + + + + + 511 + + + + + 512 + + + + + 513 + + + + + 514 + + + + + 515 + + + + + 516 + + + + + 517 + + + + + 534 + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{380, 496}, {480, 360}} + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + 535 + + + + + ABCardController + NSObject + + id + id + id + id + id + id + id + + + + addCardViewField: + id + + + copy: + id + + + cut: + id + + + doDelete: + id + + + find: + id + + + paste: + id + + + saveChanges: + id + + + + ABCardView + NSButton + NSManagedObjectContext + NSSearchField + NSTextField + NSWindow + + + + mCardView + ABCardView + + + mEditButton + NSButton + + + mManagedObjectContext + NSManagedObjectContext + + + mSearchField + NSSearchField + + + mStatusTextField + NSTextField + + + mWindow + NSWindow + + + + IBProjectSource + ./Classes/ABCardController.h + + + + ABCardView + NSView + + id + id + + + + commitAndSave: + id + + + statusImageClicked: + id + + + + NSObjectController + NSImageView + NSView + ABNameFrameView + NSView + NSImage + ABImageView + + + + mBindingsController + NSObjectController + + + mBuddyStatusImage + NSImageView + + + mHeaderView + NSView + + + mNameView + ABNameFrameView + + + mNextKeyView + NSView + + + mUserImage + NSImage + + + mUserImageView + ABImageView + + + + IBProjectSource + ./Classes/ABCardView.h + + + + ABImageView + NSImageView + + id + id + id + id + + + + copy: + id + + + cut: + id + + + delete: + id + + + paste: + id + + + + IBProjectSource + ./Classes/ABImageView.h + + + + AppDelegate + NSObject + + id + id + + + + applicationShouldTerminate: + id + + + applicationWillFinishLaunching: + id + + + + IBProjectSource + ./Classes/AppDelegate.h + + + + DVTBorderedView + DVTLayoutView_ML + + contentView + NSView + + + contentView + + contentView + NSView + + + + IBProjectSource + ./Classes/DVTBorderedView.h + + + + DVTDelayedMenuButton + NSButton + + IBProjectSource + ./Classes/DVTDelayedMenuButton.h + + + + DVTGradientImageButton + NSButton + + IBProjectSource + ./Classes/DVTGradientImageButton.h + + + + DVTImageAndTextCell + NSTextFieldCell + + IBProjectSource + ./Classes/DVTImageAndTextCell.h + + + + DVTImageAndTextColumn + NSTableColumn + + IBProjectSource + ./Classes/DVTImageAndTextColumn.h + + + + DVTLayoutView_ML + NSView + + IBProjectSource + ./Classes/DVTLayoutView_ML.h + + + + DVTOutlineView + NSOutlineView + + IBProjectSource + ./Classes/DVTOutlineView.h + + + + DVTSplitView + NSSplitView + + IBProjectSource + ./Classes/DVTSplitView.h + + + + DVTStackView_ML + DVTLayoutView_ML + + IBProjectSource + ./Classes/DVTStackView_ML.h + + + + DVTTableView + NSTableView + + IBProjectSource + ./Classes/DVTTableView.h + + + + DVTViewController + NSViewController + + IBProjectSource + ./Classes/DVTViewController.h + + + + HFController + NSObject + + selectAll: + id + + + selectAll: + + selectAll: + id + + + + IBProjectSource + ./Classes/HFController.h + + + + HFRepresenterTextView + NSView + + selectAll: + id + + + selectAll: + + selectAll: + id + + + + IBProjectSource + ./Classes/HFRepresenterTextView.h + + + + IBEditor + NSObject + + id + id + id + id + id + + + + changeFont: + id + + + performCopy: + id + + + performCut: + id + + + selectAll: + id + + + sizeSelectionToFit: + id + + + + IBProjectSource + ./Classes/IBEditor.h + + + + IDECapsuleListView + DVTStackView_ML + + dataSource + id + + + dataSource + + dataSource + id + + + + IBProjectSource + ./Classes/IDECapsuleListView.h + + + + IDEDMArrayController + NSArrayController + + IBProjectSource + ./Classes/IDEDMArrayController.h + + + + IDEDMEditor + IDEEditor + + DVTBorderedView + NSView + IDEDMEditorSourceListController + DVTSplitView + + + + bottomToolbarBorderView + DVTBorderedView + + + sourceListSplitViewPane + NSView + + + sourceListViewController + IDEDMEditorSourceListController + + + splitView + DVTSplitView + + + + IBProjectSource + ./Classes/IDEDMEditor.h + + + + IDEDMEditorController + IDEViewController + + IBProjectSource + ./Classes/IDEDMEditorController.h + + + + IDEDMEditorSourceListController + IDEDMEditorController + + DVTBorderedView + IDEDMEditor + DVTImageAndTextColumn + DVTOutlineView + NSTreeController + + + + borderedView + DVTBorderedView + + + parentEditor + IDEDMEditor + + + primaryColumn + DVTImageAndTextColumn + + + sourceListOutlineView + DVTOutlineView + + + sourceListTreeController + NSTreeController + + + + IBProjectSource + ./Classes/IDEDMEditorSourceListController.h + + + + IDEDMHighlightImageAndTextCell + DVTImageAndTextCell + + IBProjectSource + ./Classes/IDEDMHighlightImageAndTextCell.h + + + + IDEDataModelBrowserEditor + IDEDMEditorController + + IDEDataModelPropertiesTableController + IDECapsuleListView + NSArrayController + IDEDataModelPropertiesTableController + IDEDataModelEntityContentsEditor + IDEDataModelPropertiesTableController + + + + attributesTableViewController + IDEDataModelPropertiesTableController + + + capsuleView + IDECapsuleListView + + + entityArrayController + NSArrayController + + + fetchedPropertiesTableViewController + IDEDataModelPropertiesTableController + + + parentEditor + IDEDataModelEntityContentsEditor + + + relationshipsTableViewController + IDEDataModelPropertiesTableController + + + + IBProjectSource + ./Classes/IDEDataModelBrowserEditor.h + + + + IDEDataModelConfigurationEditor + IDEDMEditorController + + IDECapsuleListView + IDEDataModelEditor + IDEDataModelConfigurationTableController + + + + capsuleListView + IDECapsuleListView + + + parentEditor + IDEDataModelEditor + + + tableController + IDEDataModelConfigurationTableController + + + + IBProjectSource + ./Classes/IDEDataModelConfigurationEditor.h + + + + IDEDataModelConfigurationTableController + IDEDMEditorController + + NSArrayController + NSArrayController + IDEDataModelConfigurationEditor + XDTableView + + + + configurationsArrayController + NSArrayController + + + entitiesArrayController + NSArrayController + + + parentEditor + IDEDataModelConfigurationEditor + + + tableView + XDTableView + + + + IBProjectSource + ./Classes/IDEDataModelConfigurationTableController.h + + + + IDEDataModelDiagramEditor + IDEDMEditorController + + XDDiagramView + IDEDataModelEntityContentsEditor + + + + diagramView + XDDiagramView + + + parentEditor + IDEDataModelEntityContentsEditor + + + + IBProjectSource + ./Classes/IDEDataModelDiagramEditor.h + + + + IDEDataModelEditor + IDEDMEditor + + DVTDelayedMenuButton + DVTDelayedMenuButton + NSSegmentedControl + IDEDataModelConfigurationEditor + IDEDataModelEntityContentsEditor + IDEDataModelFetchRequestEditor + NSSegmentedControl + NSTabView + + + + addEntityButton + DVTDelayedMenuButton + + + addPropertyButton + DVTDelayedMenuButton + + + browserDiagramSegmentControl + NSSegmentedControl + + + configurationViewController + IDEDataModelConfigurationEditor + + + entityContentsViewController + IDEDataModelEntityContentsEditor + + + fetchRequestViewController + IDEDataModelFetchRequestEditor + + + hierarchySegmentControl + NSSegmentedControl + + + tabView + NSTabView + + + + IBProjectSource + ./Classes/IDEDataModelEditor.h + + + + IDEDataModelEntityContentsEditor + IDEDMEditorController + + IDEDataModelBrowserEditor + IDEDataModelDiagramEditor + IDEDataModelEditor + NSTabView + + + + browserViewController + IDEDataModelBrowserEditor + + + diagramViewController + IDEDataModelDiagramEditor + + + parentEditor + IDEDataModelEditor + + + tabView + NSTabView + + + + IBProjectSource + ./Classes/IDEDataModelEntityContentsEditor.h + + + + IDEDataModelFetchRequestEditor + IDEDMEditorController + + NSArrayController + IDEDataModelEditor + IDECapsuleListView + + + + entityController + NSArrayController + + + parentEditor + IDEDataModelEditor + + + tableView + IDECapsuleListView + + + + IBProjectSource + ./Classes/IDEDataModelFetchRequestEditor.h + + + + IDEDataModelPropertiesTableController + IDEDMEditorController + + IDEDMArrayController + NSTableColumn + NSArrayController + IDEDataModelBrowserEditor + IDEDMHighlightImageAndTextCell + XDTableView + + + + arrayController + IDEDMArrayController + + + entitiesColumn + NSTableColumn + + + entityArrayController + NSArrayController + + + parentEditor + IDEDataModelBrowserEditor + + + propertyNameAndImageCell + IDEDMHighlightImageAndTextCell + + + tableView + XDTableView + + + + IBProjectSource + ./Classes/IDEDataModelPropertiesTableController.h + + + + IDEDocDownloadsTableViewController + NSObject + + NSButtonCell + DVTTableView + IDEDocViewingPrefPaneController + + + + _downloadButtonCell + NSButtonCell + + + _tableView + DVTTableView + + + prefPaneController + IDEDocViewingPrefPaneController + + + + IBProjectSource + ./Classes/IDEDocDownloadsTableViewController.h + + + + IDEDocSetOutlineView + NSOutlineView + + IBProjectSource + ./Classes/IDEDocSetOutlineView.h + + + + IDEDocSetOutlineViewController + NSObject + + id + id + id + id + id + + + + getDocSetAction: + id + + + showProblemInfoForUpdate: + id + + + subscribeToPublisherAction: + id + + + unsubscribeFromPublisher: + id + + + updateDocSetAction: + id + + + + docSetOutlineView + IDEDocSetOutlineView + + + docSetOutlineView + + docSetOutlineView + IDEDocSetOutlineView + + + + IBProjectSource + ./Classes/IDEDocSetOutlineViewController.h + + + + IDEDocViewingPrefPaneController + IDEViewController + + id + id + id + id + id + id + id + id + id + id + id + + + + addSubscription: + id + + + checkForAndInstallUpdatesNow: + id + + + deleteDocSet: + id + + + downloadAction: + id + + + minimumFontSizeComboBoxAction: + id + + + minimumFontSizeEnabledAction: + id + + + showHelp: + id + + + showSubscriptionSheet: + id + + + subscriptionCancelAction: + id + + + toggleAutoCheckForAndInstallUpdates: + id + + + toggleDocSetInfo: + id + + + + DVTGradientImageButton + DVTGradientImageButton + DVTGradientImageButton + NSSplitView + NSView + NSView + DVTBorderedView + DVTBorderedView + NSButton + NSTextView + IDEDocSetOutlineViewController + IDEDocDownloadsTableViewController + NSComboBox + NSTextField + NSButton + NSTextField + NSWindow + NSButton + + + + _addButton + DVTGradientImageButton + + + _deleteButton + DVTGradientImageButton + + + _showInfoAreaButton + DVTGradientImageButton + + + _splitView + NSSplitView + + + _splitViewDocSetInfoSubview + NSView + + + _splitViewDocSetsListSubview + NSView + + + borderedViewAroundSplitView + DVTBorderedView + + + borderedViewBelowTable + DVTBorderedView + + + checkAndInstallNowButton + NSButton + + + docSetInfoTextView + NSTextView + + + docSetOutlineViewController + IDEDocSetOutlineViewController + + + downloadsTableViewController + IDEDocDownloadsTableViewController + + + minimumFontSizeControl + NSComboBox + + + noUpdatesAvailableMessage + NSTextField + + + showInfoButton + NSButton + + + subscriptionTextField + NSTextField + + + subscriptionWindow + NSWindow + + + validateAddSubscriptionButton + NSButton + + + + IBProjectSource + ./Classes/IDEDocViewingPrefPaneController.h + + + + IDEEditor + IDEViewController + + IBProjectSource + ./Classes/IDEEditor.h + + + + IDEViewController + DVTViewController + + IBProjectSource + ./Classes/IDEViewController.h + + + + IKImageView + + id + id + id + id + + + + copy: + id + + + crop: + id + + + cut: + id + + + paste: + id + + + + IBProjectSource + ./Classes/IKImageView.h + + + + NSDocument + + id + id + id + id + id + id + + + + printDocument: + id + + + revertDocumentToSaved: + id + + + runPageLayout: + id + + + saveDocument: + id + + + saveDocumentAs: + id + + + saveDocumentTo: + id + + + + IBProjectSource + ./Classes/NSDocument.h + + + + NSResponder + + _insertFindPattern: + id + + + _insertFindPattern: + + _insertFindPattern: + id + + + + IBProjectSource + ./Classes/NSResponder.h + + + + QLPreviewBubble + NSObject + + id + id + + + + hide: + id + + + show: + id + + + + parentWindow + NSWindow + + + parentWindow + + parentWindow + NSWindow + + + + IBProjectSource + ./Classes/QLPreviewBubble.h + + + + QTMovieView + + id + id + id + id + id + + + + showAll: + id + + + showCustomButton: + id + + + toggleLoops: + id + + + zoomIn: + id + + + zoomOut: + id + + + + IBProjectSource + ./Classes/QTMovieView.h + + + + WebView + + id + id + id + id + + + + reloadFromOrigin: + id + + + resetPageZoom: + id + + + zoomPageIn: + id + + + zoomPageOut: + id + + + + IBProjectSource + ./Classes/WebView.h + + + + XDDiagramView + NSView + + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + id + + + + _graphLayouterMenuItemAction: + id + + + _zoomPopUpButtonAction: + id + + + alignBottomEdges: + id + + + alignCentersHorizontallyInContainer: + id + + + alignCentersVerticallyInContainer: + id + + + alignHorizontalCenters: + id + + + alignLeftEdges: + id + + + alignRightEdges: + id + + + alignTopEdges: + id + + + alignVerticalCenters: + id + + + bringToFront: + id + + + collapseAllCompartments: + id + + + copy: + id + + + cut: + id + + + delete: + id + + + deleteBackward: + id + + + deleteForward: + id + + + deselectAll: + id + + + diagramZoomIn: + id + + + diagramZoomOut: + id + + + expandAllCompartments: + id + + + flipHorizontally: + id + + + flipVertically: + id + + + layoutGraphicsConcentrically: + id + + + layoutGraphicsHierarchically: + id + + + lock: + id + + + makeSameHeight: + id + + + makeSameWidth: + id + + + moveDown: + id + + + moveDownAndModifySelection: + id + + + moveLeft: + id + + + moveLeftAndModifySelection: + id + + + moveRight: + id + + + moveRightAndModifySelection: + id + + + moveUp: + id + + + moveUpAndModifySelection: + id + + + paste: + id + + + rollDownAllCompartments: + id + + + rollUpAllCompartments: + id + + + selectAll: + id + + + sendToBack: + id + + + sizeToFit: + id + + + toggleGridShown: + id + + + toggleHiddenGraphicsShown: + id + + + togglePageBreaksShown: + id + + + toggleRuler: + id + + + toggleSnapsToGrid: + id + + + unlock: + id + + + + _diagramController + IDEDataModelDiagramEditor + + + _diagramController + + _diagramController + IDEDataModelDiagramEditor + + + + IBProjectSource + ./Classes/XDDiagramView.h + + + + XDTableView + NSTableView + + showAllTableColumns: + id + + + showAllTableColumns: + + showAllTableColumns: + id + + + + IBProjectSource + ./Classes/XDTableView.h + + + + + 0 + IBCocoaFramework + YES + 3 + + {11, 11} + {10, 3} + + YES + + diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.pbxproj b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.pbxproj new file mode 100644 index 000000000..84c5a1827 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.pbxproj @@ -0,0 +1,430 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + 81993FC71B69AA940077D6B9 /* Bootstrap */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 81993FCA1B69AA950077D6B9 /* Build configuration list for PBXAggregateTarget "Bootstrap" */; + buildPhases = ( + 81993FCB1B69AA9F0077D6B9 /* ShellScript */, + ); + dependencies = ( + ); + name = Bootstrap; + productName = Bootstrap; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 814C3ACA1B69877600E307BB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AC61B69877600E307BB /* Main.storyboard */; }; + 814C3ACB1B69877600E307BB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AC81B69877600E307BB /* Images.xcassets */; }; + 81BA814B1A49DA1800E65899 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BA814A1A49DA1800E65899 /* AppDelegate.swift */; }; + 81BA814D1A49DA1800E65899 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BA814C1A49DA1800E65899 /* ViewController.swift */; }; + 81BA81701A49DB6800E65899 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BA816B1A49DB6800E65899 /* Bolts.framework */; }; + 81BA81711A49DB6800E65899 /* Parse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BA816C1A49DB6800E65899 /* Parse.framework */; }; + 81BA81771A49E0D500E65899 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BA81761A49E0D500E65899 /* libsqlite3.dylib */; }; + 81BA81791A49E0DB00E65899 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BA81781A49E0DB00E65899 /* AudioToolbox.framework */; }; + 81BA817B1A49E0E500E65899 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BA817A1A49E0E500E65899 /* SystemConfiguration.framework */; }; + 81BA817F1A49E0F000E65899 /* libstdc++.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BA817E1A49E0F000E65899 /* libstdc++.6.dylib */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 81993FCC1B69AAE40077D6B9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 81BA813D1A49DA1800E65899 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 81993FC71B69AA940077D6B9; + remoteInfo = Bootstrap; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 814C3AC71B69877600E307BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 814C3AC81B69877600E307BB /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 814C3AC91B69877600E307BB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 81BA81451A49DA1800E65899 /* ParseStarterProject-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ParseStarterProject-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 81BA814A1A49DA1800E65899 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 81BA814C1A49DA1800E65899 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 81BA816B1A49DB6800E65899 /* Bolts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Bolts.framework; sourceTree = ""; }; + 81BA816C1A49DB6800E65899 /* Parse.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Parse.framework; sourceTree = ""; }; + 81BA816D1A49DB6800E65899 /* ParseCrashReporting.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ParseCrashReporting.framework; sourceTree = ""; }; + 81BA816E1A49DB6800E65899 /* ParseFacebookUtils.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ParseFacebookUtils.framework; sourceTree = ""; }; + 81BA81761A49E0D500E65899 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; + 81BA81781A49E0DB00E65899 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 81BA817A1A49E0E500E65899 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 81BA817E1A49E0F000E65899 /* libstdc++.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libstdc++.6.dylib"; path = "usr/lib/libstdc++.6.dylib"; sourceTree = SDKROOT; }; + 81BA81801A49E10C00E65899 /* ParseUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ParseUI.framework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 81BA81421A49DA1800E65899 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 81BA817F1A49E0F000E65899 /* libstdc++.6.dylib in Frameworks */, + 81BA817B1A49E0E500E65899 /* SystemConfiguration.framework in Frameworks */, + 81BA81791A49E0DB00E65899 /* AudioToolbox.framework in Frameworks */, + 81BA81771A49E0D500E65899 /* libsqlite3.dylib in Frameworks */, + 81BA81711A49DB6800E65899 /* Parse.framework in Frameworks */, + 81BA81701A49DB6800E65899 /* Bolts.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 814C3AC51B69877600E307BB /* Resources */ = { + isa = PBXGroup; + children = ( + 814C3AC81B69877600E307BB /* Images.xcassets */, + 814C3AC91B69877600E307BB /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 81BA813C1A49DA1800E65899 = { + isa = PBXGroup; + children = ( + 81BA81471A49DA1800E65899 /* ParseStarterProject */, + 814C3AC51B69877600E307BB /* Resources */, + 81BA816A1A49DB5600E65899 /* Frameworks */, + 81BA81461A49DA1800E65899 /* Products */, + ); + sourceTree = ""; + }; + 81BA81461A49DA1800E65899 /* Products */ = { + isa = PBXGroup; + children = ( + 81BA81451A49DA1800E65899 /* ParseStarterProject-Swift.app */, + ); + name = Products; + sourceTree = ""; + }; + 81BA81471A49DA1800E65899 /* ParseStarterProject */ = { + isa = PBXGroup; + children = ( + 81BA814A1A49DA1800E65899 /* AppDelegate.swift */, + 81BA814C1A49DA1800E65899 /* ViewController.swift */, + 814C3AC61B69877600E307BB /* Main.storyboard */, + ); + path = ParseStarterProject; + sourceTree = ""; + }; + 81BA816A1A49DB5600E65899 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 81BA816B1A49DB6800E65899 /* Bolts.framework */, + 81BA816C1A49DB6800E65899 /* Parse.framework */, + 81BA816D1A49DB6800E65899 /* ParseCrashReporting.framework */, + 81BA816E1A49DB6800E65899 /* ParseFacebookUtils.framework */, + 81BA81801A49E10C00E65899 /* ParseUI.framework */, + 81BA81751A49E0C500E65899 /* System Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + 81BA81751A49E0C500E65899 /* System Frameworks */ = { + isa = PBXGroup; + children = ( + 81BA817E1A49E0F000E65899 /* libstdc++.6.dylib */, + 81BA817A1A49E0E500E65899 /* SystemConfiguration.framework */, + 81BA81781A49E0DB00E65899 /* AudioToolbox.framework */, + 81BA81761A49E0D500E65899 /* libsqlite3.dylib */, + ); + name = "System Frameworks"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 81BA81441A49DA1800E65899 /* ParseStarterProject-Swift */ = { + isa = PBXNativeTarget; + buildConfigurationList = 81BA81641A49DA1800E65899 /* Build configuration list for PBXNativeTarget "ParseStarterProject-Swift" */; + buildPhases = ( + 81BA81411A49DA1800E65899 /* Sources */, + 81BA81421A49DA1800E65899 /* Frameworks */, + 81BA81431A49DA1800E65899 /* Resources */, + 81CC85E11A49F6D40076DE19 /* Upload Symbol Files */, + ); + buildRules = ( + ); + dependencies = ( + 81993FCD1B69AAE40077D6B9 /* PBXTargetDependency */, + ); + name = "ParseStarterProject-Swift"; + productName = ParseStarterProject; + productReference = 81BA81451A49DA1800E65899 /* ParseStarterProject-Swift.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 81BA813D1A49DA1800E65899 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Parse; + TargetAttributes = { + 81993FC71B69AA940077D6B9 = { + CreatedOnToolsVersion = 6.4; + }; + 81BA81441A49DA1800E65899 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 81BA81401A49DA1800E65899 /* Build configuration list for PBXProject "ParseStarterProject-Swift" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 81BA813C1A49DA1800E65899; + productRefGroup = 81BA81461A49DA1800E65899 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 81BA81441A49DA1800E65899 /* ParseStarterProject-Swift */, + 81993FC71B69AA940077D6B9 /* Bootstrap */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 81BA81431A49DA1800E65899 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 814C3ACA1B69877600E307BB /* Main.storyboard in Resources */, + 814C3ACB1B69877600E307BB /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 81993FCB1B69AA9F0077D6B9 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ ! -d \"Bolts.framework\" ]]; then\n cp -R ../../../Vendor/Bolts-ObjC/build/ios/Bolts.framework .\nfi\n"; + }; + 81CC85E11A49F6D40076DE19 /* Upload Symbol Files */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Symbol Files"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script will upload symbol files for your application,\n# so all your crash reports are going to be symbolicated\n\n# Important!\n# Before using the script, please initialize CloudCode folder and replace with the path to it\n#\n# Read more on Parse.com - https://parse.com/apps/quickstart#analytics/crashreporting/ios/\n############################################################\n\n\n# export PATH=/usr/local/bin:$PATH\n# cd \n\n# parse symbols -p \"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 81BA81411A49DA1800E65899 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81BA814D1A49DA1800E65899 /* ViewController.swift in Sources */, + 81BA814B1A49DA1800E65899 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 81993FCD1B69AAE40077D6B9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 81993FC71B69AA940077D6B9 /* Bootstrap */; + targetProxy = 81993FCC1B69AAE40077D6B9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 814C3AC61B69877600E307BB /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 814C3AC71B69877600E307BB /* Base */, + ); + name = Main.storyboard; + path = ../Resources; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 81993FC81B69AA950077D6B9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 81993FC91B69AA950077D6B9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 81BA81621A49DA1800E65899 /* 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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = 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 = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 81BA81631A49DA1800E65899 /* 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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + 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 = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 81BA81651A49DA1800E65899 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + INFOPLIST_FILE = Resources/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "ParseStarterProject-Swift"; + }; + name = Debug; + }; + 81BA81661A49DA1800E65899 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + INFOPLIST_FILE = Resources/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "ParseStarterProject-Swift"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 81993FCA1B69AA950077D6B9 /* Build configuration list for PBXAggregateTarget "Bootstrap" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81993FC81B69AA950077D6B9 /* Debug */, + 81993FC91B69AA950077D6B9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; + 81BA81401A49DA1800E65899 /* Build configuration list for PBXProject "ParseStarterProject-Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81BA81621A49DA1800E65899 /* Debug */, + 81BA81631A49DA1800E65899 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 81BA81641A49DA1800E65899 /* Build configuration list for PBXNativeTarget "ParseStarterProject-Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 81BA81651A49DA1800E65899 /* Debug */, + 81BA81661A49DA1800E65899 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 81BA813D1A49DA1800E65899 /* Project object */; +} diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..b162c5ad7 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseStarterProject-Swift.xcscheme b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseStarterProject-Swift.xcscheme new file mode 100644 index 000000000..83aa47616 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject-Swift.xcodeproj/xcshareddata/xcschemes/ParseStarterProject-Swift.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/AppDelegate.swift b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/AppDelegate.swift new file mode 100644 index 000000000..63f3324ae --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/AppDelegate.swift @@ -0,0 +1,137 @@ +/** +* Copyright (c) 2015-present, Parse, LLC. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +*/ + +import UIKit + +import Bolts +import Parse + +// If you want to use any of the UI components, uncomment this line +// import ParseUI + +// If you want to use Crash Reporting - uncomment this line +// import ParseCrashReporting + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + //-------------------------------------- + // MARK: - UIApplicationDelegate + //-------------------------------------- + + func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + // Enable storing and querying data from Local Datastore. + // Remove this line if you don't want to use Local Datastore features or want to use cachePolicy. + Parse.enableLocalDatastore() + + // **************************************************************************** + // Uncomment this line if you want to enable Crash Reporting + // ParseCrashReporting.enable() + // + // Uncomment and fill in with your Parse credentials: + // Parse.setApplicationId("your_application_id", clientKey: "your_client_key") + // + // If you are using Facebook, uncomment and add your FacebookAppID to your bundle's plist as + // described here: https://developers.facebook.com/docs/getting-started/facebook-sdk-for-ios/ + // Uncomment the line inside ParseStartProject-Bridging-Header and the following line here: + // PFFacebookUtils.initializeFacebook() + // **************************************************************************** + + PFUser.enableAutomaticUser() + + let defaultACL = PFACL(); + + // If you would like all objects to be private by default, remove this line. + defaultACL.setPublicReadAccess(true) + + PFACL.setDefaultACL(defaultACL, withAccessForCurrentUser:true) + + if application.applicationState != UIApplicationState.Background { + // Track an app open here if we launch with a push, unless + // "content_available" was used to trigger a background push (introduced in iOS 7). + // In that case, we skip tracking here to avoid double counting the app-open. + + let preBackgroundPush = !application.respondsToSelector("backgroundRefreshStatus") + let oldPushHandlerOnly = !self.respondsToSelector("application:didReceiveRemoteNotification:fetchCompletionHandler:") + var noPushPayload = false; + if let options = launchOptions { + noPushPayload = options[UIApplicationLaunchOptionsRemoteNotificationKey] != nil; + } + if (preBackgroundPush || oldPushHandlerOnly || noPushPayload) { + PFAnalytics.trackAppOpenedWithLaunchOptions(launchOptions) + } + } + if application.respondsToSelector("registerUserNotificationSettings:") { + let userNotificationTypes = UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound + let settings = UIUserNotificationSettings(forTypes: userNotificationTypes, categories: nil) + application.registerUserNotificationSettings(settings) + application.registerForRemoteNotifications() + } else { + let types = UIRemoteNotificationType.Badge | UIRemoteNotificationType.Alert | UIRemoteNotificationType.Sound + application.registerForRemoteNotificationTypes(types) + } + + return true + } + + //-------------------------------------- + // MARK: Push Notifications + //-------------------------------------- + + func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) { + let installation = PFInstallation.currentInstallation() + installation.setDeviceTokenFromData(deviceToken) + installation.saveInBackground() + + PFPush.subscribeToChannelInBackground("") { (succeeded: Bool, error: NSError?) in + if succeeded { + println("ParseStarterProject successfully subscribed to push notifications on the broadcast channel."); + } else { + println("ParseStarterProject failed to subscribe to push notifications on the broadcast channel with error = %@.", error) + } + } + } + + func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) { + if error.code == 3010 { + println("Push notifications are not supported in the iOS Simulator.") + } else { + println("application:didFailToRegisterForRemoteNotificationsWithError: %@", error) + } + } + + func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) { + PFPush.handlePush(userInfo) + if application.applicationState == UIApplicationState.Inactive { + PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo) + } + } + + /////////////////////////////////////////////////////////// + // Uncomment this method if you want to use Push Notifications with Background App Refresh + /////////////////////////////////////////////////////////// + // func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) { + // if application.applicationState == UIApplicationState.Inactive { + // PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo) + // } + // } + + //-------------------------------------- + // MARK: Facebook SDK Integration + //-------------------------------------- + + /////////////////////////////////////////////////////////// + // Uncomment this method if you are using Facebook + /////////////////////////////////////////////////////////// + // func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject?) -> Bool { + // return FBAppCall.handleOpenURL(url, sourceApplication:sourceApplication, session:PFFacebookUtils.session()) + // } +} diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/ViewController.swift b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/ViewController.swift new file mode 100644 index 000000000..92689cef6 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/ParseStarterProject/ViewController.swift @@ -0,0 +1,24 @@ +/** +* Copyright (c) 2015-present, Parse, LLC. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +*/ + +import UIKit +import Parse + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } +} diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Base.lproj/Main.storyboard b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Base.lproj/Main.storyboard new file mode 100644 index 000000000..3a2a49bad --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Base.lproj/Main.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..36d2c80d8 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "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" : "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" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/LaunchImage.launchimage/Contents.json b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 000000000..5a2966687 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,49 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "extent" : "full-screen", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "extent" : "full-screen", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "extent" : "full-screen", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Info.plist b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Info.plist new file mode 100644 index 000000000..f6782fc13 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject-Swift/Resources/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.parse.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.8.0 + CFBundleSignature + ???? + CFBundleVersion + 1.8.0 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.pbxproj b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.pbxproj new file mode 100644 index 000000000..50ba6d122 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.pbxproj @@ -0,0 +1,463 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + 814C3AE61B69A87F00E307BB /* Bootstrap */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 814C3AE91B69A87F00E307BB /* Build configuration list for PBXAggregateTarget "Bootstrap" */; + buildPhases = ( + 814C3AEA1B69A88300E307BB /* ShellScript */, + ); + dependencies = ( + ); + name = Bootstrap; + productName = Bootstrap; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 092354B313A1D7EB00DA740F /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 092354B213A1D7EB00DA740F /* CFNetwork.framework */; }; + 092354B513A1D7F000DA740F /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 092354B413A1D7F000DA740F /* SystemConfiguration.framework */; }; + 095ACE8613C68EA300566243 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 095ACE8513C68EA300566243 /* AudioToolbox.framework */; }; + 099CCEAD13F9E3760039A464 /* libz.1.1.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 099CCEAC13F9E3760039A464 /* libz.1.1.3.dylib */; }; + 09ABC07213A1D52D009C3FCF /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09ABC07113A1D52D009C3FCF /* UIKit.framework */; }; + 09ABC07413A1D52D009C3FCF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09ABC07313A1D52D009C3FCF /* Foundation.framework */; }; + 09ABC07F13A1D52D009C3FCF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 09ABC07E13A1D52D009C3FCF /* main.m */; }; + 09ABC08213A1D52D009C3FCF /* ParseStarterProjectAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 09ABC08113A1D52D009C3FCF /* ParseStarterProjectAppDelegate.m */; }; + 09ABC08813A1D52E009C3FCF /* ParseStarterProjectViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09ABC08713A1D52E009C3FCF /* ParseStarterProjectViewController.m */; }; + 09BEF34C13D51C3F001BBCDB /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09BEF34B13D51C3F001BBCDB /* Security.framework */; }; + 2FCDD6B014A573F500295AAC /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCDD6AF14A573F500295AAC /* QuartzCore.framework */; }; + 4998650515BF305000803E05 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4998650415BF305000803E05 /* StoreKit.framework */; }; + 743D7B6D157DA60100084B67 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 743D7B6C157DA60100084B67 /* CoreGraphics.framework */; }; + 814C3ABD1B69864600E307BB /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AB21B69864600E307BB /* Default-568h@2x.png */; }; + 814C3ABE1B69864600E307BB /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AB31B69864600E307BB /* Default.png */; }; + 814C3ABF1B69864600E307BB /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AB41B69864600E307BB /* Default@2x.png */; }; + 814C3AC01B69864600E307BB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AB51B69864600E307BB /* InfoPlist.strings */; }; + 814C3AC11B69864600E307BB /* MainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AB71B69864600E307BB /* MainWindow.xib */; }; + 814C3AC21B69864600E307BB /* ParseStarterProjectViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 814C3AB91B69864600E307BB /* ParseStarterProjectViewController.xib */; }; + 817AD67C196B009E0014C796 /* Bolts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 817AD67B196B009E0014C796 /* Bolts.framework */; }; + 81A6CA611A2EA82800297C39 /* libstdc++.6.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 81A6CA601A2EA82800297C39 /* libstdc++.6.dylib */; }; + 81AFA6791B0EDD12000763C0 /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 6332375315E438E900AE2736 /* libsqlite3.dylib */; }; + 81DDFD1919B4A60300BE649C /* Parse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81DDFD1719B4A60300BE649C /* Parse.framework */; }; + DD952FCB16E7F5CF00470144 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97E18B1E1624CB5700B17A67 /* CoreLocation.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 814C3AEB1B69A8B000E307BB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 09ABC06413A1D52D009C3FCF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 814C3AE61B69A87F00E307BB; + remoteInfo = Bootstrap; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 092354B213A1D7EB00DA740F /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 092354B413A1D7F000DA740F /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 095ACE8513C68EA300566243 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 099CCEAC13F9E3760039A464 /* libz.1.1.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.1.1.3.dylib; path = usr/lib/libz.1.1.3.dylib; sourceTree = SDKROOT; }; + 09ABC06D13A1D52D009C3FCF /* ParseStarterProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ParseStarterProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 09ABC07113A1D52D009C3FCF /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 09ABC07313A1D52D009C3FCF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 09ABC07E13A1D52D009C3FCF /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 09ABC08013A1D52D009C3FCF /* ParseStarterProjectAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ParseStarterProjectAppDelegate.h; sourceTree = ""; }; + 09ABC08113A1D52D009C3FCF /* ParseStarterProjectAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ParseStarterProjectAppDelegate.m; sourceTree = ""; }; + 09ABC08613A1D52E009C3FCF /* ParseStarterProjectViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ParseStarterProjectViewController.h; sourceTree = ""; }; + 09ABC08713A1D52E009C3FCF /* ParseStarterProjectViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ParseStarterProjectViewController.m; sourceTree = ""; }; + 09BEF34B13D51C3F001BBCDB /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 2FCDD6AF14A573F500295AAC /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 4998650415BF305000803E05 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 6332375315E438E900AE2736 /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; }; + 6372FF521613CC3C002132AF /* Accounts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accounts.framework; path = System/Library/Frameworks/Accounts.framework; sourceTree = SDKROOT; }; + 6372FF551613CC42002132AF /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; + 6372FF571613CC47002132AF /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; + 743D7B6C157DA60100084B67 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 814C3AB21B69864600E307BB /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 814C3AB31B69864600E307BB /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; + 814C3AB41B69864600E307BB /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; + 814C3AB61B69864600E307BB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 814C3AB81B69864600E307BB /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainWindow.xib; sourceTree = ""; }; + 814C3ABA1B69864600E307BB /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ParseStarterProjectViewController.xib; sourceTree = ""; }; + 814C3ABC1B69864600E307BB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 817AD67B196B009E0014C796 /* Bolts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Bolts.framework; sourceTree = ""; }; + 81A6CA601A2EA82800297C39 /* libstdc++.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libstdc++.6.dylib"; path = "usr/lib/libstdc++.6.dylib"; sourceTree = SDKROOT; }; + 81DDFD1719B4A60300BE649C /* Parse.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Parse.framework; sourceTree = ""; }; + 97E18B1E1624CB5700B17A67 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 09ABC06A13A1D52D009C3FCF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 81A6CA611A2EA82800297C39 /* libstdc++.6.dylib in Frameworks */, + DD952FCB16E7F5CF00470144 /* CoreLocation.framework in Frameworks */, + 4998650515BF305000803E05 /* StoreKit.framework in Frameworks */, + 743D7B6D157DA60100084B67 /* CoreGraphics.framework in Frameworks */, + 81AFA6791B0EDD12000763C0 /* libsqlite3.dylib in Frameworks */, + 2FCDD6B014A573F500295AAC /* QuartzCore.framework in Frameworks */, + 099CCEAD13F9E3760039A464 /* libz.1.1.3.dylib in Frameworks */, + 09BEF34C13D51C3F001BBCDB /* Security.framework in Frameworks */, + 095ACE8613C68EA300566243 /* AudioToolbox.framework in Frameworks */, + 092354B513A1D7F000DA740F /* SystemConfiguration.framework in Frameworks */, + 092354B313A1D7EB00DA740F /* CFNetwork.framework in Frameworks */, + 81DDFD1919B4A60300BE649C /* Parse.framework in Frameworks */, + 09ABC07213A1D52D009C3FCF /* UIKit.framework in Frameworks */, + 09ABC07413A1D52D009C3FCF /* Foundation.framework in Frameworks */, + 817AD67C196B009E0014C796 /* Bolts.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 09ABC06213A1D52D009C3FCF = { + isa = PBXGroup; + children = ( + 09ABC07713A1D52D009C3FCF /* ParseStarterProject */, + 814C3AB11B69864600E307BB /* Resources */, + 09ABC07013A1D52D009C3FCF /* Frameworks */, + 09ABC06E13A1D52D009C3FCF /* Products */, + ); + sourceTree = ""; + }; + 09ABC06E13A1D52D009C3FCF /* Products */ = { + isa = PBXGroup; + children = ( + 09ABC06D13A1D52D009C3FCF /* ParseStarterProject.app */, + ); + name = Products; + sourceTree = ""; + }; + 09ABC07013A1D52D009C3FCF /* Frameworks */ = { + isa = PBXGroup; + children = ( + 81DDFD1719B4A60300BE649C /* Parse.framework */, + 817AD67B196B009E0014C796 /* Bolts.framework */, + 81A6CA5E1A2EA0B800297C39 /* System Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; + 09ABC07713A1D52D009C3FCF /* ParseStarterProject */ = { + isa = PBXGroup; + children = ( + 09ABC08013A1D52D009C3FCF /* ParseStarterProjectAppDelegate.h */, + 09ABC08113A1D52D009C3FCF /* ParseStarterProjectAppDelegate.m */, + 814C3AB71B69864600E307BB /* MainWindow.xib */, + 09ABC08613A1D52E009C3FCF /* ParseStarterProjectViewController.h */, + 09ABC08713A1D52E009C3FCF /* ParseStarterProjectViewController.m */, + 814C3AB91B69864600E307BB /* ParseStarterProjectViewController.xib */, + 09ABC07813A1D52D009C3FCF /* Other Sources */, + ); + path = ParseStarterProject; + sourceTree = ""; + }; + 09ABC07813A1D52D009C3FCF /* Other Sources */ = { + isa = PBXGroup; + children = ( + 09ABC07E13A1D52D009C3FCF /* main.m */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + 814C3AB11B69864600E307BB /* Resources */ = { + isa = PBXGroup; + children = ( + 814C3AB21B69864600E307BB /* Default-568h@2x.png */, + 814C3AB31B69864600E307BB /* Default.png */, + 814C3AB41B69864600E307BB /* Default@2x.png */, + 814C3AB51B69864600E307BB /* InfoPlist.strings */, + 814C3ABC1B69864600E307BB /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 81A6CA5E1A2EA0B800297C39 /* System Frameworks */ = { + isa = PBXGroup; + children = ( + 6372FF571613CC47002132AF /* AdSupport.framework */, + 6372FF551613CC42002132AF /* Social.framework */, + 6372FF521613CC3C002132AF /* Accounts.framework */, + 095ACE8513C68EA300566243 /* AudioToolbox.framework */, + 092354B213A1D7EB00DA740F /* CFNetwork.framework */, + 743D7B6C157DA60100084B67 /* CoreGraphics.framework */, + 97E18B1E1624CB5700B17A67 /* CoreLocation.framework */, + 09ABC07313A1D52D009C3FCF /* Foundation.framework */, + 6332375315E438E900AE2736 /* libsqlite3.dylib */, + 099CCEAC13F9E3760039A464 /* libz.1.1.3.dylib */, + 81A6CA601A2EA82800297C39 /* libstdc++.6.dylib */, + 2FCDD6AF14A573F500295AAC /* QuartzCore.framework */, + 09BEF34B13D51C3F001BBCDB /* Security.framework */, + 4998650415BF305000803E05 /* StoreKit.framework */, + 092354B413A1D7F000DA740F /* SystemConfiguration.framework */, + 09ABC07113A1D52D009C3FCF /* UIKit.framework */, + ); + name = "System Frameworks"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 09ABC06C13A1D52D009C3FCF /* ParseStarterProject */ = { + isa = PBXNativeTarget; + buildConfigurationList = 09ABC08E13A1D52E009C3FCF /* Build configuration list for PBXNativeTarget "ParseStarterProject" */; + buildPhases = ( + 09ABC06913A1D52D009C3FCF /* Sources */, + 09ABC06A13A1D52D009C3FCF /* Frameworks */, + 09ABC06B13A1D52D009C3FCF /* Resources */, + 81A6CA5F1A2EA3D600297C39 /* Upload Symbol Files */, + ); + buildRules = ( + ); + dependencies = ( + 814C3AEC1B69A8B000E307BB /* PBXTargetDependency */, + ); + name = ParseStarterProject; + productName = ParseStarterProject; + productReference = 09ABC06D13A1D52D009C3FCF /* ParseStarterProject.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 09ABC06413A1D52D009C3FCF /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0510; + TargetAttributes = { + 814C3AE61B69A87F00E307BB = { + CreatedOnToolsVersion = 6.4; + }; + }; + }; + buildConfigurationList = 09ABC06713A1D52D009C3FCF /* Build configuration list for PBXProject "ParseStarterProject" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 09ABC06213A1D52D009C3FCF; + productRefGroup = 09ABC06E13A1D52D009C3FCF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 09ABC06C13A1D52D009C3FCF /* ParseStarterProject */, + 814C3AE61B69A87F00E307BB /* Bootstrap */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 09ABC06B13A1D52D009C3FCF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 814C3ABE1B69864600E307BB /* Default.png in Resources */, + 814C3AC21B69864600E307BB /* ParseStarterProjectViewController.xib in Resources */, + 814C3ABF1B69864600E307BB /* Default@2x.png in Resources */, + 814C3ABD1B69864600E307BB /* Default-568h@2x.png in Resources */, + 814C3AC11B69864600E307BB /* MainWindow.xib in Resources */, + 814C3AC01B69864600E307BB /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 814C3AEA1B69A88300E307BB /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ ! -d \"Bolts.framework\" ]]; then\n cp -R ../../../Vendor/Bolts-ObjC/build/ios/Bolts.framework .\nfi\n"; + }; + 81A6CA5F1A2EA3D600297C39 /* Upload Symbol Files */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Symbol Files"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script will upload symbol files for your application,\n# so all your crash reports are going to be symbolicated\n\n# Important!\n# Before using the script, please initialize CloudCode folder and replace with the path to it\n#\n# Read more on Parse.com - https://parse.com/apps/quickstart#analytics/crashreporting/ios/\n############################################################\n\n\n# export PATH=/usr/local/bin:$PATH\n# cd \n\n# parse symbols -p \"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 09ABC06913A1D52D009C3FCF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 09ABC07F13A1D52D009C3FCF /* main.m in Sources */, + 09ABC08213A1D52D009C3FCF /* ParseStarterProjectAppDelegate.m in Sources */, + 09ABC08813A1D52E009C3FCF /* ParseStarterProjectViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 814C3AEC1B69A8B000E307BB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 814C3AE61B69A87F00E307BB /* Bootstrap */; + targetProxy = 814C3AEB1B69A8B000E307BB /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 814C3AB51B69864600E307BB /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 814C3AB61B69864600E307BB /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 814C3AB71B69864600E307BB /* MainWindow.xib */ = { + isa = PBXVariantGroup; + children = ( + 814C3AB81B69864600E307BB /* en */, + ); + name = MainWindow.xib; + path = ../Resources; + sourceTree = ""; + }; + 814C3AB91B69864600E307BB /* ParseStarterProjectViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 814C3ABA1B69864600E307BB /* en */, + ); + name = ParseStarterProjectViewController.xib; + path = ../Resources; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 09ABC08C13A1D52E009C3FCF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = DEBUG; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 09ABC08D13A1D52E009C3FCF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + SDKROOT = iphoneos; + }; + name = Release; + }; + 09ABC08F13A1D52E009C3FCF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + "$(PROJECT_DIR)", + ); + GCC_DYNAMIC_NO_PIC = NO; + INFOPLIST_FILE = Resources/Info.plist; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 09ABC09013A1D52E009C3FCF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)", + "$(PROJECT_DIR)", + ); + INFOPLIST_FILE = Resources/Info.plist; + PRODUCT_NAME = "$(TARGET_NAME)"; + VALIDATE_PRODUCT = YES; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + 814C3AE71B69A87F00E307BB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 814C3AE81B69A87F00E307BB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 09ABC06713A1D52D009C3FCF /* Build configuration list for PBXProject "ParseStarterProject" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09ABC08C13A1D52E009C3FCF /* Debug */, + 09ABC08D13A1D52E009C3FCF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 09ABC08E13A1D52E009C3FCF /* Build configuration list for PBXNativeTarget "ParseStarterProject" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09ABC08F13A1D52E009C3FCF /* Debug */, + 09ABC09013A1D52E009C3FCF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 814C3AE91B69A87F00E307BB /* Build configuration list for PBXAggregateTarget "Bootstrap" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 814C3AE71B69A87F00E307BB /* Debug */, + 814C3AE81B69A87F00E307BB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 09ABC06413A1D52D009C3FCF /* Project object */; +} diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..0fc58ae10 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/xcshareddata/xcschemes/ParseStarterProject.xcscheme b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/xcshareddata/xcschemes/ParseStarterProject.xcscheme new file mode 100644 index 000000000..97728ce13 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject.xcodeproj/xcshareddata/xcschemes/ParseStarterProject.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.h b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.h new file mode 100644 index 000000000..9e820084d --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class ParseStarterProjectViewController; + +@interface ParseStarterProjectAppDelegate : NSObject + +@property (nonatomic, strong) IBOutlet UIWindow *window; + +@property (nonatomic, strong) IBOutlet ParseStarterProjectViewController *viewController; + +@end diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.m b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.m new file mode 100644 index 000000000..bf5f9f22f --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectAppDelegate.m @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +// If you want to use any of the UI components, uncomment this line +// #import + +// If you are using Facebook, uncomment this line +// #import + +// If you want to use Crash Reporting - uncomment this line +// #import + +#import "ParseStarterProjectAppDelegate.h" +#import "ParseStarterProjectViewController.h" + +@implementation ParseStarterProjectAppDelegate + +#pragma mark - +#pragma mark UIApplicationDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Enable storing and querying data from Local Datastore. Remove this line if you don't want to + // use Local Datastore features or want to use cachePolicy. + [Parse enableLocalDatastore]; + + // **************************************************************************** + // Uncomment this line if you want to enable Crash Reporting + // [ParseCrashReporting enable]; + // + // Uncomment and fill in with your Parse credentials: + // [Parse setApplicationId:@"your_application_id" clientKey:@"your_client_key"]; + // + // If you are using Facebook, uncomment and add your FacebookAppID to your bundle's plist as + // described here: https://developers.facebook.com/docs/getting-started/facebook-sdk-for-ios/ + // [PFFacebookUtils initializeFacebook]; + // **************************************************************************** + + [PFUser enableAutomaticUser]; + + PFACL *defaultACL = [PFACL ACL]; + + // If you would like all objects to be private by default, remove this line. + [defaultACL setPublicReadAccess:YES]; + + [PFACL setDefaultACL:defaultACL withAccessForCurrentUser:YES]; + + // Override point for customization after application launch. + + self.window.rootViewController = self.viewController; + [self.window makeKeyAndVisible]; + + if (application.applicationState != UIApplicationStateBackground) { + // Track an app open here if we launch with a push, unless + // "content_available" was used to trigger a background push (introduced in iOS 7). + // In that case, we skip tracking here to avoid double counting the app-open. + BOOL preBackgroundPush = ![application respondsToSelector:@selector(backgroundRefreshStatus)]; + BOOL oldPushHandlerOnly = ![self respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]; + BOOL noPushPayload = ![launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; + if (preBackgroundPush || oldPushHandlerOnly || noPushPayload) { + [PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions]; + } + } + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + if ([application respondsToSelector:@selector(registerUserNotificationSettings:)]) { + UIUserNotificationType userNotificationTypes = (UIUserNotificationTypeAlert | + UIUserNotificationTypeBadge | + UIUserNotificationTypeSound); + UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes + categories:nil]; + [application registerUserNotificationSettings:settings]; + [application registerForRemoteNotifications]; + } else +#endif + { + [application registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | + UIRemoteNotificationTypeAlert | + UIRemoteNotificationTypeSound)]; + } + + return YES; +} + +#pragma mark Push Notifications + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + PFInstallation *currentInstallation = [PFInstallation currentInstallation]; + [currentInstallation setDeviceTokenFromData:deviceToken]; + [currentInstallation saveInBackground]; + + [PFPush subscribeToChannelInBackground:@"" block:^(BOOL succeeded, NSError *error) { + if (succeeded) { + NSLog(@"ParseStarterProject successfully subscribed to push notifications on the broadcast channel."); + } else { + NSLog(@"ParseStarterProject failed to subscribe to push notifications on the broadcast channel."); + } + }]; +} + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { + if (error.code == 3010) { + NSLog(@"Push notifications are not supported in the iOS Simulator."); + } else { + // show some alert or otherwise handle the failure to register. + NSLog(@"application:didFailToRegisterForRemoteNotificationsWithError: %@", error); + } +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { + [PFPush handlePush:userInfo]; + + if (application.applicationState == UIApplicationStateInactive) { + [PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo]; + } +} + +/////////////////////////////////////////////////////////// +// Uncomment this method if you want to use Push Notifications with Background App Refresh +/////////////////////////////////////////////////////////// +//- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { +// if (application.applicationState == UIApplicationStateInactive) { +// [PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo]; +// } +//} + +#pragma mark Facebook SDK Integration + +/////////////////////////////////////////////////////////// +// Uncomment this method if you are using Facebook +/////////////////////////////////////////////////////////// +//- (BOOL)application:(UIApplication *)application +// openURL:(NSURL *)url +// sourceApplication:(NSString *)sourceApplication +// annotation:(id)annotation { +// return [PFFacebookUtils handleOpenURL:url]; +//} + +@end diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.h b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.h new file mode 100644 index 000000000..21464ce88 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface ParseStarterProjectViewController : UIViewController + +@end diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.m b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.m new file mode 100644 index 000000000..4a2382c54 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/ParseStarterProjectViewController.m @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ParseStarterProjectViewController.h" + +#import + +@implementation ParseStarterProjectViewController + +#pragma mark - +#pragma mark UIViewController + +// Implement viewDidLoad to do additional setup after loading the view, typically from a nib. +- (void)viewDidLoad { + [super viewDidLoad]; +} + +- (void)didReceiveMemoryWarning { + // Releases the view if it doesn't have a superview. + [super didReceiveMemoryWarning]; + + // Release any cached data, images, etc that aren't in use. +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + // Return YES for supported orientations + return (interfaceOrientation == UIInterfaceOrientationPortrait); +} + +@end diff --git a/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/main.m b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/main.m new file mode 100644 index 000000000..c7b07d661 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/ParseStarterProject/main.m @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + +#import + +#import "ParseStarterProjectAppDelegate.h" + +int main(int argc, char *argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([ParseStarterProjectAppDelegate class])); + } +} diff --git a/ParseStarterProject/iOS/ParseStarterProject/Resources/Default-568h@2x.png b/ParseStarterProject/iOS/ParseStarterProject/Resources/Default-568h@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f17cfa3980ac8595fb8aef0b710219afbafa9ffe GIT binary patch literal 17091 zcmeHOXc<5Cz1_A|MnM2B=O2%gAD17J=f-v>)c<!AxbJ`8z4zP% z4>!m4Dtamq1g&>=qIp3OS_DDzTqO*`IiCC!`IR?dIyiVZJ2+rHc${!%Bm;tWZ#?Zg z%o*_6WShY8%G9>Q`a7OUyi?@seV|fbub>&@oSHSgPdjU(*Ti59wn)g;R~(oq%X5!E zX@OZl+4sq*IazOCj|q+MdtHCi+53!1p0iW@c$3<8qiF@-k*e5GWACQG**EQ$!Z zpHBI4$?Y}1?s(GUt^}NWJzHz5aPyq1=iQ!&GM7ztit}^Ka?ImbTV6gemq+KLzY$*6 z5OlQ|+qiAFQ~t8kWKyd@sTyNl-5X9Srw7aFwx*8wvh{D{!c$5MzNYi;kC2k1$|-Nv zh?in!rh-26pTC?{8CIc|Fl74Lr#8Xv(XE*+FI|d*iz=PzpD4^Fzv(`K7HspJ$^BZ$ zAA0T>99*zz2s5M*-^I;d=Q@8hh7!L;x1?*7M7p4Vxu3MyDv^tpF`T?uzeB-%o?Hg{#ez&<@dtYgr(k6d$8|tkJ7Q;oYv^y5h2EQv2hv5J~UVl29Cvm+wxrZ!l&VXEftP1i<5@E4+9{bewT)nZ=EZZ2z_>n$oFEU>)L zRw2g|FMQYbGPY4!fZu8phUEJo?ED1xM8hdys0wy52@K>H7}=>Uw5c3ZmrSftCSvfy%kUR zm%Q#yHABo5%%f(XJE!vb+5rPsQ?GlOw>J0A;Im@`FF(DID1g%wjhzx+B)K=WjGCd# z)2rtW&KVV7mz!Nm)g;646V@O18@8H_UV)?h_RgsAFLibVeT*62kl~yk`@MOa)<=Cs zc|{*6pJ=O~20e5;BUn=5ZA@jY-!GYCpq$IlgCTzIJcypepOf1GF=58?7e& zjvUC*6i%zp*6HtrISzgZ%O?f#_%7`^Ge@ zZ5DcO3oZ1lf^l!PyBM$R0-z{Cq`s*p0vO6%r+{b(T8BKZ(?mgOmvto|;^WWvcij(% zaact9Ax{zG+^Xe%qHQm5*2=g4bI=y1M9%y zF|fOc6e1O8tAfR1t$BySVK3T;GINBo!5!iAxiED#mcI6ymh99HR`3zesnL;IyEeTX}ABs6lwuE<)tx0QiR(wjV z9e4~npTqOva3XCzm;y#5OH5*^M2ggMwOnRuO%lhjDke$7#NAoL|E*a0%UW{l?WQ;`Lf+{mu&0K5Xz+rKDjOb{wgIzNH z)x5R(EA9KTnf&EGtzoaV@?VkLE_Z7+Q)cjgOEyEm{4dECOWsemmAA!Im!A&0yO;2jb} z695GSQUNFcP>{}_02BZy$iD3WC;(6Zpa7N+V1b7PD^_Gq084Q3h6UJDK;{In>w|1D z0Z;&-Alpv_pa4KYx-$(x0e}Jk1#mtjuLKUfkZ&Y#f``B%IFb}EoWP+m@{I&Q0f2(E z907m=00mGQ18RLh@e*1dl$RlM0;rcm*3F;*5L7fGD<*OMCjbQi3exfe015yU04RXU zdQi75**X9vn&J-xP`3^0wx!pEz-10_nL~Et3xEOu1po@aX-wqM6>&y>YB4)1;;ojbLbbV-W^iFB1wa3^zCog^LCAReC4K0-?R_2{6 zrP*)4+_uWUy3w5N52M3PW_}MFMP9a~>YLvVZ1D_k*IMQ2QT^fwzoOb(*3gH$%aYWC zkHmcab=va2<#X%jakpJ;<1@F;k__#bwtC&%^D0v(FBh9K&$sK+<}2RJS609D)17$w ztdQP8(eLM8Ka}m_IQ@3wyMKP)l=oM4-?`YS_*P?4V_ORLPxsj&7Ju#kH;>6^Kp?T7~ zl+q?{UOOqV==?+d{=)5s|M~T1mwtH@+Z^$G&eEO9JNP^AX@3jZ*J*!!>lc|1-W%fA z@AOQpXZ_Lt>rxFXrGp*zLPiW@uo_c7C{As>j zWeX)wi+LTp_)@KYZCX{j;H?|1yXT4DnlS(Fr8gyP5|uaX_gLvaW0ScZdnG7o+u{T6 zFI-%d{ls*WuCDa5UJ@|RXv&ejZe}*BMkiWY51&pnRPw(hlykSzvj6e%mYz-GdvzBD zF10?szF_~!jS=?2HyQuPCvARXAe}C}WP|yQ*>5~~=*Nxq8+HHW1~FMDRCP^TcacKuk$ z(U#REVv)D!PhJ*ecH-ELFUrfyV&*)Z)>UCOuS?yd^L@Afk>ihynYPc{^CRwu+JHX+#$@YsC4c|l0tGigsn@jy) zXD($Ouk>H+V(Mr6NQT0S9BFM~V6nkj;1OBOz`zY;a|<&v%$g$sE<{2iN+NuHtdjF{ z^%7I^lT!66atnZ}85nFTtboki)RIJnirk#MVyg;UC9n!BAR8pCucQE0Qj%?}6yY17 z;GAESs$i;TsAr^P$EBd4U{jQmW)z9|8>y;bpKhp88yV>WRp=I1=9MH?=;jqGLkxkL znOgw2D6bgmE1>`MD-sLz4fPE4;U)t$+5r7%<(r?9nO5nNSdwaIWMF8dYhbBsWENs* zW@TV$WolrfkERA;Cs?<0QEFmIeo;t%evVy0W<_dFE{LmOq-O}xi7XG*YNHSG1CpP> z0S0monm8f9mSmGf<&$Si!xJzp$rO3JD?je#E|?4mWvEZEzZv=1*Dpa=tbRDkF3+1fMgw2d^{1RZT`k9I4tcA$3f9%0{ev_H`pH0a~$>gTe~DWM4f D>S|f?5q$_l%JNF zlghwgA=nyvKmT@!h+SuX&=N6wg$VcduA}QigQIs!ORYV|<~e!kTQ9MtFK=k6Jh3wG zTq(_d`{lM({?(11++CKW z9(*L`>#x(6_bs1WKaacZVjG{ib(Umk$F$Y!=AKuXI(fO+lzF~gmo{JVX1=oerJwG^ z(`SYB4vKz1_xho17su(h6W{&w`=-3Vs{78xP9^^Xd7`qll0SF?&s@pg<}I?H{r8W| z?QC~G%Y3+O#=^9kX?pD(p^A&g)ouM0?K%4{XuTF)%dv;eX3vc@{q2eI%n~OiKMc*2 z-ldc_ar4?qiACoh>h~9JH~i19H@o!9o7m=v-*cAkOy0rYc~ARW$i7bd>s!CbeDK~7 zzka7@Vm<4R{$H1Bm@4E0zpR{8TXOHYdwl#_k$Y_SSgS71Zm zFF*UFfxW?g%i29FSkGRvGB;i%_Q^XuJ8#`e$6M=WMFw*JHJWK1*MCAYclXpoQ{+$c z?J8R+d0Nct*u|G>^=s3r(gknbVBI}W{MLl|cQ3suVUwu5VZX;p{~4Ri?bs_pN!u15 zaDUc}7!?>~%AxVFh;Kj8gMUcFVJ~?cea( zoMPMkwnIN|tly~nCpU=6I-#m-+QL&G$_{Z>D9hT$ zFkV*WeVzXx^1`j4TN@WIef6{Pg0k8{`xAC&F714HVb#Ay|5oO$h+iRo<-T=Ri=_23 zm0GP@?Y|ne>b3fReY#2_cPaftu^DB;?i<-4_7_alph3BM}aT(;}S zLXNhqE)k2oJ%94DAhQ$4ZhTQ*UKTUw>9np2t9f1O9-Z&I{f``fWX!aE-ku+EKh!oX zWB&SYvao@b)q`9eAJr4akLFqxhPi&>Dw%!ZY-}CdeZEDWtIMvLoe~Ydxqj7pP0pHA zJB(tly0?p-H4#1kRKu)%$KFHTi&geDEA_o_y!U&bYuR0%+;`C~?f(y1zYfcYULjoj zR^aP?satIy{A!*VFY0Fhx@+&F=Zig_uuTlM+Hn83>`U)QcT2WktZ4WidS2bdTHajh z|2lIi>v*OAVi!|C+eb1KR^&))3j>P{&H|6fVg?3oAe&p5kzv*x31A^|FEb>fB*NFn zDmgz_FEJ%QDOIl`w*aV`fx)K23dqb&ElE_U$j!+swyLmI0;{kBvO&W7N(x{lCE2!0 z5xxNm&iO^D3Z{C7dPYiiTnY*bHbp6ERzWUqQ0+jTtx`rwNr9EVetCJhUb(Seeo?x< zp{1pzzJZaxk&!M?g>G?WUP)qwZeFo6#1NP{E~&-IMVSR9nfZANAafIw@=Hr>m6Sjh z!2!gbC7EdmoAQdG-U511A0(r1sAr%LHyfzc1|(_~lv&|7+n}) z8$_3Ler`cgYH=}8o1H0C3$hrx`Ur$IcIHM<4akD%8vKhgQ-J{jau?i9$YSW~Be7Y4 zEC|(LqYny1q(BA77$`K*#0dp5WH7CL)GdDF49CT(T1~&Rw zWsv*_mUqt2$u9~nNK8%z1qZqix@xdJkqCP%%~0$?lA(66q$I=BdO>MX4mhQ!BJXdjti(0R(9EOCG0%l%)p>F!PCVtq=ND7)q|W2 z3Op>1TmR2fHhWm7#w)|qc-MHd3?mZ@hk(L|@CSe9Zgy*60C8;?8NacEL>&|a1RgNl zRROCJNU(PR%QG=D@?G#1QULK7zcG(091R{OmeE8pniWP%iqZ0Mv{o3cGzCVR3Zo6A z(PrUjvv9OoINC!R?Ldxpn@0x=M#oZs?Sau|;b^mPv{^XXEF5hXjy4NNn}wsz!qH~o zXtQv%SvWd#Hac@QI&(HUb2d72Hac@QIz%$Mv}l+wsM?{(#8unBtmQput*xi4pUXO@ GgeCxlsfZW= literal 0 HcmV?d00001 diff --git a/ParseStarterProject/iOS/ParseStarterProject/Resources/Info.plist b/ParseStarterProject/iOS/ParseStarterProject/Resources/Info.plist new file mode 100644 index 000000000..a1e0395bc --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/Resources/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.parse.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.8.0 + CFBundleSignature + ???? + CFBundleVersion + 1.8.0 + LSRequiresIPhoneOS + + NSMainNibFile + MainWindow + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/InfoPlist.strings b/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/InfoPlist.strings new file mode 100644 index 000000000..b92732c79 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +/* Localized versions of Info.plist keys */ diff --git a/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/MainWindow.xib b/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/MainWindow.xib new file mode 100644 index 000000000..8cdaa4a7f --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/MainWindow.xib @@ -0,0 +1,444 @@ + + + + 1024 + 10D571 + 786 + 1038.29 + 460.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 112 + + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + IBCocoaTouchFramework + + + ParseStarterProjectViewController + + + 1 + + IBCocoaTouchFramework + NO + + + + 292 + {320, 480} + + 1 + MSAxIDEAA + + NO + NO + + IBCocoaTouchFramework + YES + + + + + YES + + + delegate + + + + 4 + + + + viewController + + + + 11 + + + + window + + + + 14 + + + + + YES + + 0 + + + + + + -1 + + + File's Owner + + + 3 + + + ParseStarterProject App Delegate + + + -2 + + + + + 10 + + + + + 12 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 10.CustomClassName + 10.IBEditorWindowLastContentRect + 10.IBPluginDependency + 12.IBEditorWindowLastContentRect + 12.IBPluginDependency + 3.CustomClassName + 3.IBPluginDependency + + + YES + UIApplication + UIResponder + ParseStarterProjectViewController + {{234, 376}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + {{525, 346}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + ParseStarterProjectAppDelegate + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + + YES + + + + + YES + + + YES + + + + 15 + + + + YES + + UIWindow + UIView + + IBUserSource + + + + + ParseStarterProjectAppDelegate + NSObject + + YES + + YES + viewController + window + + + YES + ParseStarterProjectViewController + UIWindow + + + + YES + + YES + viewController + window + + + YES + + viewController + ParseStarterProjectViewController + + + window + UIWindow + + + + + IBProjectSource + ParseStarterProjectAppDelegate.h + + + + ParseStarterProjectAppDelegate + NSObject + + IBUserSource + + + + + ParseStarterProjectViewController + UIViewController + + IBProjectSource + ParseStarterProjectViewController.h + + + + + YES + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UIAccessibility.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UINibLoading.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UIResponder.h + + + + UIApplication + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIApplication.h + + + + UIResponder + NSObject + + + + UISearchBar + UIView + + IBFrameworkSource + UIKit.framework/Headers/UISearchBar.h + + + + UISearchDisplayController + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UISearchDisplayController.h + + + + UIView + + IBFrameworkSource + UIKit.framework/Headers/UITextField.h + + + + UIView + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIView.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UINavigationController.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UIPopoverController.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UISplitViewController.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UITabBarController.h + + + + UIViewController + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIViewController.h + + + + UIWindow + UIView + + IBFrameworkSource + UIKit.framework/Headers/UIWindow.h + + + + + 0 + IBCocoaTouchFramework + + com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS + + + + com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 + + + YES + ParseStarterProject.xcodeproj + 3 + 112 + + diff --git a/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/ParseStarterProjectViewController.xib b/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/ParseStarterProjectViewController.xib new file mode 100644 index 000000000..0b8ad95c0 --- /dev/null +++ b/ParseStarterProject/iOS/ParseStarterProject/Resources/en.lproj/ParseStarterProjectViewController.xib @@ -0,0 +1,190 @@ + + + + 1056 + 10J869 + 1306 + 1038.35 + 461.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 301 + + + YES + IBProxyObject + IBUIView + IBUILabel + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + + + YES + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + + 274 + + YES + + + 292 + {{65, 159}, {191, 21}} + + + NO + YES + 7 + NO + IBCocoaTouchFramework + You're all set with Parse! + + Helvetica + 17 + 16 + + + 1 + MCAwIDAAA + + + 1 + 10 + + + {{0, 20}, {320, 460}} + + + + 3 + MC43NQA + + 2 + + + NO + + IBCocoaTouchFramework + + + + + YES + + + view + + + + 7 + + + + + YES + + 0 + + + + + + -1 + + + File's Owner + + + -2 + + + + + 6 + + + YES + + + + + + 8 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 6.IBEditorWindowLastContentRect + 6.IBPluginDependency + 8.IBPluginDependency + + + YES + ParseStarterProjectViewController + UIResponder + {{239, 654}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + + + + + YES + + + + + 8 + + + + YES + + ParseStarterProjectViewController + UIViewController + + IBProjectSource + ./Classes/ParseStarterProjectViewController.h + + + + + 0 + IBCocoaTouchFramework + + com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 + + + YES + 3 + 301 + + diff --git a/README.md b/README.md new file mode 100644 index 000000000..99a96acdf --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Parse SDK for iOS/OS X + +[![Build Status][build-status-svg]][build-status-link] +[![Coverage Status][coverage-status-svg]][coverage-status-link] +[![Podspec][podspec-svg]][podspec-link] +[![License][license-svg]][license-link] +![Platforms][platforms-svg] +[![Dependencies][dependencies-svg]][dependencies-link] +[![References][references-svg]][references-link] + +A library that gives you access to the powerful Parse cloud platform from your iOS or OS X app. +For more information Parse and its features, see [the website][parse.com] and [getting started][docs]. + +## Other Parse Projects + + - [ParseUI for iOS][parseui-ios-link] + - [Parse SDK for Android][android-sdk-link] + +## Getting Started + +To use parse, head on over to the [releases][releases] page, and download the latest build. +And you're off! Take a look at the public [documentation][docs] and start building. + +**Other Installation Options** + + 1. **CocoaPods** + + Add the following line to your podfile: + + pod 'Parse' + + Run pod install, and you should now have the latest parse release. + + 2. **Compiling for yourself** + + If you want to manually compile the SDK, clone it locally, and run the following command in the root directory of the repository: + + rake package:deployment + + Your binaries should now be located inside the `build` folder, and you can link them as you'd please. + + 3. **Using Parse as a sub-project** + + You can also include parse as a subproject inside of your application if you'd prefer, although we do not recommend this, as it will increase your indexing time significantly. To do so, just drag and drop the Parse.xcodeproj file into your workspace. Note that unit tests will be unavailable if you use Parse like this, as OCMock will be unable to be found. + +## How Do I Contribute? + +We want to make contributing to this project as easy and transparent as possible. Please refer to the [Contribution Guidelines][contributing]. + +## Dependencies + +We use the following libraries as dependencies inside of Parse: + + - [Bolts][bolts-framework], for task management. + - [OCMock][ocmock-framework], for unit testing. + +## License + +``` +Copyright (c) 2015-present, Parse, LLC. +All rights reserved. + +This source code is licensed under the BSD-style license found in the +LICENSE file in the root directory of this source tree. An additional grant +of patent rights can be found in the PATENTS file in the same directory. +``` + + [parse.com]: https://www.parse.com/products/ios + [docs]: https://www.parse.com/docs/ios/guide + [blog]: https://blog.parse.com/ + + [parseui-ios-link]: https://github.com/ParsePlatform/ParseUI-iOS + [android-sdk-link]: https://github.com/ParsePlatform/Parse-SDK-Android + + [releases]: https://github.com/ParsePlatform/Parse-SDK-iOS-OSX/releases + [contributing]: https://github.com/ParsePlatform/Parse-SDK-iOS-OSX/blob/master/CONTRIBUTING.md + + [bolts-framework]: https://github.com/BoltsFramework/Bolts-iOS + [ocmock-framework]: http://ocmock.org + + [build-status-svg]: https://travis-ci.org/ParsePlatform/Parse-SDK-iOS-OSX.svg + [build-status-link]: https://travis-ci.org/ParsePlatform/Parse-SDK-iOS-OSX/branches + + [coverage-status-svg]: https://coveralls.io/repos/ParsePlatform/Parse-SDK-iOS-OSX/badge.svg?branch=master&service=github + [coverage-status-link]: https://coveralls.io/github/ParsePlatform/Parse-SDK-iOS-OSX?branch=master + + [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg + [license-link]: https://github.com/ParsePlatform/Parse-SDK-iOS-OSX/blob/master/LICENSE + + [podspec-svg]: https://img.shields.io/cocoapods/v/Parse.svg + [podspec-link]: https://cocoapods.org/pods/Parse + + [platforms-svg]: https://img.shields.io/badge/platform-ios%20%7C%20osx-lightgrey.svg + + [dependencies-svg]: https://img.shields.io/badge/dependencies-2-yellowgreen.svg + [dependencies-link]: https://github.com/ParsePlatform/Parse-SDK-iOS-OSX/blob/master/Vendor + + [references-svg]: https://www.versioneye.com/objective-c/parse/reference_badge.svg + [references-link]: https://www.versioneye.com/objective-c/parse/references diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..772eee25f --- /dev/null +++ b/Rakefile @@ -0,0 +1,313 @@ +# +# Copyright (c) 2015-present, Parse, LLC. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# + +require_relative 'Scripts/xctask/build_task' +require_relative 'Scripts/xctask/build_framework_task' + +script_folder = File.expand_path(File.dirname(__FILE__)) +build_folder = File.join(script_folder, 'build') +release_folder = File.join(build_folder, 'release') +bolts_build_folder = File.join(script_folder, 'Vendor', 'Bolts-ObjC', 'build') + +module Constants + require 'plist' + + script_folder = File.expand_path(File.dirname(__FILE__)) + + PARSE_CONSTANTS_HEADER = File.join(script_folder, 'Parse', 'PFConstants.h') + PLISTS = [ + File.join(script_folder, 'Parse', 'Resources', 'Framework.plist'), + File.join(script_folder, 'Parse', 'Resources', 'FrameworkOSX.plist'), + File.join(script_folder, 'ParseStarterProject', 'iOS', 'ParseStarterProject', 'Resources', 'Info.plist'), + File.join(script_folder, 'ParseStarterProject', 'iOS', 'ParseStarterProject-Swift', 'Resources', 'Info.plist'), + File.join(script_folder, 'ParseStarterProject', 'OSX', 'ParseOSXStarterProject', 'Resources', 'Info.plist'), + File.join(script_folder, 'ParseStarterProject', 'OSX', 'ParseOSXStarterProject-Swift', 'Resources', 'Info.plist') + ] + + def self.current_version + constants_file = File.open(PARSE_CONSTANTS_HEADER, 'r').read + matches = constants_file.match(/(.*PARSE_VERSION\s*@")(.*)(")/) + matches[2] # Return the second match, which is the version itself + end + + def self.update_version(version) + constants_file = File.open(PARSE_CONSTANTS_HEADER, 'r+') + constants = constants_file.read + constants.gsub!(/(.*PARSE_VERSION\s*@")(.*)(")/, "\\1#{version}\\3") + + constants_file.seek(0) + constants_file.write(constants) + + PLISTS.each do |plist| + update_info_plist_version(plist, version) + end + end + + def self.update_info_plist_version(plist_path, version) + info_plist = Plist.parse_xml(plist_path) + info_plist['CFBundleShortVersionString'] = version + info_plist['CFBundleVersion'] = version + File.open(plist_path, 'w') { |f| f.write(info_plist.to_plist) } + end +end + +namespace :build do + desc 'Build iOS framework.' + task :ios do + task = XCTask::BuildFrameworkTask.new do |t| + t.directory = script_folder + t.build_directory = build_folder + t.framework_type = XCTask::FrameworkType::IOS + t.framework_name = 'Parse.framework' + + t.workspace = 'Parse.xcworkspace' + t.scheme = 'Parse-iOS' + t.configuration = 'Release' + end + result = task.execute + unless result + puts 'Failed to build iOS Framework.' + exit(1) + end + end + + desc 'Build OS X framework.' + task :osx do + task = XCTask::BuildFrameworkTask.new do |t| + t.directory = script_folder + t.build_directory = build_folder + t.framework_type = XCTask::FrameworkType::OSX + t.framework_name = 'ParseOSX.framework' + + t.workspace = 'Parse.xcworkspace' + t.scheme = 'Parse-OSX' + t.configuration = 'Release' + end + result = task.execute + unless result + puts 'Failed to build OS X Framework.' + exit(1) + end + end +end + +namespace :package do + package_ios_name = 'Parse-iOS.zip' + package_osx_name = 'Parse-OSX.zip' + package_starter_ios_name = 'ParseStarterProject-iOS.zip' + package_starter_osx_name = 'ParseStarterProject-OSX.zip' + + task :prepare do + `rm -rf #{build_folder} && mkdir -p #{build_folder}` + `rm -rf #{bolts_build_folder} && mkdir -p #{bolts_build_folder}` + end + + desc 'Build and package all frameworks for the release' + task :frameworks, [:version] => :prepare do |_, args| + version = args[:version] || Constants.current_version + Constants.update_version(version) + + ## Build iOS Framework + Rake::Task['build:ios'].invoke + bolts_path = File.join(bolts_build_folder, 'ios', 'Bolts.framework') + ios_framework_path = File.join(build_folder, 'Parse.framework') + make_package(release_folder, + [ios_framework_path, bolts_path], + package_ios_name) + + ## Build OS X Framework + Rake::Task['build:osx'].invoke + bolts_path = File.join(bolts_build_folder, 'osx', 'Bolts.framework') + osx_framework_path = File.join(build_folder, 'ParseOSX.framework') + make_package(release_folder, + [osx_framework_path, bolts_path], + package_osx_name) + end + + desc 'Build and package all starter projects for the release' + task :starters, [:version] => :frameworks do |_, _args| + require 'xcodeproj' + + ios_starters = [ + File.join(script_folder, 'ParseStarterProject', 'iOS', 'ParseStarterProject'), + File.join(script_folder, 'ParseStarterProject', 'iOS', 'ParseStarterProject-Swift') + ] + ios_framework_archive = File.join(release_folder, package_ios_name) + make_starter_package(release_folder, ios_starters, ios_framework_archive, package_starter_ios_name) + + osx_starters = [ + File.join(script_folder, 'ParseStarterProject', 'OSX', 'ParseOSXStarterProject'), + File.join(script_folder, 'ParseStarterProject', 'OSX', 'ParseOSXStarterProject-Swift') + ] + osx_framework_archive = File.join(release_folder, package_osx_name) + make_starter_package(release_folder, osx_starters, osx_framework_archive, package_starter_osx_name) + end + + def make_package(target_path, items, archive_name) + temp_folder = File.join(target_path, 'tmp') + `mkdir -p #{temp_folder}` + + item_list = '' + items.each do |item| + `cp -R #{item} #{temp_folder}` + + file_name = File.basename(item) + item_list << " #{file_name}" + end + + archive_path = File.join(target_path, archive_name) + `cd #{temp_folder}; zip -r --symlinks #{archive_path} #{item_list}` + `rm -rf #{temp_folder}` + puts "Release archive created: #{File.join(target_path, archive_name)}" + end + + def make_starter_package(target_path, starter_projects, framework_archive, archive_name) + starter_projects.each do |project_path| + `git clean -xfd #{project_path}` + `cd #{project_path} && unzip -o #{framework_archive}` + + xcodeproj_path = Dir.glob(File.join(project_path, '*.xcodeproj'))[0] + prepare_xcodeproj(xcodeproj_path) + end + make_package(target_path, starter_projects, archive_name) + + starter_projects.each do |project_path| + `git clean -xfd #{project_path}` + `git checkout #{project_path}` + end + end + + def prepare_xcodeproj(path) + project = Xcodeproj::Project.open(path) + project.targets.each do |target| + if target.name == 'Bootstrap' + target.remove_from_project + else + target.dependencies.each(&:remove_from_project) + end + end + project.save + + `rm -rf #{File.join(path, 'xcshareddata', 'xcschemes', '*')}` + end +end + +namespace :test do + desc 'Run iOS Tests' + task :ios do |_| + task = XCTask::BuildTask.new do |t| + t.directory = script_folder + t.workspace = 'Parse.xcworkspace' + + t.scheme = 'Parse-iOS' + t.sdk = 'iphonesimulator8.4' + t.destinations = ['"platform=iOS Simulator,OS=8.4,name=iPhone 4s"', + '"platform=iOS Simulator,OS=8.4,name=iPhone 6 Plus"'] + t.configuration = 'Test' + + t.actions = [XCTask::BuildAction::TEST] + t.formatter = XCTask::BuildFormatter::XCPRETTY + end + unless task.execute + puts 'iOS Tests Failed!' + exit(1) + end + # Slather if running in Travis + `slather` if ENV['TRAVIS'] + end + + desc 'Run OS X Tests' + task :osx do |_| + task = XCTask::BuildTask.new do |t| + t.directory = script_folder + t.workspace = 'Parse.xcworkspace' + + t.scheme = 'Parse-OSX' + t.sdk = 'macosx10.10' + t.destinations = ['arch=x86_64'] + t.configuration = 'Test' + + t.actions = [XCTask::BuildAction::TEST] + t.formatter = XCTask::BuildFormatter::XCPRETTY + end + unless task.execute + puts 'OS X Tests Failed!' + exit(1) + end + # Slather if running in Travis + `slather` if ENV['TRAVIS'] + end + + desc 'Run Deployment Tests' + task :deployment do |_| + Rake::Task['package:frameworks'].invoke + Rake::Task['package:starters'].invoke + end + + desc 'Run Starter Project Tests' + task :starters do |_| + results = [] + ios_schemes = ['ParseStarterProject', + 'ParseStarterProject-Swift'] + osx_schemes = ['ParseOSXStarterProject', + 'ParseOSXStarterProject-Swift'] + + ios_schemes.each do |scheme| + task = XCTask::BuildTask.new do |t| + t.directory = script_folder + t.workspace = 'Parse.xcworkspace' + + t.scheme = scheme + t.configuration = 'Debug' + t.sdk = 'iphonesimulator' + + t.actions = [XCTask::BuildAction::CLEAN, XCTask::BuildAction::BUILD] + t.formatter = XCTask::BuildFormatter::XCPRETTY + end + results << task.execute + end + osx_schemes.each do |scheme| + task = XCTask::BuildTask.new do |t| + t.directory = script_folder + t.workspace = 'Parse.xcworkspace' + + t.scheme = scheme + t.configuration = 'Debug' + t.sdk = 'macosx' + + t.actions = [XCTask::BuildAction::CLEAN, XCTask::BuildAction::BUILD] + t.formatter = XCTask::BuildFormatter::XCPRETTY + end + results << task.execute + end + + results.each do |result| + unless result + puts 'Starter Project Tests Failed!' + exit(1) + end + end + end + + desc 'Run Podspec Lint' + task :podspecs do |_| + podspecs = ['Parse.podspec', + 'Parse-OSX.podspec'] + results = [] + podspecs.each do |podspec| + results << system("pod lib lint #{podspec} --verbose") + end + results.each do |result| + unless result + puts 'Podspec Tests Failed!' + exit(1) + end + end + end +end diff --git a/Scripts/build_third_party.sh b/Scripts/build_third_party.sh new file mode 100755 index 000000000..d95af8481 --- /dev/null +++ b/Scripts/build_third_party.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# +# Copyright (c) 2015-present, Parse, LLC. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# + +set -e + +if [[ $ACTION == "clean" ]]; then + exit 0 +fi + +if [[ $1 == "" || $2 == "" || $3 == "" ]]; then + echo "Use this script to build a thid party framework for iOS/OSX." + echo "It is intended to support building Bolts.framework and FacebookSDK.framework" + echo "Usage: 'build_third_party.sh " + exit 1 +fi + +SOURCE_DIR=$(cd $(dirname $0); pwd) +FRAMEWORK_DIR=$(cd $1; pwd) +BUILT_PRODUCTS_DIR=$2 +SCRIPT_PATH=$3 + +if [ ! -d "$FRAMEWORK_DIR" ]; then + echo "Framework path supplied doesn't exist. Please double check it and try again." + exit 1 +fi + +NUM_CHANGES=$(git status --porcelain $FRAMEWORK_DIR | wc -l) +HAS_CHANGES=$([[ $NUM_CHANGES -gt 0 ]] && echo 1 || echo 0) + +BUILD_REVISION_PATH=$BUILT_PRODUCTS_DIR/build_revision +LAST_REVISION=$(git log -n 1 --format=%h .) + +if [[ $HAS_CHANGES == 0 ]]; then + echo "No local changes inside $FRAMEWORK_DIR." + + LAST_BUILD_REVISION=$([ -e $BUILD_REVISION_PATH ] && cat $BUILD_REVISION_PATH || echo 0) + + if [[ $LAST_REVISION != $LAST_BUILD_REVISION ]]; then + echo "Found new revision for $FRAMEWORK_DIR. Rebuilding..." + HAS_CHANGES=1 + fi +fi + +if [[ $HAS_CHANGES == 1 ]]; then + SCRIPTS_DIR=$(dirname "$3") + SCRIPT_FILE=$(basename "$3") + + cd $SCRIPTS_DIR + + eval "XCTOOL=xcodebuild ./$SCRIPT_FILE" + BUILD_RESULT=$? + + if [[ $BUILD_RESULT == 0 ]]; then + cd $SOURCE_DIR + echo $LAST_REVISION > $BUILD_REVISION_PATH + fi +fi diff --git a/Scripts/xctask/build_framework_task.rb b/Scripts/xctask/build_framework_task.rb new file mode 100755 index 000000000..3f8ce93a9 --- /dev/null +++ b/Scripts/xctask/build_framework_task.rb @@ -0,0 +1,140 @@ +#!/usr/bin/env ruby +# +# Copyright (c) 2015-present, Parse, LLC. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# + +require 'naturally' +require_relative 'build_task' + +module XCTask + # This class defines all possible framework types for BuildFrameworkTask. + class FrameworkType + IOS = 'ios' + OSX = 'osx' + + def self.verify(type) + if type.nil? || (type != IOS && type != OSX) + fail "Unknown framework type. Available types: 'ios', 'osx'." + end + end + end + + # This class adds ability to easily configure a building of iOS/OSX framework and execute the build process. + class BuildFrameworkTask + attr_accessor :directory + attr_accessor :build_directory + attr_accessor :framework_type + attr_accessor :framework_name + + attr_accessor :workspace + attr_accessor :project + attr_accessor :scheme + attr_accessor :configuration + + def initialize + @directory = '.' + @build_directory = './build' + yield self if block_given? + end + + def execute + verify + prepare_build + build + end + + private + + def verify + FrameworkType.verify(@framework_type) + end + + def prepare_build + Dir.chdir(@directory) unless @directory.nil? + end + + def build + case @framework_type + when FrameworkType::IOS + build_ios_framework + when FrameworkType::OSX + build_osx_framework + end + end + + def build_ios_framework + framework_paths = [] + framework_paths << build_framework('iphoneos') + framework_paths << build_framework('iphonesimulator') + final_path = final_framework_path + + system("rm -rf #{final_path} && cp -R #{framework_paths[0]} #{final_path}") + + binary_name = File.basename(@framework_name, '.framework') + system("rm -rf #{final_path}/#{binary_name}") + + lipo_command = 'lipo -create' + framework_paths.each do |path| + lipo_command += " #{path}/#{binary_name}" + end + lipo_command += " -o #{final_path}/#{binary_name}" + + result = system(lipo_command) + unless result + puts 'Failed to lipo iOS framework.' + exit(1) + end + result + end + + def build_osx_framework + build_path = build_framework('macosx') + final_path = final_framework_path + system("rm -rf #{final_path} && cp -R #{build_path} #{final_path}") + end + + def build_framework(sdk) + configuration_directory = configuration_build_directory(sdk) + build_task = BuildTask.new do |t| + t.directory = @directory + t.project = @project + t.workspace = @workspace + + t.scheme = @scheme + t.sdk = latest_sdk(sdk) + t.configuration = @configuration + + t.actions = [BuildAction::CLEAN, BuildAction::BUILD] + t.formatter = BuildFormatter::XCPRETTY + + t.additional_options = { 'CONFIGURATION_BUILD_DIR' => "#{configuration_directory}" } + end + + result = build_task.execute + unless result + puts "Failed to build framework for #{sdk}." + exit(1) + end + + "#{configuration_directory}/#{@framework_name}" + end + + def latest_sdk(platform) + sdks = Naturally.sort(`xcodebuild -showsdks`.scan(/-sdk (.*)$/)).reverse.flatten + sdks.select { |s| s =~ /#{platform}/ }[0] + end + + def configuration_build_directory(sdk) + "#{@build_directory}/#{@configuration}-#{@framework_type}-#{sdk}" + end + + def final_framework_path + "#{@build_directory}/#{@framework_name}" + end + end +end diff --git a/Scripts/xctask/build_task.rb b/Scripts/xctask/build_task.rb new file mode 100755 index 000000000..88d66f749 --- /dev/null +++ b/Scripts/xctask/build_task.rb @@ -0,0 +1,132 @@ +#!/usr/bin/env ruby +# +# Copyright (c) 2015-present, Parse, LLC. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# + +module XCTask + # This class defines all possible build formatters for BuildTask. + class BuildFormatter + XCODEBUILD = 'xcodebuild' + XCPRETTY = 'xcpretty' + XCTOOL = 'xctool' + + def self.verify(formatter) + if formatter && + formatter != XCPRETTY && + formatter != XCODEBUILD && + formatter != XCTOOL + fail "Unknown formatter used. Available formatters: 'xcodebuild', 'xcpretty', 'xctool'." + end + end + end + + # This class defines all possible build actions for BuildTask. + class BuildAction + BUILD = 'build' + CLEAN = 'clean' + TEST = 'test' + + def self.verify(action) + if action.nil? || + (action != BUILD && + action != CLEAN && + action != TEST) + fail "Unknown build action used. Available actions: 'build', 'clean', 'test'." + end + end + end + + # This class adds ability to easily configure a xcodebuild task and execute it. + class BuildTask + attr_accessor :directory + attr_accessor :workspace + attr_accessor :project + + attr_accessor :scheme + attr_accessor :sdk + attr_accessor :configuration + attr_accessor :destinations + + attr_accessor :additional_options + attr_accessor :actions + attr_accessor :formatter + + attr_accessor :reports_enabled + + def initialize + @directory = '.' + @destinations = [] + @additional_options = {} + + yield self if block_given? + end + + def execute + verify + prepare_build + build + end + + private + + def verify + BuildFormatter.verify(@formatter) + @actions.each do |action| + BuildAction.verify(action) + end + end + + def prepare_build + Dir.chdir(@directory) unless @directory.nil? + end + + def build + system(build_command_string(@formatter)) + end + + def build_command_string(formatter) + command_string = nil + + case formatter + when BuildFormatter::XCODEBUILD + command_string = 'xcodebuild ' + build_options_string + when BuildFormatter::XCPRETTY + command_string = build_command_string('xcodebuild') + ' | xcpretty -c' + if @actions.include? BuildAction::TEST + command_string += " --report junit --output build/reports/#{@sdk}.xml" + end + command_string += ' ; exit ${PIPESTATUS[0]}' + when BuildFormatter::XCTOOL + command_string = 'xctool ' + build_options_string + else + command_string = build_command_string(BuildFormatter::XCODEBUILD) + end + + command_string + end + + def build_options_string + opts = [] + opts << "-workspace #{@workspace}" if @workspace + opts << "-project #{@project}" if @project + opts << "-scheme #{@scheme}" if @scheme + opts << "-sdk #{@sdk}" if @sdk + opts << "-configuration #{@configuration}" if @configuration + @destinations.each do |d| + opts << "-destination #{d}" + end + opts << @actions.compact.join(' ') if @actions + + @additional_options.each do |key, value| + opts << "#{key}=#{value}" + end + + opts.compact.join(' ') + end + end +end diff --git a/Tests/Other/Cache/TestCache.h b/Tests/Other/Cache/TestCache.h new file mode 100644 index 000000000..a90b858af --- /dev/null +++ b/Tests/Other/Cache/TestCache.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/*! + Because OCMock is not thread-safe, let's create our own class that implements NSCache. + Note that we don't inherit from NSCache, so we still get 'strict mock' functionality. We cannot do expectations + this way, however. + */ +@interface TestCache : NSObject + ++ (NSCache *)cache; + +- (id)objectForKey:(id)key; +- (void)setObject:(id)object forKey:(id)aKey; +- (void)removeObjectForKey:(id)aKey; +- (void)removeAllObjects; + +@end diff --git a/Tests/Other/Cache/TestCache.m b/Tests/Other/Cache/TestCache.m new file mode 100644 index 000000000..3994a942e --- /dev/null +++ b/Tests/Other/Cache/TestCache.m @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "TestCache.h" + +@implementation TestCache { + NSMutableDictionary *_cache; + dispatch_queue_t _queue; +} + ++ (NSCache *)cache { + return (NSCache *)[[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _cache = [[NSMutableDictionary alloc] init]; + _queue = dispatch_queue_create("com.parse.test.cache", DISPATCH_QUEUE_SERIAL); + + return self; +} + +- (id)objectForKey:(id)key { + __block id results = nil; + dispatch_sync(_queue, ^{ + results = _cache[key]; + }); + + return results; +} + +- (void)setObject:(id)object forKey:(id)aKey { + dispatch_sync(_queue, ^{ + _cache[aKey] = object; + }); +} + +- (void)removeObjectForKey:(id)aKey { + dispatch_sync(_queue, ^{ + [_cache removeObjectForKey:aKey]; + }); +} + +- (void)removeAllObjects { + dispatch_sync(_queue, ^{ + [_cache removeAllObjects]; + }); +} + +@end diff --git a/Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.h b/Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.h new file mode 100644 index 000000000..9aa970a87 --- /dev/null +++ b/Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFExtensionDataSharingTestHelper : NSObject + +@property (nonatomic, assign) BOOL swizzledGroupContainerDirectoryPath; +@property (nonatomic, assign) BOOL runningInExtensionEnvironment; + ++ (NSString *)sharedTestDirectoryPath; ++ (NSString *)sharedTestDirectoryPathForGroupIdentifier:(NSString *)groupIdentifier; + +@end diff --git a/Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.m b/Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.m new file mode 100644 index 000000000..5c126b4f9 --- /dev/null +++ b/Tests/Other/ExtensionDataSharing/PFExtensionDataSharingTestHelper.m @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFExtensionDataSharingTestHelper.h" + +#import "PFApplication.h" +#import "PFTestSwizzlingUtilities.h" + +@interface PFExtensionDataSharingTestHelper () + +@property (nonatomic, strong) PFTestSwizzledMethod *groupContainerSwizzledMethod; +@property (nonatomic, strong) PFTestSwizzledMethod *extensionEnvironmentSwizzledMethod; + +@end + +@implementation PFExtensionDataSharingTestHelper + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (NSString *)sharedTestDirectoryPath { + NSString *library = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"]; + NSString *privateDocuments = [library stringByAppendingPathComponent:@"Private Documents"]; + return [privateDocuments stringByAppendingPathComponent:@"Test"]; +} + ++ (NSString *)sharedTestDirectoryPathForGroupIdentifier:(NSString *)groupIdentifier { +#if TARGET_OS_IPHONE + return [[PFExtensionDataSharingTestHelper sharedTestDirectoryPath] stringByAppendingPathComponent:groupIdentifier]; +#else + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + return [paths firstObject]; +#endif +} + +#pragma mark Dealloc + +- (void)dealloc { + _extensionEnvironmentSwizzledMethod.swizzled = NO; + _groupContainerSwizzledMethod.swizzled = NO; +} + +#pragma mark Swizzling + +- (void)setSwizzledGroupContainerDirectoryPath:(BOOL)swizzled { + if (!_groupContainerSwizzledMethod && swizzled) { + _groupContainerSwizzledMethod = [PFTestSwizzlingUtilities swizzleMethod:@selector(containerURLForSecurityApplicationGroupIdentifier:) + inClass:[NSFileManager class] + withMethod:@selector(_swizzledContainerURLForSecurityApplicationGroupIdentifier:) + inClass:[self class]]; + } + _groupContainerSwizzledMethod.swizzled = swizzled; +} + ++ (NSURL *)_swizzledContainerURLForSecurityApplicationGroupIdentifier:(NSString *)identifier { + NSString *path = [PFExtensionDataSharingTestHelper sharedTestDirectoryPathForGroupIdentifier:identifier]; + return [NSURL fileURLWithPath:path]; +} + +- (void)setRunningInExtensionEnvironment:(BOOL)extensionEnvironment { + if (self.extensionEnvironmentSwizzledMethod) { + _extensionEnvironmentSwizzledMethod.swizzled = NO; + } + _extensionEnvironmentSwizzledMethod = [PFTestSwizzlingUtilities swizzleMethod:@selector(isExtensionEnvironment) + inClass:[PFApplication class] + withMethod:(extensionEnvironment + ? @selector(_alwaysTrue) + : @selector(_alwaysFalse)) + inClass:[self class]]; + _extensionEnvironmentSwizzledMethod.swizzled = YES; +} + ++ (BOOL)_alwaysTrue { + return YES; +} + ++ (BOOL)_alwaysFalse { + return NO; +} + +@end diff --git a/Tests/Other/FileManager/TestFileManager.h b/Tests/Other/FileManager/TestFileManager.h new file mode 100644 index 000000000..1f1a88c6d --- /dev/null +++ b/Tests/Other/FileManager/TestFileManager.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface TestFileManager : NSObject + ++ (NSFileManager *)fileManager; + +- (NSData *)contentsAtPath:(NSString *)path; +- (BOOL)createFileAtPath:(NSString *)path contents:(NSData *)data attributes:(NSDictionary *)attr; +- (BOOL) removeItemAtURL:(NSURL *)URL error:(NSError **)error; +- (BOOL)createDirectoryAtURL:(NSURL *)url + withIntermediateDirectories:(BOOL)createIntermediates + attributes:(NSDictionary *)attributes + error:(NSError **)error; +- (NSDirectoryEnumerator *)enumeratorAtPath:(NSString *)path; +- (NSDictionary *)attributesOfItemAtPath:(NSString *)path error:(NSError **)error; +- (BOOL)setAttributes:(NSDictionary *)attributes ofItemAtPath:(NSString *)path error:(NSError **)error; + +@end diff --git a/Tests/Other/FileManager/TestFileManager.m b/Tests/Other/FileManager/TestFileManager.m new file mode 100644 index 000000000..27ccf95a3 --- /dev/null +++ b/Tests/Other/FileManager/TestFileManager.m @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "TestFileManager.h" + +@interface TestFileManagerDirectoryEnumerator : NSObject + +- (instancetype)initWithFileManager:(TestFileManager *)manager path:(NSString *)path; + +- (NSDictionary *)fileAttributes; +- (void)skipDescendants; +- (id)nextObject; + +@end + +@implementation TestFileManager { + // Use public so the directory enumerator can use all of these. Not encapsulated properly, but this is just a test + // class. +@public + NSTimeInterval _lastReturnedTime; + + NSMutableDictionary *_fileAttributes; + NSMutableDictionary *_fileContents; + dispatch_queue_t _queue; +} + ++ (NSFileManager *)fileManager { + return (NSFileManager *)[[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + _lastReturnedTime = 0; + _fileAttributes = [NSMutableDictionary new]; + _fileContents = [NSMutableDictionary new]; + _queue = dispatch_queue_create("com.parse.testfilemanager.sync", DISPATCH_QUEUE_SERIAL); + + return self; +} + +- (NSData *)contentsAtPath:(NSString *)path { + __block NSData *results = nil; + dispatch_sync(_queue, ^{ + results = _fileContents[path]; + }); + + return results; +} + +- (BOOL)createFileAtPath:(NSString *)path contents:(NSData *)data attributes:(NSDictionary *)attributes { + dispatch_sync(_queue, ^{ + NSMutableDictionary *newAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:_lastReturnedTime++]; + + newAttributes[NSFileModificationDate] = newAttributes[NSURLContentModificationDateKey] = date; + newAttributes[NSFileSize] = @([data length]); + + _fileContents[path] = data; + [_fileAttributes setObject:newAttributes forKey:path]; + + }); + return YES; +} + +- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error { + dispatch_sync(_queue, ^{ + [_fileContents removeObjectForKey:URL.path]; + [_fileAttributes removeObjectForKey:URL.path]; + }); + + return YES; +} + +- (BOOL)createDirectoryAtURL:(NSURL *)url + withIntermediateDirectories:(BOOL)createIntermediates + attributes:(NSDictionary *)attributes + error:(NSError **)error { + // No-op + return YES; +} + +- (NSDirectoryEnumerator *)enumeratorAtPath:(NSString *)path { + return (NSDirectoryEnumerator *) [[TestFileManagerDirectoryEnumerator alloc] initWithFileManager:self path:path]; +} + +- (NSDictionary *)attributesOfItemAtPath:(NSString *)path error:(NSError **)error { + __block NSDictionary *results = nil; + dispatch_sync(_queue, ^{ + results = [_fileAttributes[path] copy]; + }); + + return results; +} + +- (BOOL)setAttributes:(NSDictionary *)attributes ofItemAtPath:(NSString *)path error:(NSError **)error { + dispatch_sync(_queue, ^{ + NSMutableDictionary *newAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:_lastReturnedTime++]; + + newAttributes[NSFileModificationDate] = newAttributes[NSURLContentModificationDateKey] = date; + + [_fileAttributes[path] addEntriesFromDictionary:newAttributes]; + }); + + return YES; +} + +@end + +@implementation TestFileManagerDirectoryEnumerator { + TestFileManager *_manager; + NSString *_path; + + NSString *_currentPath; + NSEnumerator *_enumerator; +} + +- (instancetype)initWithFileManager:(TestFileManager *)manager path:(NSString *)path { + self = [super init]; + if (!self) return nil; + + _manager = manager; + _path = [path copy]; + + dispatch_sync(_manager->_queue, ^{ + _enumerator = [manager->_fileContents keyEnumerator]; + }); + + return self; +} + +- (NSDictionary *)fileAttributes { + __block NSDictionary *results = nil; + dispatch_sync(_manager->_queue, ^{ + results = [_manager->_fileAttributes[_currentPath] copy]; + }); + + return results; +} + +- (id)nextObject { + dispatch_sync(_manager->_queue, ^{ + _currentPath = nil; + while (true) { + if ([_currentPath hasPrefix:_path]) break; + _currentPath = [_enumerator nextObject]; + + if (!_currentPath) break; + } + }); + + return [_currentPath lastPathComponent]; +} + +- (void)skipDescendants { + // No-op +} + +@end diff --git a/Tests/Other/LocationManager/CLLocationManager+TestAdditions.h b/Tests/Other/LocationManager/CLLocationManager+TestAdditions.h new file mode 100644 index 000000000..2dded7842 --- /dev/null +++ b/Tests/Other/LocationManager/CLLocationManager+TestAdditions.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#define CL_DEFAULT_LATITUDE 37.7937 +#define CL_DEFAULT_LONGITUDE -122.3967 + +@interface CLLocationManager (TestAdditions) + +// Used to simulate a delay in finding + returning updated location. ++ (void)setMockingEnabled:(BOOL)enabled; ++ (void)setAuthorizationStatus:(CLAuthorizationStatus)status; + ++ (void)setReturnLocation:(BOOL)doReturnLocation; ++ (void)setWillFail:(BOOL)doFail; ++ (void)reset; + +- (void)overriddenStartUpdatingLocation; + +@end diff --git a/Tests/Other/LocationManager/CLLocationManager+TestAdditions.m b/Tests/Other/LocationManager/CLLocationManager+TestAdditions.m new file mode 100644 index 000000000..91d3d2924 --- /dev/null +++ b/Tests/Other/LocationManager/CLLocationManager+TestAdditions.m @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "CLLocationManager+TestAdditions.h" + +#import "PFTestSwizzlingUtilities.h" + +@interface CLLocationManager () + ++ (void)setAuthorizationStatus:(BOOL)status forBundleIdentifier:(NSString *)bundleIdentifier; + +- (void)resetApps; + +@end + +@implementation CLLocationManager (TestAdditions) + +static BOOL returnLocation = YES; +static BOOL willFail = NO; +static BOOL mockingEnabled = NO; + +///-------------------------------------- +#pragma mark - Configuration +///-------------------------------------- + ++ (void)setMockingEnabled:(BOOL)enabled { + // There is no ability to use real CLLocationManager on Mac, due to permission requests +#if PARSE_OSX_ONLY + if (!enabled) { + return; + } +#endif + if (mockingEnabled != enabled) { + mockingEnabled = enabled; + + [PFTestSwizzlingUtilities swizzleMethod:@selector(startUpdatingLocation) withMethod:@selector(overriddenStartUpdatingLocation) inClass:self]; + } +} + ++ (void)setAuthorizationStatus:(CLAuthorizationStatus)status { + if (status == kCLAuthorizationStatusNotDetermined) { + // Use private API to reset all apps + [[[CLLocationManager alloc] init] resetApps]; + } else { + // Use private API to set the auth status, without triggering the permission request + [self setAuthorizationStatus:status forBundleIdentifier:[NSBundle mainBundle].bundleIdentifier]; + } +} + ++ (void)setReturnLocation:(BOOL)doReturnLocation { + returnLocation = doReturnLocation; +} + ++ (void)setWillFail:(BOOL)doFail { + willFail = doFail; +} + ++ (void)reset { + [self setMockingEnabled:YES]; + returnLocation = YES; + willFail = NO; +} + +///-------------------------------------- +#pragma mark - Swizzled Selector +///-------------------------------------- + +- (void)overriddenStartUpdatingLocation { + if (willFail) { + [self.delegate locationManager:self didFailWithError:[NSError errorWithDomain:kCLErrorDomain + code:kCLErrorLocationUnknown + userInfo:nil]]; + } else if (returnLocation) { + CLLocation *fakeLocation = [[CLLocation alloc] initWithLatitude:CL_DEFAULT_LATITUDE + longitude:CL_DEFAULT_LONGITUDE]; +#if PARSE_IOS_ONLY + [self.delegate locationManager:self didUpdateLocations:[NSArray arrayWithObject:fakeLocation]]; +#else + CLLocation *emptyLocation = [[CLLocation alloc] initWithLatitude:CL_DEFAULT_LATITUDE + longitude:CL_DEFAULT_LONGITUDE]; + [self.delegate locationManager:self didUpdateToLocation:fakeLocation fromLocation:emptyLocation]; +#endif + } +} + +@end diff --git a/Tests/Other/NetworkMocking/PFMockURLProtocol.h b/Tests/Other/NetworkMocking/PFMockURLProtocol.h new file mode 100644 index 000000000..a45fda68a --- /dev/null +++ b/Tests/Other/NetworkMocking/PFMockURLProtocol.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFMockURLResponse.h" + +typedef BOOL(^PFMockURLProtocolRequestTestBlock)(NSURLRequest *request); +typedef PFMockURLResponse*(^PFMockURLResponseContructingBlock)(NSURLRequest *request); + +@interface PFMockURLProtocol : NSURLProtocol + ++ (void)mockRequestsWithResponse:(PFMockURLResponseContructingBlock)constructingBlock; ++ (void)mockRequestsPassingTest:(PFMockURLProtocolRequestTestBlock)testBlock + withResponse:(PFMockURLResponseContructingBlock)constructingBlock; ++ (void)mockRequestsPassingTest:(PFMockURLProtocolRequestTestBlock)testBlock + withResponse:(PFMockURLResponseContructingBlock)constructingBlock + forAttempts:(NSUInteger)attemptsCount; + ++ (void)removeAllMocking; + +@end diff --git a/Tests/Other/NetworkMocking/PFMockURLProtocol.m b/Tests/Other/NetworkMocking/PFMockURLProtocol.m new file mode 100644 index 000000000..c3ed1e367 --- /dev/null +++ b/Tests/Other/NetworkMocking/PFMockURLProtocol.m @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMockURLProtocol.h" + +#import "PFTestSwizzlingUtilities.h" + +@interface PFMockURLProtocolMock : NSObject + +@property (nonatomic, strong) PFMockURLProtocolRequestTestBlock testBlock; +@property (nonatomic, strong) PFMockURLResponseContructingBlock responseBlock; +@property (nonatomic, assign) NSUInteger attempts; + +@end + +@implementation PFMockURLProtocolMock + +@end + +///-------------------------------------- +#pragma mark - PFMockURLProtocol +///-------------------------------------- + +@interface PFMockURLProtocol () + +@property (nonatomic, strong) PFMockURLProtocolMock *mock; +@property (nonatomic, assign, getter = isLoading) BOOL loading; + +@end + +@implementation PFMockURLProtocol + +static NSMutableArray *_mocksArray; +static PFTestSwizzledMethod *_swizzledURLSessionMethod; + +///-------------------------------------- +#pragma mark - Mocking +///-------------------------------------- + ++ (void)mockRequestsWithResponse:(PFMockURLResponseContructingBlock)constructingBlock { + return [self mockRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withResponse:constructingBlock]; +} + ++ (void)mockRequestsPassingTest:(PFMockURLProtocolRequestTestBlock)testBlock + withResponse:(PFMockURLResponseContructingBlock)constructingBlock { + return [self mockRequestsPassingTest:testBlock + withResponse:constructingBlock + forAttempts:NSUIntegerMax]; +} + ++ (void)mockRequestsPassingTest:(PFMockURLProtocolRequestTestBlock)testBlock + withResponse:(PFMockURLResponseContructingBlock)constructingBlock + forAttempts:(NSUInteger)attemptsCount { + NSParameterAssert(testBlock != nil); + NSParameterAssert(constructingBlock != nil); + + PFMockURLProtocolMock *mock = [[PFMockURLProtocolMock alloc] init]; + mock.testBlock = testBlock; + mock.responseBlock = constructingBlock; + mock.attempts = attemptsCount; + + if (!_mocksArray) { + _mocksArray = [NSMutableArray array]; + } + [_mocksArray addObject:mock]; + + if ([_mocksArray count] == 1) { + [NSURLProtocol registerClass:self]; + Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: [NSURLSessionConfiguration class]; + _swizzledURLSessionMethod = [PFTestSwizzlingUtilities swizzleMethod:@selector(protocolClasses) + inClass:cls + withMethod:@selector(protocolClasses) + inClass:[self class]]; + } +} + ++ (void)removeAllMocking { + if (_mocksArray) { + [NSURLProtocol unregisterClass:self]; + _swizzledURLSessionMethod.swizzled = NO; + } + [_mocksArray removeAllObjects]; +} + ++ (PFMockURLProtocolMock *)_firstMockForRequest:(NSURLRequest *)request { + for (PFMockURLProtocolMock *mock in _mocksArray) { + if (mock.attempts > 0 && mock.testBlock(request)) { + return mock; + } + } + return nil; +} + +- (NSArray *)protocolClasses { + return @[[PFMockURLProtocol class]]; +} + +///-------------------------------------- +#pragma mark - NSURLProtocol +///-------------------------------------- + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request { + return [self _firstMockForRequest:request] != nil; +} + ++ (BOOL)canInitWithTask:(NSURLSessionTask *)task { + return [self _firstMockForRequest:task.originalRequest] != nil; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { + return request; +} + +- (instancetype)initWithRequest:(NSURLRequest *)request + cachedResponse:(NSCachedURLResponse *)response + client:(id)client { + self = [super initWithRequest:request cachedResponse:response client:client]; + if (self) { + _mock = [[self class] _firstMockForRequest:request]; + } + return self; +} + +- (instancetype)initWithTask:(NSURLSessionTask *)task + cachedResponse:(NSCachedURLResponse *)cachedResponse + client:(id)client { + self = [super initWithTask:task cachedResponse:cachedResponse client:client]; + if (!self) return nil; + + _mock = [[self class] _firstMockForRequest:task.originalRequest]; + + return self; +} + +- (NSCachedURLResponse *)cachedResponse { + return nil; +} + +- (void)startLoading { + self.loading = YES; + self.mock.attempts -= 1; + + NSURLRequest *request = self.request; + id client = self.client; + + PFMockURLResponse *response = self.mock.responseBlock(request); + + if (response.error) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(response.delay * NSEC_PER_SEC)), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + if (self.loading) { + [client URLProtocol:self didFailWithError:response.error]; + } + }); + return; + } + + NSHTTPURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:response.statusCode + HTTPVersion:@"HTTP/1.1" + headerFields:response.httpHeaders]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(response.delay * NSEC_PER_SEC)), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + if (!self.loading) { + return; + } + + [client URLProtocol:self + didReceiveResponse:urlResponse + cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + + if (response.responseData) { + [client URLProtocol:self didLoadData:response.responseData]; + } + + [client URLProtocolDidFinishLoading:self]; + }); +} + +- (void)stopLoading { + self.loading = NO; +} + +@end diff --git a/Tests/Other/NetworkMocking/PFMockURLResponse.h b/Tests/Other/NetworkMocking/PFMockURLResponse.h new file mode 100644 index 000000000..590ae4f17 --- /dev/null +++ b/Tests/Other/NetworkMocking/PFMockURLResponse.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFMockURLResponse : NSObject + +@property (nonatomic, assign, readonly) NSInteger statusCode; +@property (nonatomic, copy, readonly) NSDictionary *httpHeaders; + +@property (nonatomic, strong, readonly) NSError *error; +@property (nonatomic, copy, readonly) NSData *responseData; + +@property (nonatomic, assign, readonly) NSTimeInterval delay; + + ++ (instancetype)responseWithError:(NSError *)error; ++ (instancetype)responseWithError:(NSError *)error delay:(NSTimeInterval)delay; + ++ (instancetype)responseWithString:(NSString *)string; ++ (instancetype)responseWithString:(NSString *)string + statusCode:(NSInteger)statusCode + delay:(NSTimeInterval)delay; ++ (instancetype)responseWithString:(NSString *)string + statusCode:(NSInteger)statusCode + delay:(NSTimeInterval)delay + headers:(NSDictionary *)httpHeaders; + ++ (instancetype)responseWithData:(NSData *)data + statusCode:(NSInteger)statusCode + delay:(NSTimeInterval)delay + headers:(NSDictionary *)httpHeaders; + +@end diff --git a/Tests/Other/NetworkMocking/PFMockURLResponse.m b/Tests/Other/NetworkMocking/PFMockURLResponse.m new file mode 100644 index 000000000..c89cd325d --- /dev/null +++ b/Tests/Other/NetworkMocking/PFMockURLResponse.m @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMockURLResponse.h" + +@interface PFMockURLResponse () + +@property (nonatomic, assign, readwrite) NSInteger statusCode; +@property (nonatomic, copy, readwrite) NSDictionary *httpHeaders; + +@property (nonatomic, strong, readwrite) NSError *error; +@property (nonatomic, copy, readwrite) NSData *responseData; + +@property (nonatomic, assign, readwrite) NSTimeInterval delay; + +@end + +@implementation PFMockURLResponse + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (instancetype)responseWithError:(NSError *)error { + return [self responseWithError:error delay:0.0]; +} + ++ (instancetype)responseWithError:(NSError *)error delay:(NSTimeInterval)delay { + PFMockURLResponse *response = [[PFMockURLResponse alloc] init]; + response.error = error; + response.delay = delay; + return response; +} + ++ (instancetype)responseWithString:(NSString *)string { + return [self responseWithString:string statusCode:200 delay:0.0]; +} + ++ (instancetype)responseWithString:(NSString *)string + statusCode:(NSInteger)statusCode + delay:(NSTimeInterval)delay { + NSDictionary *headers = @{ @"Content-Type" : @"application/json" }; + return [self responseWithString:string statusCode:statusCode delay:delay headers:headers]; +} + ++ (instancetype)responseWithString:(NSString *)string + statusCode:(NSInteger)statusCode + delay:(NSTimeInterval)delay + headers:(NSDictionary *)httpHeaders { + return [self responseWithData:[string dataUsingEncoding:NSUTF8StringEncoding] + statusCode:statusCode + delay:delay + headers:httpHeaders]; +} + ++ (instancetype)responseWithData:(NSData *)data + statusCode:(NSInteger)statusCode + delay:(NSTimeInterval)delay + headers:(NSDictionary *)httpHeaders { + PFMockURLResponse *response = [[PFMockURLResponse alloc] init]; + response.statusCode = statusCode; + response.httpHeaders = httpHeaders; + response.responseData = data; + response.delay = delay; + return response; +} + +@end diff --git a/Tests/Other/OCMock/OCMock+Parse.h b/Tests/Other/OCMock/OCMock+Parse.h new file mode 100644 index 000000000..a51d0d8ad --- /dev/null +++ b/Tests/Other/OCMock/OCMock+Parse.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface OCMockObject (PFCommandRunning) + +- (void)mockCommandResult:(id)result forCommandsPassingTest:(BOOL (^)(id obj))block; + +@end diff --git a/Tests/Other/OCMock/OCMock+Parse.m b/Tests/Other/OCMock/OCMock+Parse.m new file mode 100644 index 000000000..9296d8dbe --- /dev/null +++ b/Tests/Other/OCMock/OCMock+Parse.m @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "OCMock+Parse.h" + +#import + +#import "PFCommandResult.h" +#import "PFCommandRunning.h" + +@implementation OCMockObject (PFCOmmandRunning) + +- (void)mockCommandResult:(id)result forCommandsPassingTest:(BOOL (^)(id obj))block { + PFCommandResult *commandResult = [PFCommandResult commandResultWithResult:result + resultString:nil + httpResponse:nil]; + BFTask *task = [BFTask taskWithResult:commandResult]; + OCMStub([[(id)self ignoringNonObjectArgs] runCommandAsync:[OCMArg checkWithBlock:block] + withOptions:0]).andReturn(task); + OCMStub([[(id)self ignoringNonObjectArgs] runCommandAsync:[OCMArg checkWithBlock:block] + withOptions:0 + cancellationToken:OCMOCK_ANY]).andReturn(task); +} + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.h b/Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.h new file mode 100644 index 000000000..2a09bcd5f --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFTestSKPaymentQueue : SKPaymentQueue + ++ (void)setCanMakePayments:(BOOL)canMakePayments; + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.m b/Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.m new file mode 100644 index 000000000..2e6f05635 --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKPaymentQueue.m @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestSKPaymentQueue.h" + +#import "PFTestSKPaymentTransaction.h" + +@interface PFTestSKPaymentQueue () +{ + NSMutableSet *_observers; +} + +@end + +@implementation PFTestSKPaymentQueue + ++ (instancetype)defaultQueue { + static PFTestSKPaymentQueue *queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = [[self alloc] init]; + }); + return queue; +} + +static BOOL _canMakePayments = YES; ++ (BOOL)canMakePayments { + return _canMakePayments; +} + ++ (void)setCanMakePayments:(BOOL)canMakePayments { + _canMakePayments = canMakePayments; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + + _observers = [NSMutableSet set]; + + return self; +} + +- (void)addPayment:(SKPayment *)payment { + dispatch_async(dispatch_get_main_queue(), ^{ + PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStatePurchased]; + dispatch_async(dispatch_get_main_queue(), ^{ + for (NSValue *value in _observers) { + id observer = [value nonretainedObjectValue]; + if (observer) { + [observer paymentQueue:self updatedTransactions:@[ transaction ]]; + } + } + }); + }); +} + +- (void)finishTransaction:(SKPaymentTransaction *)transaction { +} + +- (void)addTransactionObserver:(id)observer { + [_observers addObject:[NSValue valueWithNonretainedObject:observer]]; +} + +- (void)removeTransactionObserver:(id)observer { + [_observers removeObject:[NSValue valueWithNonretainedObject:observer]]; + [_observers filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSValue *evaluatedObject, NSDictionary *bindings) { + return ([evaluatedObject nonretainedObjectValue] != nil); + }]]; +} + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.h b/Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.h new file mode 100644 index 000000000..f22f0fe10 --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFTestSKPaymentTransaction : SKPaymentTransaction + +@property (nonatomic, strong) NSError *error; +@property (nonatomic, strong) SKPaymentTransaction *originalTransaction; +@property (nonatomic, strong) SKPayment *payment; +@property (nonatomic, strong) NSDate *transactionDate; +@property (nonatomic, copy) NSString *transactionIdentifier; +@property (nonatomic, strong) NSData *transactionReceipt; +@property (nonatomic, assign) SKPaymentTransactionState transactionState; + ++ (instancetype)transactionForPayment:(SKPayment *)payment + withError:(NSError *)error + inState:(SKPaymentTransactionState)state; + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.m b/Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.m new file mode 100644 index 000000000..513c4e936 --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKPaymentTransaction.m @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestSKPaymentTransaction.h" + +@implementation PFTestSKPaymentTransaction + +@synthesize +error = _error, +originalTransaction = _originalTransaction, +payment = _payment, +transactionDate = _transactionDate, +transactionIdentifier = _transactionIdentifier, +transactionReceipt = _transactionReceipt, +transactionState = _transactionState; + ++ (instancetype)transactionForPayment:(SKPayment *)payment + withError:(NSError *)error + inState:(SKPaymentTransactionState)state { + PFTestSKPaymentTransaction *transaction = [[self alloc] init]; + transaction.payment = payment; + transaction.error = error; + transaction.transactionState = state; + return transaction; +} + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKProduct.h b/Tests/Other/StoreKitMocking/PFTestSKProduct.h new file mode 100644 index 000000000..0dc2c4a9d --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKProduct.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFTestSKProduct : SKProduct + ++ (instancetype)productWithProductIdentifier:(NSString *)productIdentifier + price:(NSDecimalNumber *)price + title:(NSString *)title + description:(NSString *)description; + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKProduct.m b/Tests/Other/StoreKitMocking/PFTestSKProduct.m new file mode 100644 index 000000000..2a70d4bf3 --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKProduct.m @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestSKProduct.h" + +@interface PFTestSKProduct () + +@property (nonatomic, copy) NSString *productIdentifier; +@property (nonatomic, strong) NSDecimalNumber *price; +@property (nonatomic, copy) NSString *localizedTitle; +@property (nonatomic, copy) NSString *localizedDescription; + +@end + +@implementation PFTestSKProduct + +@synthesize productIdentifier = _productIdentifier; +@synthesize price = _price; +@synthesize localizedTitle = _localizedTitle; +@synthesize localizedDescription = _localizedDescription; + ++ (instancetype)productWithProductIdentifier:(NSString *)productIdentifier + price:(NSDecimalNumber *)price + title:(NSString *)title + description:(NSString *)description { + PFTestSKProduct *product = [[self alloc] init]; + product.productIdentifier = [productIdentifier copy]; + product.price = price; + product.localizedTitle = title; + product.localizedDescription = description; + return product; +} + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKProductsRequest.h b/Tests/Other/StoreKitMocking/PFTestSKProductsRequest.h new file mode 100644 index 000000000..744a154e2 --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKProductsRequest.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFTestSKProductsRequest : SKProductsRequest + ++ (void)setValidProducts:(NSSet *)products; + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKProductsRequest.m b/Tests/Other/StoreKitMocking/PFTestSKProductsRequest.m new file mode 100644 index 000000000..6a038fbfb --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKProductsRequest.m @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestSKProductsRequest.h" + +#import "PFTestSKProductsResponse.h" + +@interface PFTestSKProductsRequest () + +@property (nonatomic, copy) NSSet *productIdentifiers; + +@end + +@implementation PFTestSKProductsRequest + +static NSSet *_validProducts; + +///-------------------------------------- +#pragma mark - Class +///-------------------------------------- + ++ (void)setValidProducts:(NSSet *)products { + _validProducts = products; +} + +///-------------------------------------- +#pragma mark - SKProductsRequest +///-------------------------------------- + +- (instancetype)initWithProductIdentifiers:(NSSet *)productIdentifiers { + self = [super init]; + if (!self) return nil; + + _productIdentifiers = [productIdentifiers copy]; + + return self; +} + +- (void)start { + dispatch_async(dispatch_get_main_queue(), ^{ + NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(SKProduct *evaluatedObject, + NSDictionary *bindings) { + return [_productIdentifiers containsObject:evaluatedObject.productIdentifier]; + }]; + NSSet *validProducts = [_validProducts filteredSetUsingPredicate:filterPredicate]; + + NSMutableSet *invalidProductIdentifiers = [_productIdentifiers mutableCopy]; + [invalidProductIdentifiers minusSet:[_validProducts valueForKey:@"productIdentifier"]]; + + PFTestSKProductsResponse *response = [[PFTestSKProductsResponse alloc] initWithProducts:[validProducts allObjects] + invalidProductIdentifiers:[invalidProductIdentifiers allObjects]]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate productsRequest:self didReceiveResponse:response]; + [self.delegate requestDidFinish:self]; + }); + }); +} + +- (void)cancel { +} + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKProductsResponse.h b/Tests/Other/StoreKitMocking/PFTestSKProductsResponse.h new file mode 100644 index 000000000..913f4b315 --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKProductsResponse.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFTestSKProductsResponse : SKProductsResponse + +- (instancetype)initWithProducts:(NSArray *)products + invalidProductIdentifiers:(NSArray *)invalidProductIdentifiers NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Tests/Other/StoreKitMocking/PFTestSKProductsResponse.m b/Tests/Other/StoreKitMocking/PFTestSKProductsResponse.m new file mode 100644 index 000000000..a55823646 --- /dev/null +++ b/Tests/Other/StoreKitMocking/PFTestSKProductsResponse.m @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestSKProductsResponse.h" + +#import "PFAssert.h" + +@interface PFTestSKProductsResponse () + +@property (nonatomic, copy) NSArray *products; +@property (nonatomic, copy) NSArray *invalidProductIdentifiers; + +@end + +@implementation PFTestSKProductsResponse + +@synthesize products = _products; +@synthesize invalidProductIdentifiers = _invalidProductIdentifiers; + +- (instancetype)init { + return [self initWithProducts:nil invalidProductIdentifiers:nil]; +} + +- (instancetype)initWithProducts:(NSArray *)products + invalidProductIdentifiers:(NSArray *)invalidProductIdentifiers { + self = [super init]; + if (!self) return nil; + + _products = [products copy]; + _invalidProductIdentifiers = [invalidProductIdentifiers copy]; + + return self; +} + +@end diff --git a/Tests/Other/Swift/ParseUnitTests-OSX-Bridging-Header.h b/Tests/Other/Swift/ParseUnitTests-OSX-Bridging-Header.h new file mode 100644 index 000000000..12b14787f --- /dev/null +++ b/Tests/Other/Swift/ParseUnitTests-OSX-Bridging-Header.h @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import diff --git a/Tests/Other/Swift/ParseUnitTests-iOS-Bridging-Header.h b/Tests/Other/Swift/ParseUnitTests-iOS-Bridging-Header.h new file mode 100644 index 000000000..96f46e5ce --- /dev/null +++ b/Tests/Other/Swift/ParseUnitTests-iOS-Bridging-Header.h @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import diff --git a/Tests/Other/Swift/SwiftSubclass.swift b/Tests/Other/Swift/SwiftSubclass.swift new file mode 100644 index 000000000..bf4b83a2e --- /dev/null +++ b/Tests/Other/Swift/SwiftSubclass.swift @@ -0,0 +1,23 @@ +/** +* Copyright (c) 2015-present, Parse, LLC. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the root directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +*/ + +import Foundation + +@objc +public class SwiftSubclass : PFObject, PFSubclassing { + @NSManaged public var primitiveProperty : Int; + @NSManaged public var objectProperty : AnyObject?; + + @NSManaged public var relationProperty : PFRelation?; + @NSManaged public var badProperty : CGPoint; + + public static func parseClassName() -> String { + return "SwiftSubclass"; + } +} diff --git a/Tests/Other/Swizzling/PFTestSwizzledMethod.h b/Tests/Other/Swizzling/PFTestSwizzledMethod.h new file mode 100644 index 000000000..2980a48d8 --- /dev/null +++ b/Tests/Other/Swizzling/PFTestSwizzledMethod.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface PFTestSwizzledMethod : NSObject + +@property (nonatomic, assign, getter=isSwizzled) BOOL swizzled; + +- (instancetype)initWithOriginalSelector:(SEL)originalSelector + inClass:(Class)originalClass + replacementSelector:(SEL)replacementSelector + inClass:(Class)replcementClass; + +- (instancetype)initWithOriginalSelector:(SEL)originalSelector + inClass:(Class)originalClass + replacementSelector:(SEL)replacementSelector + inClass:(Class)replcementClass + isClassMethod:(BOOL)isClassMethod; + +@end diff --git a/Tests/Other/Swizzling/PFTestSwizzledMethod.m b/Tests/Other/Swizzling/PFTestSwizzledMethod.m new file mode 100644 index 000000000..b4bafb0f3 --- /dev/null +++ b/Tests/Other/Swizzling/PFTestSwizzledMethod.m @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestSwizzledMethod.h" + +#import + +@interface PFTestSwizzledMethod () + +@property (nonatomic, assign) Method originalMethod; +@property (nonatomic, assign) Method overrideMethod; + +@end + +@implementation PFTestSwizzledMethod + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)initWithOriginalSelector:(SEL)originalSelector + inClass:(Class)originalClass + replacementSelector:(SEL)replacementSelector + inClass:(Class)replacementClass + isClassMethod:(BOOL)isClassMethod{ + self = [super init]; + if (!self) return nil; + + _originalMethod = class_getInstanceMethod(originalClass, originalSelector); + if (_originalMethod == NULL || isClassMethod) { + _originalMethod = class_getClassMethod(originalClass, originalSelector); + } + + _overrideMethod = class_getInstanceMethod(replacementClass, replacementSelector); + if (_overrideMethod == NULL || isClassMethod) { + _overrideMethod = class_getClassMethod(replacementClass, replacementSelector); + } + + return self; +} + +- (instancetype)initWithOriginalSelector:(SEL)originalSelector + inClass:(Class)originalClass + replacementSelector:(SEL)replacementSelector + inClass:(Class)replcementClass { + return [self initWithOriginalSelector:originalSelector + inClass:originalClass + replacementSelector:replacementSelector + inClass:replcementClass + isClassMethod:NO]; +} + +///-------------------------------------- +#pragma mark - Accessors +///-------------------------------------- + +- (void)setSwizzled:(BOOL)swizzled { + if (self.swizzled != swizzled) { + _swizzled = swizzled; + + method_exchangeImplementations(self.originalMethod, self.overrideMethod); + } +} + +@end diff --git a/Tests/Other/Swizzling/PFTestSwizzlingUtilities.h b/Tests/Other/Swizzling/PFTestSwizzlingUtilities.h new file mode 100644 index 000000000..04da9848b --- /dev/null +++ b/Tests/Other/Swizzling/PFTestSwizzlingUtilities.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +#import +#else +#import +#endif + +#import "PFTestSwizzledMethod.h" + +@interface PFTestSwizzlingUtilities : NSObject + ++ (PFTestSwizzledMethod *)swizzleMethod:(SEL)originalSelector + withMethod:(SEL)overrideSelector + inClass:(Class)aClass; ++ (PFTestSwizzledMethod *)swizzleMethod:(SEL)originalSelector + inClass:(Class)originalClass + withMethod:(SEL)overrideSelector + inClass:(Class)overrideClass; ++ (PFTestSwizzledMethod *)swizzleClassMethod:(SEL)originalSelector + inClass:(Class)aClass + withMethod:(SEL)overrideSelector + inClass:(Class)overrideClass; + +@end diff --git a/Tests/Other/Swizzling/PFTestSwizzlingUtilities.m b/Tests/Other/Swizzling/PFTestSwizzlingUtilities.m new file mode 100644 index 000000000..0dc13d51e --- /dev/null +++ b/Tests/Other/Swizzling/PFTestSwizzlingUtilities.m @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestSwizzlingUtilities.h" + +#import + +@implementation PFTestSwizzlingUtilities + ++ (PFTestSwizzledMethod *)swizzleMethod:(SEL)originalSelector + inClass:(Class)originalClass + withMethod:(SEL)overrideSelector + inClass:(Class)overrideClass { + PFTestSwizzledMethod *method = [[PFTestSwizzledMethod alloc] initWithOriginalSelector:originalSelector + inClass:originalClass + replacementSelector:overrideSelector + inClass:overrideClass]; + method.swizzled = YES; + return method; +} + ++ (PFTestSwizzledMethod *)swizzleMethod:(SEL)originalSelector + withMethod:(SEL)overrideSelector + inClass:(Class)aClass { + return [self swizzleMethod:originalSelector + inClass:aClass + withMethod:overrideSelector + inClass:aClass]; +} + ++ (PFTestSwizzledMethod *)swizzleClassMethod:(SEL)originalSelector + inClass:(Class)aClass + withMethod:(SEL)overrideSelector + inClass:(Class)overrideClass { + PFTestSwizzledMethod *method = [[PFTestSwizzledMethod alloc] initWithOriginalSelector:originalSelector + inClass:aClass + replacementSelector:overrideSelector + inClass:overrideClass + isClassMethod:YES]; + method.swizzled = YES; + return method; +} + +@end diff --git a/Tests/Other/TestCases/TestCase/PFTestCase.h b/Tests/Other/TestCases/TestCase/PFTestCase.h new file mode 100644 index 000000000..b40972ba5 --- /dev/null +++ b/Tests/Other/TestCases/TestCase/PFTestCase.h @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@interface PFTestCase : XCTestCase + +///-------------------------------------- +/// @name XCTestCase +///-------------------------------------- + +- (void)setUp NS_REQUIRES_SUPER; +- (void)tearDown NS_REQUIRES_SUPER; + +///-------------------------------------- +/// @name Expectations +///-------------------------------------- + +- (XCTestExpectation *)currentSelectorTestExpectation; +- (void)waitForTestExpectations; + +///-------------------------------------- +/// @name File Asserts +///-------------------------------------- + +- (void)assertFileExists:(NSString *)path; +- (void)assertFileDoesntExist:(NSString *)path; +- (void)assertFile:(NSString *)path hasContents:(NSString *)expected; + +- (void)assertDirectoryExists:(NSString *)path; +- (void)assertDirectoryDoesntExist:(NSString *)path; + +- (void)assertDirectory:(NSString *)directoryPath hasContents:(NSDictionary *)expected only:(BOOL)only; + +///-------------------------------------- +/// @name Mocks +///-------------------------------------- + +- (void)registerMockObject:(id)mockObject; + +@end + +#define _PFRegisterMock(mockObject) [self registerMockObject:mockObject] +#define _PFMockShim(method, args...) ({ id mock = method(args); _PFRegisterMock(mock); mock; }) +#define _PFOCMockWarning _Pragma("GCC warning \"Please use PF mocking methods instead of OCMock ones.\"") + +#define _PFStrictClassMock(kls) [OCMockObject mockForClass:kls] +#define _PFClassMock(kls) [OCMockObject niceMockForClass:kls] +#define _PFStrictProtocolMock(proto) [OCMockObject mockForProtocol:proto] +#define _PFProtocolMock(proto) [OCMockObject niceMockForProtocol:proto] +#define _PFPartialMock(obj) [OCMockObject partialMockForObject:obj] + +#define PFStrictClassMock(...) _PFMockShim(_PFStrictClassMock, __VA_ARGS__) +#define PFClassMock(...) _PFMockShim(_PFClassMock, __VA_ARGS__) +#define PFStrictProtocolMock(...) _PFMockShim(_PFStrictProtocolMock, __VA_ARGS__) +#define PFProtocolMock(...) _PFMockShim(_PFProtocolMock, __VA_ARGS__) +#define PFPartialMock(...) _PFMockShim(_PFPartialMock, __VA_ARGS__) + +#undef OCMStrictClassMock +#undef OCMClassMock +#undef OCMStrictProtocolMock +#undef OCMProtocolMock +#undef OCMPartialMock + +#define OCMStrictClassMock _PFOCMockWarning _PFStrictClassMock +#define OCMClassMock _PFOCMockWarning _PFClassMock +#define OCMStrictProtocolMock _PFOCMockWarning _PFStrictProtocolMock +#define OCMProtocolMock _PFOCMockWarning _PFProtocolMock +#define OCMPartialMock _PFOCMockWarning _PFPartialMock + +#define GHFail XCTFail +#define GHAssertTrue XCTAssertTrue +#define GHAssertFalse XCTAssertFalse +#define GHAssertNil XCTAssertNil +#define GHAssertNotNil XCTAssertNotNil +#define GHAssertEquals XCTAssertEqual +#define GHAssertNotEquals XCTAssertNotEqual +#define GHAssertEqualStrings XCTAssertEqualObjects +#define GHAssertNotEqualStrings XCTAssertNotEqualObjects +#define GHAssertEqualObjects XCTAssertEqualObjects +#define GHAssertNotEqualObjects XCTAssertNotEqualObjects +#define GHAssertEqualsWithAccuracy XCTAssertEqualWithAccuracy +#define GHAssertThrows XCTAssertThrows +#define GHAssertThrowsSpecificNamed XCTAssertThrowsSpecificNamed +#define GHAssertNoThrow XCTAssertNoThrow + +#define PFAssertEqualInts(a1, a2, description...) \ +XCTAssertEqual((int)(a1), (int)(a2), ## description); + +#define PFAssertNotEqualInts(a1, a2, description...) \ +XCTAssertNotEqual((int)(a1), (int)(a2), ## description); + +#define PFAssertIsKindOfClass(a1, a2, description...) \ +XCTAssertTrue([a1 isKindOfClass:[a2 class]], ## description) + +#define PFAssertNotKindOfClass(a1, a2, description...) \ +XCTAssertFalse([a1 isKindOfClass:[a2 class]], ## description) + +#define PFAssertThrowsInconsistencyException(expression, ...) \ +GHAssertThrowsSpecificNamed(expression, NSException, NSInternalInconsistencyException, __VA_ARGS__) + +#define PFAssertThrowsInvalidArgumentException(expression, ...) \ +GHAssertThrowsSpecificNamed(expression, NSException, NSInvalidArgumentException, __VA_ARGS__) + +#define PFAssertStringContains(a, b) XCTAssertTrue([(a) rangeOfString:(b)].location != NSNotFound) diff --git a/Tests/Other/TestCases/TestCase/PFTestCase.m b/Tests/Other/TestCases/TestCase/PFTestCase.m new file mode 100644 index 000000000..29b49b032 --- /dev/null +++ b/Tests/Other/TestCases/TestCase/PFTestCase.m @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestCase.h" + +#import + +#import "PFTestSwizzlingUtilities.h" + +@interface BFTask () + +- (void)warnOperationOnMainThread; + +@end + +@interface BFTask (TestAdditions) + +- (void)warnOperationOnMainThreadNoOp; + +@end + +@implementation BFTask (TestAdditions) + +- (void)warnOperationOnMainThreadNoOp { + // Method for tests +} + +@end + +@implementation PFTestCase { + NSMutableArray *_mocks; + dispatch_queue_t _mockQueue; +} + ++ (void)swizzleWarnOnMainThread { + [PFTestSwizzlingUtilities swizzleMethod:@selector(warnOperationOnMainThread) + withMethod:@selector(warnOperationOnMainThreadNoOp) + inClass:[BFTask class]]; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Remove any custom test log that is attached if it's not available. + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSString *observerClassName = [userDefaults objectForKey:XCTestObserverClassKey]; + if (observerClassName && !NSClassFromString(observerClassName)) { + [userDefaults removeObjectForKey:XCTestObserverClassKey]; + [userDefaults synchronize]; + } +#pragma clang diagnostic pop + }); +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + ++ (void)setUp { + [super setUp]; + + [[self class] swizzleWarnOnMainThread]; +} + ++ (void)tearDown { + [[self class] swizzleWarnOnMainThread]; // restore the original implementation + + [super tearDown]; +} + +- (void)setUp { + [super setUp]; + + _mocks = [[NSMutableArray alloc] init]; + _mockQueue = dispatch_queue_create("com.parse.tests.mock.queue", DISPATCH_QUEUE_SERIAL); +} + +- (void)tearDown { + dispatch_sync(_mockQueue, ^{ + [_mocks makeObjectsPerformSelector:@selector(stopMocking)]; + }); + + _mocks = nil; + _mockQueue = nil; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (XCTestExpectation *)currentSelectorTestExpectation { + NSInvocation *invocation = self.invocation; + NSString *selectorName = invocation ? NSStringFromSelector(invocation.selector) : @"testExpectation"; + return [self expectationWithDescription:selectorName]; +} + +- (void)waitForTestExpectations { + [self waitForExpectationsWithTimeout:10.0 handler:nil]; +} + +///-------------------------------------- +#pragma mark - File Asserts +///-------------------------------------- + +- (void)assertFileExists:(NSString *)path { + BOOL isDir = YES; + NSFileManager *fm = [NSFileManager defaultManager]; + XCTAssertTrue([fm fileExistsAtPath:path isDirectory:&isDir], @"%@ should exist.", path); + XCTAssertTrue(!isDir, @"%@ should not be a directory.", path); +} + +- (void)assertFileDoesntExist:(NSString *)path { + XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:path], @"%@ shouldn't exist.", path); +} + +- (void)assertFile:(NSString *)path hasContents:(NSString *)expected { + [self assertFileExists:path]; + NSString *contents = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; + XCTAssertEqualObjects(expected, contents, @"File contents didn't match. (%@ vs %@)", expected, contents); +} + +- (void)assertDirectoryExists:(NSString *)path { + BOOL isDir = NO; + NSFileManager *fm = [NSFileManager defaultManager]; + XCTAssertTrue([fm fileExistsAtPath:path isDirectory:&isDir], @"%@ should exist.", path); + XCTAssertTrue(isDir, @"%@ should be a directory.", path); +} + +- (void)assertDirectoryDoesntExist:(NSString *)path { + XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:path], @"%@ shouldn't exist.", path); +} + +- (void)assertDirectory:(NSString *)directoryPath hasContents:(NSDictionary *)expected only:(BOOL)only { + [self assertDirectoryExists:directoryPath]; + + // Check for missing files. + [expected enumerateKeysAndObjectsUsingBlock:^(id filename, id contents, BOOL *stop) { + NSString *path = [directoryPath stringByAppendingPathComponent:filename]; + if ([contents isKindOfClass:[NSDictionary class]]) { + [self assertDirectory:path hasContents:contents only:only]; + } else if ([contents isKindOfClass:[NSString class]]) { + [self assertFile:path hasContents:contents]; + } else if ([contents isKindOfClass:[NSNull class]]) { + [self assertFileExists:path]; + } else { + GHFail(@"Not sure what to do with a %@", [contents class]); + } + }]; + + if (only) { + // Check for unexpected files. + NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:directoryPath]; + NSString *filename = nil; + while (filename = [enumerator nextObject]) { + XCTAssertNotNil(expected[filename], @"Unexpected file %@", filename); + } + } +} + +///-------------------------------------- +#pragma mark - Mock Registration +///-------------------------------------- + +- (void)registerMockObject:(id)mockObject { + dispatch_sync(_mockQueue, ^{ + [_mocks addObject:mockObject]; + }); +} + +@end diff --git a/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.h b/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.h new file mode 100644 index 000000000..42c3c9c93 --- /dev/null +++ b/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestCase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface PFUnitTestCase : PFTestCase + +@property (nonatomic, copy, readonly) NSString *applicationId; +@property (nonatomic, copy, readonly) NSString *clientKey; + +///-------------------------------------- +/// @name XCTestCase +///-------------------------------------- + +- (void)setUp NS_REQUIRES_SUPER; +- (void)tearDown NS_REQUIRES_SUPER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m b/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m new file mode 100644 index 000000000..d01940896 --- /dev/null +++ b/Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUnitTestCase.h" + +#import "PFObjectSubclassingController.h" +#import "Parse_Private.h" + +@interface PFUnitTestCase () + +@property (nonatomic, copy, readwrite) NSString *applicationId; +@property (nonatomic, copy, readwrite) NSString *clientKey; + +@end + +@implementation PFUnitTestCase + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + self.applicationId = [[NSUUID UUID] UUIDString]; + self.clientKey = [[NSUUID UUID] UUIDString]; + + [Parse setApplicationId:self.applicationId clientKey:self.clientKey]; + + // NOTE: (richardross) This may seem crazy, but this is to solve an issue with OCMock's mocking, which isn't thread + // Safe. +[Parse setApplicationId: clientKey:] launches a background task that uses several class methods that are + // mocked throughout our unit tests, and this ensures that that task has completed before we continue. + [[Parse _currentManager] clearEventuallyQueue]; +} + +- (void)tearDown { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [PFObjectSubclassingController clearDefaultController]; + + [super tearDown]; +} + +@end diff --git a/Tests/Resources/ParseUnitTests-OSX-Info.plist b/Tests/Resources/ParseUnitTests-OSX-Info.plist new file mode 100644 index 000000000..00950c697 --- /dev/null +++ b/Tests/Resources/ParseUnitTests-OSX-Info.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.parse.tests.unit.osx + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + fb258812780817952 + + + + CFBundleVersion + 1.0 + FacebookAppID + fake_id + LSApplicationCategoryType + + LSRequiresIPhoneOS + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Tests/Resources/ParseUnitTests-iOS-Info.plist b/Tests/Resources/ParseUnitTests-iOS-Info.plist new file mode 100644 index 000000000..9cb8d8bf6 --- /dev/null +++ b/Tests/Resources/ParseUnitTests-iOS-Info.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.parse.tests.unit.ios + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + fb258812780817952 + + + + CFBundleVersion + 1.0 + FacebookAppID + fake_id + LSApplicationCategoryType + + LSRequiresIPhoneOS + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Tests/Unit/ACLStateTests.m b/Tests/Unit/ACLStateTests.m new file mode 100644 index 000000000..92ed60f74 --- /dev/null +++ b/Tests/Unit/ACLStateTests.m @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFACLState.h" +#import "PFMutableACLState.h" +#import "PFTestCase.h" + +@interface ACLStateTests : PFTestCase + +@end + +@implementation ACLStateTests + +- (void)testConstructors { + PFACLState *state = [[PFACLState alloc] init]; + XCTAssertEqualObjects(state.permissions, @{ }); + XCTAssertFalse(state.shared); + + PFMutableACLState *mutableState = [[PFMutableACLState alloc] init]; + mutableState.permissions[@"key"] = @"value"; + mutableState.shared = YES; + + state = [[PFACLState alloc] initWithState:mutableState]; + XCTAssertEqualObjects(state.permissions, @{ @"key": @"value" }); + XCTAssertTrue(state.shared); + + state = [PFACLState stateWithState:mutableState]; + XCTAssertEqualObjects(state.permissions, @{ @"key": @"value" }); + XCTAssertTrue(state.shared); + + state = [[PFACLState alloc] initWithState:[PFACLState new] mutatingBlock:^(PFMutableACLState *toMutate) { + toMutate.permissions[@"key"] = @"value"; + toMutate.shared = YES; + }]; + XCTAssertEqualObjects(state.permissions, @{ @"key": @"value" }); + XCTAssertTrue(state.shared); + + state = [PFACLState stateWithState:[PFACLState new] mutatingBlock:^(PFMutableACLState *toMutate) { + toMutate.permissions[@"key"] = @"value"; + toMutate.shared = YES; + }]; + XCTAssertEqualObjects(state.permissions, @{ @"key": @"value" }); + XCTAssertTrue(state.shared); +} + +- (void)testCopy { + PFMutableACLState *toCopy = [[PFMutableACLState alloc] init]; + toCopy.permissions[@"key"] = @"value"; + + PFACLState *newState = [toCopy copy]; + XCTAssertEqualObjects(toCopy, newState); + XCTAssertFalse([newState isKindOfClass:[PFMutableACLState class]]); + + newState = [toCopy mutableCopy]; + XCTAssertEqualObjects(toCopy, newState); + XCTAssertTrue([newState isKindOfClass:[PFMutableACLState class]]); + + newState = [toCopy copyByMutatingWithBlock:^(PFMutableACLState *newState) { + newState.shared = YES; + }]; + + XCTAssertEqualObjects(newState.permissions, toCopy.permissions); + XCTAssertTrue(newState.shared); +} + +@end diff --git a/Tests/Unit/ACLUnitTests.m b/Tests/Unit/ACLUnitTests.m new file mode 100644 index 000000000..bab67c273 --- /dev/null +++ b/Tests/Unit/ACLUnitTests.m @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFACLPrivate.h" +#import "PFMacros.h" +#import "PFObjectPrivate.h" +#import "PFRole.h" +#import "PFUnitTestCase.h" +#import "PFUserPrivate.h" + +@interface ACLUnitTests : PFUnitTestCase + +@end + +@implementation ACLUnitTests + +- (void)testConstructors { + id mockedUser = PFStrictClassMock([PFUser class]); + OCMStub([mockedUser objectId]).andReturn(@"1337"); + + PFACL *acl = [PFACL ACL]; + XCTAssertNotNil(acl); + XCTAssertFalse([acl getReadAccessForUser:mockedUser]); + XCTAssertFalse([acl getWriteAccessForUser:mockedUser]); + + acl = [PFACL ACLWithUser:mockedUser]; + XCTAssertNotNil(acl); + XCTAssertTrue([acl getReadAccessForUser:mockedUser]); + XCTAssertTrue([acl getWriteAccessForUser:mockedUser]); +} + +- (void)testShared { + PFACL *acl = [PFACL ACL]; + XCTAssertFalse(acl.isShared); + + [acl setShared:YES]; + XCTAssertTrue(acl.isShared); +} + +- (void)testPublicAccess { + PFACL *acl = [PFACL ACL]; + + XCTAssertFalse([acl getPublicReadAccess]); + XCTAssertFalse([acl getPublicWriteAccess]); + + [acl setPublicReadAccess:YES]; + XCTAssertTrue([acl getPublicReadAccess]); + + [acl setPublicWriteAccess:YES]; + XCTAssertTrue([acl getPublicWriteAccess]); +} + +- (void)testReadAccess { + PFRole *mockedRole = PFStrictClassMock([PFRole class]); + PFUser *mockedUser = PFStrictClassMock([PFUser class]); + + OCMStub(mockedRole.name).andReturn(@"aRoleName"); + OCMStub(mockedRole.objectId).andReturn(@"aRoleID"); + OCMStub(mockedUser.objectId).andReturn(@"aUserID"); + + PFACL *acl = [PFACL ACL]; + + XCTAssertFalse([acl getReadAccessForUserId:@"someUserID"]); + XCTAssertFalse([acl getReadAccessForUser:mockedUser]); + XCTAssertFalse([acl getReadAccessForRoleWithName:@"someRoleName"]); + XCTAssertFalse([acl getReadAccessForRole:mockedRole]); + + [acl setReadAccess:YES forUserId:@"someUserID"]; + XCTAssertTrue([acl getReadAccessForUserId:@"someUserID"]); + + [acl setReadAccess:YES forUser:mockedUser]; + XCTAssertTrue([acl getReadAccessForUser:mockedUser]); + + [acl setReadAccess:YES forRoleWithName:@"someRoleName"]; + XCTAssertTrue([acl getReadAccessForRoleWithName:@"someRoleName"]); + + [acl setReadAccess:YES forRole:mockedRole]; + XCTAssertTrue([acl getReadAccessForRole:mockedRole]); +} + +- (void)testWriteAccess { + PFRole *mockedRole = PFStrictClassMock([PFRole class]); + PFUser *mockedUser = PFStrictClassMock([PFUser class]); + + OCMStub(mockedRole.name).andReturn(@"aRoleName"); + OCMStub(mockedRole.objectId).andReturn(@"aRoleID"); + OCMStub(mockedUser.objectId).andReturn(@"aUserID"); + + PFACL *acl = [PFACL ACL]; + + XCTAssertFalse([acl getWriteAccessForUserId:@"someUserID"]); + XCTAssertFalse([acl getWriteAccessForUser:mockedUser]); + XCTAssertFalse([acl getWriteAccessForRoleWithName:@"someRoleName"]); + XCTAssertFalse([acl getWriteAccessForRole:mockedRole]); + + [acl setWriteAccess:YES forUserId:@"someUserID"]; + XCTAssertTrue([acl getWriteAccessForUserId:@"someUserID"]); + + [acl setWriteAccess:YES forUser:mockedUser]; + XCTAssertTrue([acl getWriteAccessForUser:mockedUser]); + + [acl setWriteAccess:YES forRoleWithName:@"someRoleName"]; + XCTAssertTrue([acl getWriteAccessForRoleWithName:@"someRoleName"]); + + [acl setWriteAccess:YES forRole:mockedRole]; + XCTAssertTrue([acl getWriteAccessForRole:mockedRole]); +} + +- (void)testLazyUser { + PFUser *lazyUser = PFStrictClassMock([PFUser class]); + + __block NSString *userId = nil; + + OCMStub(lazyUser.objectId).andDo(^(NSInvocation *invocation) { + [invocation setReturnValue:&userId]; + }); + OCMStub(lazyUser.isLazy).andReturn(YES); + + __block void (^saveListener)(id, NSError *) = nil; + + OCMStub([lazyUser registerSaveListener:[OCMArg checkWithBlock:^BOOL(id obj) { + saveListener = [obj copy]; + + return obj != nil; + }]]); + + OCMStub([lazyUser unregisterSaveListener:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:saveListener]; + }]]); + + PFACL *acl = [PFACL ACL]; + + XCTAssertFalse([acl hasUnresolvedUser]); + + [acl setReadAccess:YES forUser:lazyUser]; + [acl setWriteAccess:YES forUser:lazyUser]; + + XCTAssertTrue([acl hasUnresolvedUser]); + + XCTAssertTrue([acl getReadAccessForUser:lazyUser]); + XCTAssertTrue([acl getWriteAccessForUser:lazyUser]); + + XCTAssertFalse([acl getReadAccessForUserId:@"userID"]); + XCTAssertFalse([acl getWriteAccessForUserId:@"userID"]); + + userId = @"userID"; + + saveListener(lazyUser, nil); + + XCTAssertFalse([acl hasUnresolvedUser]); + XCTAssertTrue([acl getReadAccessForUserId:@"userID"]); + XCTAssertTrue([acl getWriteAccessForUserId:@"userID"]); +} + +- (void)testEquality { + PFACL *a = [PFACL ACL]; + PFACL *b = [PFACL ACL]; + + XCTAssertFalse([a isEqual:nil]); + XCTAssertFalse([a isEqual:@"Hello, World!"]); + + XCTAssertTrue([a isEqual:a]); + XCTAssertTrue([a isEqual:b]); + + [b setPublicWriteAccess:YES]; + + XCTAssertFalse([a isEqual:b]); +} + +- (void)testHash { + PFACL *acl = [PFACL ACL]; + NSUInteger oldHash = [acl hash]; + + [acl setReadAccess:YES forUserId:@"foo"]; + NSUInteger newHash = [acl hash]; + + XCTAssertNotEqual(oldHash, newHash); +} + +- (void)testCopy { + PFACL *aclA = [PFACL ACL]; + PFACL *aclB = [aclA copy]; + + XCTAssertNotEqual(aclA, aclB); + XCTAssertEqualObjects(aclA, aclB); + + [aclB setPublicWriteAccess:YES]; + + XCTAssertFalse([aclA getPublicWriteAccess]); +} + +- (void)testUnsharedCopy { + PFACL *sharedACL = [PFACL ACL]; + [sharedACL setShared:YES]; + [sharedACL setPublicReadAccess:YES]; + + PFACL *unsharedACL = [sharedACL createUnsharedCopy]; + XCTAssertFalse([unsharedACL isShared]); + XCTAssertTrue([unsharedACL getPublicReadAccess]); +} + +- (void)testDefaultACL { + PFACL *newACL = [PFACL ACL]; + [newACL setPublicReadAccess:YES]; + [newACL setShared:YES]; + + XCTAssertNotEqualObjects(newACL, [PFACL defaultACL]); + [PFACL setDefaultACL:newACL withAccessForCurrentUser:YES]; + XCTAssertEqualObjects(newACL, [PFACL defaultACL]); +} + +- (void)testACLRequiresObjectId { + [PFUser registerSubclass]; + + PFACL *acl = [PFACL ACL]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrowsSpecificNamed([acl setReadAccess:YES forUserId:nil], + NSException, + NSInvalidArgumentException, + @"Should not be able to give permissions to nil ids."); + XCTAssertThrowsSpecificNamed([acl setWriteAccess:YES forUserId:nil], + NSException, + NSInvalidArgumentException, + @"Should not be able to give permissions to nil ids."); +#pragma clang diagnostic pop + PFUser *user = [PFUser user]; + XCTAssertThrowsSpecificNamed([acl setReadAccess:YES forUser:user], + NSException, + NSInvalidArgumentException, + @"Should not be able to give permissions to unsaved users."); + XCTAssertThrowsSpecificNamed([acl setWriteAccess:YES forUser:user], + NSException, + NSInvalidArgumentException, + @"Should not be able to give permissions to unsaved users."); +} + +- (void)testNSCoding { + PFACL *acl = [PFACL ACL]; + [acl setReadAccess:NO forUserId:@"a"]; + [acl setReadAccess:YES forUserId:@"b"]; + [acl setWriteAccess:NO forUserId:@"c"]; + [acl setWriteAccess:YES forUserId:@"d"]; + + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:acl]; + XCTAssertTrue([data length] > 0, @"Encoded data should not be empty"); + + PFACL *decodedACL = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + XCTAssertFalse([decodedACL getReadAccessForUserId:@"a"], @"Decoded value should be the same as the encoded one."); + XCTAssertTrue([decodedACL getReadAccessForUserId:@"b"], @"Decoded value should be the same as the encoded one."); + XCTAssertFalse([decodedACL getWriteAccessForUserId:@"c"], @"Decoded value should be the same as the encoded one."); + XCTAssertTrue([decodedACL getWriteAccessForUserId:@"d"], @"Decoded value should be the same as the encoded one."); +} + +@end diff --git a/Tests/Unit/AlertViewTests.m b/Tests/Unit/AlertViewTests.m new file mode 100644 index 000000000..d555967fb --- /dev/null +++ b/Tests/Unit/AlertViewTests.m @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFAlertView.h" +#import "PFMacros.h" +#import "PFTestCase.h" +#import "PFTestSwizzlingUtilities.h" + +// Swizzling UIAlertController doesn't seem to work without these defined in UIAlertController itself. +@implementation UIAlertController (ClassOverrides) + ++ (Class)class { + return [super class]; +} + ++ (Class)_nilClass { + return nil; +} + +@end + +@interface AlertViewTests : PFTestCase + +@end + +@implementation AlertViewTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testShowAlertWithAlertViewController { + id mockedAlertController = PFStrictClassMock([UIAlertController class]); + id mockedApplication = PFStrictClassMock([UIApplication class]); + UIWindow *mockedWindow = PFStrictClassMock([UIWindow class]); + UIViewController *mockedViewController = PFStrictClassMock([UIViewController class]); + + // Using .andReturn() here will result in a retain cycle, which will cause our mocked shared application to + // persist across tests. + @weakify(mockedAlertController); + OCMStub(ClassMethod([[mockedAlertController ignoringNonObjectArgs] alertControllerWithTitle:@"Title" + message:@"Message" + preferredStyle:0])) + .andDo(^(NSInvocation *invocation) { + @strongify(mockedAlertController); + [invocation setReturnValue:&mockedAlertController]; + }); + + + @weakify(mockedApplication); + OCMStub(ClassMethod([mockedApplication sharedApplication])).andDo(^(NSInvocation *invocation) { + @strongify(mockedApplication); + [invocation setReturnValue:(void *)&mockedApplication]; + }); + + OCMStub([mockedApplication keyWindow]).andReturn(mockedWindow); + OCMStub(mockedWindow.rootViewController).andReturn(mockedViewController); + + NSMutableArray *actions = [NSMutableArray new]; + __block UIAlertAction *cancelAction = nil; + + id checker = [OCMArg checkWithBlock:^BOOL(UIAlertAction *obj) { + if ([obj.title isEqualToString:@"Cancel"] && obj.style == UIAlertActionStyleCancel) { + cancelAction = obj; + return YES; + } + + return ([obj.title isEqualToString:@"No"] && obj.style == UIAlertActionStyleDefault) || + ([obj.title isEqualToString:@"Yes"] && obj.style == UIAlertActionStyleDefault); + }]; + + OCMStub([mockedAlertController addAction:checker]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id action = nil; + [invocation getArgument:&action atIndex:2]; + + [actions addObject:action]; + }); + + OCMStub([mockedAlertController actions]).andReturn(actions); + + OCMExpect([mockedViewController presentViewController:mockedAlertController + animated:YES + completion:nil]).andDo(^(NSInvocation *invocation) { + // Private API here to make UIAlertAction completed. + void (^cancelActionHandler)(UIAlertAction *) = [cancelAction valueForKey:@"handler"]; + cancelActionHandler(cancelAction); + }); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFAlertView showAlertWithTitle:@"Title" + message:@"Message" + cancelButtonTitle:@"Cancel" + otherButtonTitles:@[ @"Yes", @"No" ] + completion:^(NSUInteger selectedOtherButtonIndex) { + XCTAssertEqual(selectedOtherButtonIndex, -1); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; + OCMVerifyAll(mockedAlertController); +} + +- (void)testShowWithoutAlertViewController { + id mockedAlertView = PFStrictClassMock([UIAlertView class]); + + PFTestSwizzledMethod *swizzledMethod = [PFTestSwizzlingUtilities swizzleClassMethod:@selector(class) + inClass:[UIAlertController class] + withMethod:@selector(_nilClass) + inClass:[UIAlertController class]]; + @try { + OCMStub([mockedAlertView alloc]).andReturn(mockedAlertView); + + __block __weak id delegate = nil; + + OCMExpect([mockedAlertView initWithTitle:@"Title" + message:@"Message" + delegate:OCMOCK_ANY + cancelButtonTitle:@"Cancel" + otherButtonTitles:nil]).andReturn(mockedAlertView); + + OCMExpect([mockedAlertView addButtonWithTitle:@"Yes"]); + OCMExpect([mockedAlertView addButtonWithTitle:@"No"]); + + + OCMExpect([mockedAlertView setDelegate:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id newDelegate = nil; + [invocation getArgument:&newDelegate atIndex:2]; + + delegate = newDelegate; + }); + + OCMExpect([mockedAlertView show]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained UIAlertView *self = nil; + [invocation getArgument:&self atIndex:0]; + + [delegate alertView:self clickedButtonAtIndex:0]; + }); + + OCMStub([mockedAlertView firstOtherButtonIndex]).andReturn(1); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFAlertView showAlertWithTitle:@"Title" + message:@"Message" + cancelButtonTitle:@"Cancel" + otherButtonTitles:@[ @"Yes", @"No" ] + completion:^(NSUInteger selectedOtherButtonIndex) { + XCTAssertEqual(selectedOtherButtonIndex, -1); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; + OCMVerifyAll(mockedAlertView); + } @finally { + [swizzledMethod setSwizzled:NO]; + } +} + +@end diff --git a/Tests/Unit/AnalyticsCommandTests.m b/Tests/Unit/AnalyticsCommandTests.m new file mode 100644 index 000000000..af121aff9 --- /dev/null +++ b/Tests/Unit/AnalyticsCommandTests.m @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFRESTAnalyticsCommand.h" +#import "PFTestCase.h" + +@interface AnalyticsCommandTests : PFTestCase + +@end + +@implementation AnalyticsCommandTests + +- (void)testTrackAppOpenedCommand { + PFRESTAnalyticsCommand *command = [PFRESTAnalyticsCommand trackAppOpenedEventCommandWithPushHash:nil + sessionToken:nil]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"events/AppOpened"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNil(command.parameters[@"push_hash"]); + XCTAssertNotNil(command.parameters); + XCTAssertNil(command.sessionToken); + + command = [PFRESTAnalyticsCommand trackAppOpenedEventCommandWithPushHash:@"yolo" sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"events/AppOpened"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters[@"push_hash"]); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +- (void)testTrackEventCommand { + PFRESTAnalyticsCommand *command = [PFRESTAnalyticsCommand trackEventCommandWithEventName:@"a" + dimensions:nil + sessionToken:nil]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"events/a"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters); + XCTAssertNil(command.sessionToken); + + command = [PFRESTAnalyticsCommand trackEventCommandWithEventName:@"a" + dimensions:@{ @"b" : @"c" } + sessionToken:@"d"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"events/a"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters); + XCTAssertNotNil(command.parameters[@"dimensions"]); + XCTAssertEqualObjects(command.sessionToken, @"d"); +} + +- (void)testCrashReportCommand { + PFRESTAnalyticsCommand *command = [PFRESTAnalyticsCommand trackCrashReportCommandWithBreakpadDumpParameters:@{ @"a" : @"yolo" } + sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"events/_CrashReport"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters[@"breakpadDump"]); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +@end diff --git a/Tests/Unit/AnalyticsControllerTests.m b/Tests/Unit/AnalyticsControllerTests.m new file mode 100644 index 000000000..81cbee7d4 --- /dev/null +++ b/Tests/Unit/AnalyticsControllerTests.m @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "BFTask+Private.h" +#import "PFAnalyticsController.h" +#import "PFCommandResult.h" +#import "PFEventuallyQueue.h" +#import "PFRESTCommand.h" +#import "PFTestCase.h" + +@interface AnalyticsControllerTests : PFTestCase + +@end + +@implementation AnalyticsControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFEventuallyQueue *)eventuallyQueueMockWithCommandResult:(PFCommandResult *)result { + BFTask *task = [BFTask taskWithResult:result]; + + id queueMock = PFClassMock([PFEventuallyQueue class]); + OCMStub([queueMock enqueueCommandInBackground:OCMOCK_ANY]).andReturn(task); + return queueMock; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + PFEventuallyQueue *queue = [self eventuallyQueueMockWithCommandResult:nil]; + + PFAnalyticsController *controller = [[PFAnalyticsController alloc] initWithEventuallyQueue:queue]; + XCTAssertNotNil(controller); + XCTAssertEqual(controller.eventuallyQueue, queue); + + controller = [PFAnalyticsController controllerWithEventuallyQueue:queue]; + XCTAssertNotNil(controller); + XCTAssertEqual(controller.eventuallyQueue, queue); +} + +- (void)testTrackEventWithInvalidParameters { + PFEventuallyQueue *queue = [self eventuallyQueueMockWithCommandResult:nil]; + PFAnalyticsController *controller = [PFAnalyticsController controllerWithEventuallyQueue:queue]; + + PFAssertThrowsInvalidArgumentException([controller trackEventAsyncWithName:nil dimensions:nil sessionToken:nil]); + PFAssertThrowsInvalidArgumentException([controller trackEventAsyncWithName:@" " dimensions:nil sessionToken:nil]); + PFAssertThrowsInvalidArgumentException([controller trackEventAsyncWithName:@"\n" dimensions:nil sessionToken:nil]); + PFAssertThrowsInvalidArgumentException([controller trackEventAsyncWithName:@"f" + dimensions:@{ @2: @"five" } + sessionToken:nil]); + PFAssertThrowsInvalidArgumentException([controller trackEventAsyncWithName:@"f" + dimensions:@{ @"num" : @5 } + sessionToken:nil]); +} + +- (void)testTrackEventParameters { + id queue = PFStrictClassMock([PFEventuallyQueue class]); + OCMExpect([queue enqueueCommandInBackground:[OCMArg checkWithBlock:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertNotEqual([command.httpPath rangeOfString:@"boom"].location, NSNotFound); + XCTAssertEqualObjects(command.parameters[@"dimensions"], @{ @"yarr" : @"yolo" }); + XCTAssertEqualObjects(command.sessionToken, @"argh"); + + return YES; + }]]); + + PFAnalyticsController *controller = [PFAnalyticsController controllerWithEventuallyQueue:queue]; + [[controller trackEventAsyncWithName:@"boom" + dimensions:@{ @"yarr" : @"yolo" } + sessionToken:@"argh"] waitUntilFinished]; + + OCMVerifyAll(queue); +} + +- (void)testTrackEventResult { + PFCommandResult *result = [PFCommandResult commandResultWithResult:@{} + resultString:nil + httpResponse:nil]; + PFEventuallyQueue *queue = [self eventuallyQueueMockWithCommandResult:result]; + PFAnalyticsController *controller = [PFAnalyticsController controllerWithEventuallyQueue:queue]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller trackEventAsyncWithName:@"a" + dimensions:nil + sessionToken:nil] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testTrackAppOpenedParameters { + id queue = PFStrictClassMock([PFEventuallyQueue class]); + OCMExpect([queue enqueueCommandInBackground:[OCMArg checkWithBlock:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertNotEqual([command.httpPath rangeOfString:@"AppOpened"].location, NSNotFound); + XCTAssertNotNil(command.parameters[@"push_hash"]); + XCTAssertEqualObjects(command.sessionToken, @"argh"); + + return YES; + }]]); + + PFAnalyticsController *controller = [PFAnalyticsController controllerWithEventuallyQueue:queue]; + [[controller trackAppOpenedEventAsyncWithRemoteNotificationPayload:@{ @"aps" : @{ @"alert" : @"yolo" } } + sessionToken:@"argh"] waitUntilFinished]; + + OCMVerifyAll(queue); +} + +- (void)testTrackAppOpenedResult { + PFCommandResult *result = [PFCommandResult commandResultWithResult:@{} + resultString:nil + httpResponse:nil]; + PFEventuallyQueue *queue = [self eventuallyQueueMockWithCommandResult:result]; + PFAnalyticsController *controller = [PFAnalyticsController controllerWithEventuallyQueue:queue]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller trackAppOpenedEventAsyncWithRemoteNotificationPayload:nil + sessionToken:nil] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/AnalyticsUnitTests.m b/Tests/Unit/AnalyticsUnitTests.m new file mode 100644 index 000000000..2a277c8a2 --- /dev/null +++ b/Tests/Unit/AnalyticsUnitTests.m @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFAnalyticsController.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface AnalyticsUnitTests : PFUnitTestCase + +@end + +@implementation AnalyticsUnitTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testTrackEvent { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + [[PFAnalytics trackEvent:@"yolo"] waitUntilFinished]; + OCMVerify([controllerMock trackEventAsyncWithName:[OCMArg isEqual:@"yolo"] + dimensions:[OCMArg isNil] + sessionToken:[OCMArg isNil]]); +} + +- (void)testTrackEventViaBlock { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + BFTask *task = [BFTask taskWithResult:@YES]; + OCMStub([controllerMock trackEventAsyncWithName:[OCMArg isEqual:@"yolo1"] + dimensions:[OCMArg isNil] + sessionToken:[OCMArg isNil]]).andReturn(task); + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFAnalytics trackEventInBackground:@"yolo1" block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testTrackEventWithDimensionsViaTask { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + NSDictionary *dimensions = @{ @"a" : @"b" }; + [[PFAnalytics trackEvent:@"yolo" dimensions:dimensions] waitUntilFinished]; + OCMVerify([controllerMock trackEventAsyncWithName:[OCMArg isEqual:@"yolo"] + dimensions:[OCMArg isEqual:dimensions] + sessionToken:[OCMArg isNil]]); +} + +- (void)testTrackEventWithDimensionsViaBlock { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + BFTask *task = [BFTask taskWithResult:@YES]; + OCMStub([controllerMock trackEventAsyncWithName:[OCMArg isEqual:@"yolo1"] + dimensions:[OCMArg isEqual:@{ @"c" : @"d" }] + sessionToken:[OCMArg isNil]]).andReturn(task); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFAnalytics trackEventInBackground:@"yolo1" dimensions:@{ @"c" : @"d" } block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testTrackEventNameValidation { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:nil block:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:nil dimensions:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:nil dimensions:nil block:nil]); +#pragma clang diagnostic pop + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:@" "]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:@" " block:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:@" " dimensions:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:@" " dimensions:nil block:nil]); + + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:@"\n"]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:@"\n" block:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:@"\n" dimensions:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:@"\n" dimensions:nil block:nil]); +} + +- (void)testTrackEventDimensionsValidation { + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:@"a" dimensions:@{ @2 : @"yolo" }]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEvent:@"a" dimensions:@{ @"yolo" : @2 }]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:@"a" + dimensions:@{ @"yolo" : @2 } + block:nil]); + PFAssertThrowsInvalidArgumentException([PFAnalytics trackEventInBackground:@"a" + dimensions:@{ @2 : @"yolo" } + block:nil]); +} + +- (void)testTrackAppOpenedWithLaunchOptionsViaTask { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + NSDictionary *notificationPayload = @{ @"aps" : @"yolo" }; +#if TARGET_OS_IPHONE + NSDictionary *launchOptions = @{ UIApplicationLaunchOptionsRemoteNotificationKey : notificationPayload }; +#else + NSDictionary *launchOptions = @{ NSApplicationLaunchUserNotificationKey : notificationPayload }; +#endif + + [[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions] waitUntilFinished]; + OCMVerify([controllerMock trackAppOpenedEventAsyncWithRemoteNotificationPayload:[OCMArg isEqual:notificationPayload] + sessionToken:[OCMArg isNil]]); +} + +- (void)testTrackAppOpenedWithLaunchOptionsViaBlock { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + NSDictionary *notificationPayload = @{ @"aps" : @"yolo" }; +#if TARGET_OS_IPHONE + NSDictionary *launchOptions = @{ UIApplicationLaunchOptionsRemoteNotificationKey : notificationPayload }; +#else + NSDictionary *launchOptions = @{ NSApplicationLaunchUserNotificationKey : notificationPayload }; +#endif + + BFTask *task = [BFTask taskWithResult:@YES]; + OCMStub([controllerMock trackAppOpenedEventAsyncWithRemoteNotificationPayload:[OCMArg isEqual:notificationPayload] + sessionToken:[OCMArg isNil]]).andReturn(task); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFAnalytics trackAppOpenedWithLaunchOptionsInBackground:launchOptions block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testTrackAppOpenedWithRemoteNotificationPayloadViaTask { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + NSDictionary *payload = @{ @"aps" : @"yolo" }; + + [[PFAnalytics trackAppOpenedWithRemoteNotificationPayload:payload] waitUntilFinished]; + OCMVerify([controllerMock trackAppOpenedEventAsyncWithRemoteNotificationPayload:[OCMArg isEqual:payload] + sessionToken:[OCMArg isNil]]); +} + +- (void)testTrackAppOpenedWithRemoteNotificationPayloadViaBlock { + id controllerMock = PFClassMock([PFAnalyticsController class]); + [Parse _currentManager].analyticsController = controllerMock; + + NSDictionary *payload = @{ @"aps" : @"yolo" }; + + BFTask *task = [BFTask taskWithResult:@YES]; + OCMStub([controllerMock trackAppOpenedEventAsyncWithRemoteNotificationPayload:[OCMArg isEqual:payload] + sessionToken:[OCMArg isNil]]).andReturn(task); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFAnalytics trackAppOpenedWithRemoteNotificationPayloadInBackground:payload block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/AnalyticsUtilitiesTests.m b/Tests/Unit/AnalyticsUtilitiesTests.m new file mode 100644 index 000000000..c8a234b9d --- /dev/null +++ b/Tests/Unit/AnalyticsUtilitiesTests.m @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFAnalyticsUtilities.h" +#import "PFHash.h" +#import "PFTestCase.h" + +@interface AnalyticsUtilitiesTests : PFTestCase + +@end + +@implementation AnalyticsUtilitiesTests + +- (void)testDigestSerializesPayload { + id payload = nil; + XCTAssertEqualObjects(PFMD5HashFromString(@""), + [PFAnalyticsUtilities md5DigestFromPushPayload:payload], + @"Digest of a nil payload should match digest of empty string"); + payload = @"derp"; + XCTAssertEqualObjects(PFMD5HashFromString(@"derp"), + [PFAnalyticsUtilities md5DigestFromPushPayload:payload], + @"Digest should match digest of raw string"); + payload = @{}; + XCTAssertEqualObjects(PFMD5HashFromString(@""), + [PFAnalyticsUtilities md5DigestFromPushPayload:payload], + @"Digest of an empty payload should match digest of empty string"); + payload = @{ @"body": @"there you go" }; + XCTAssertEqualObjects(PFMD5HashFromString(@"bodythere you go"), + [PFAnalyticsUtilities md5DigestFromPushPayload:payload], + @"Digest of an dictionary payload should match digest of flattened dictionary"); + payload = @{ @"body": @"woof", @"args": @[ @"arg one", @"arg two" ] }; + XCTAssertEqualObjects(PFMD5HashFromString(@"argsarg onearg twobodywoof"), + [PFAnalyticsUtilities md5DigestFromPushPayload:payload], + @"Digest of an dictionary payload should match digest of sorted, flattened dictionary"); +} + +@end diff --git a/Tests/Unit/AnonymousAuthenticationProviderTests.m b/Tests/Unit/AnonymousAuthenticationProviderTests.m new file mode 100644 index 000000000..c6459fa68 --- /dev/null +++ b/Tests/Unit/AnonymousAuthenticationProviderTests.m @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFAnonymousAuthenticationProvider.h" +#import "PFTestCase.h" + +@interface AnonymousAuthenticationProviderTests : PFTestCase + +@end + +@implementation AnonymousAuthenticationProviderTests + +- (void)testConstructors { + PFAnonymousAuthenticationProvider *provider = [[PFAnonymousAuthenticationProvider alloc] init]; + XCTAssertNotNil(provider); +} + +- (void)testAuthData { + PFAnonymousAuthenticationProvider *provider = [[PFAnonymousAuthenticationProvider alloc] init]; + + NSDictionary *authData = provider.authData; + XCTAssertNotNil(authData); + XCTAssertNotNil(authData[@"id"]); + XCTAssertNotEqualObjects(authData, provider.authData); +} + +- (void)testAuthType { + XCTAssertEqualObjects([PFAnonymousAuthenticationProvider authType], @"anonymous"); +} + +- (void)testAuthenticateAsync { + PFAnonymousAuthenticationProvider *provider = [[PFAnonymousAuthenticationProvider alloc] init]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[provider authenticateAsync] continueWithBlock:^id(BFTask *task) { + NSDictionary *authData = task.result; + XCTAssertNotNil(authData); + XCTAssertNotNil(authData[@"id"]); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testDeauthenticateAsync { + PFAnonymousAuthenticationProvider *provider = [[PFAnonymousAuthenticationProvider alloc] init]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[provider deauthenticateAsync] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.result); + XCTAssertFalse(task.faulted); + XCTAssertFalse(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testRestoreAuthentication { + PFAnonymousAuthenticationProvider *provider = [[PFAnonymousAuthenticationProvider alloc] init]; + XCTAssertTrue([provider restoreAuthenticationWithAuthData:@{ @"id" : @"123" }]); +} + +- (void)testRestoreAuthenticationWithNoData { + PFAnonymousAuthenticationProvider *provider = [[PFAnonymousAuthenticationProvider alloc] init]; + XCTAssertTrue([provider restoreAuthenticationWithAuthData:nil]); +} + +@end diff --git a/Tests/Unit/AnonymousUtilsTests.m b/Tests/Unit/AnonymousUtilsTests.m new file mode 100644 index 000000000..0315976c0 --- /dev/null +++ b/Tests/Unit/AnonymousUtilsTests.m @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "BFTask+Private.h" +#import "PFAnonymousUtils_Private.h" +#import "PFCoreManager.h" +#import "PFUnitTestCase.h" +#import "PFUserAuthenticationController.h" +#import "Parse_Private.h" + +@protocol AnonymousUtilsObserver + +- (void)callbackWithUser:(PFUser *)user error:(NSError *)error; + +@end + +@interface AnonymousUtilsTests : PFUnitTestCase + +@end + +@implementation AnonymousUtilsTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFUserAuthenticationController *)mockedUserAuthenticationController { + id controller = PFStrictClassMock([PFUserAuthenticationController class]); + [Parse _currentManager].coreManager.userAuthenticationController = controller; + return controller; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testInitialize { + id authController = [self mockedUserAuthenticationController]; + + __block id provider = nil; + OCMExpect([authController authenticationProviderForAuthType:@"anonymous"]); + OCMExpect([authController registerAuthenticationProvider:[OCMArg checkWithBlock:^BOOL(id obj) { + provider = obj; + return [[[obj class] authType] isEqualToString:@"anonymous"]; + }]]); + + provider = [PFAnonymousUtils _authenticationProvider]; + XCTAssertNotNil(provider); + + OCMStub([authController authenticationProviderForAuthType:@"anonymous"]).andReturn(provider); + XCTAssertEqual(provider, [PFAnonymousUtils _authenticationProvider]); + + OCMVerifyAll(authController); +} + +- (void)testLogInViaTask { + id authController = [self mockedUserAuthenticationController]; + OCMExpect([authController authenticationProviderForAuthType:@"anonymous"]).andReturn(nil); + OCMExpect([authController registerAuthenticationProvider:[OCMArg checkWithBlock:^BOOL(id obj) { + return [[[obj class] authType] isEqualToString:@"anonymous"]; + }]]); + + PFUser *user = [PFUser user]; + [OCMExpect([authController logInUserAsyncWithAuthType:@"anonymous"]) andReturn:[BFTask taskWithResult:user]]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFAnonymousUtils logInInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, user); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(authController); +} + +- (void)testLogInViaBlock { + id authController = [self mockedUserAuthenticationController]; + OCMExpect([authController authenticationProviderForAuthType:@"anonymous"]).andReturn(nil); + OCMExpect([authController registerAuthenticationProvider:[OCMArg checkWithBlock:^BOOL(id obj) { + return [[[obj class] authType] isEqualToString:@"anonymous"]; + }]]); + + PFUser *user = [PFUser user]; + [OCMExpect([authController logInUserAsyncWithAuthType:@"anonymous"]) andReturn:[BFTask taskWithResult:user]]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFAnonymousUtils logInWithBlock:^(PFUser *resultUser, NSError *error) { + XCTAssertEqual(resultUser, user); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(authController); +} + +- (void)testLogInViaTargetSelector { + id authController = [self mockedUserAuthenticationController]; + OCMExpect([authController authenticationProviderForAuthType:@"anonymous"]).andReturn(nil); + OCMExpect([authController registerAuthenticationProvider:[OCMArg checkWithBlock:^BOOL(id obj) { + return [[[obj class] authType] isEqualToString:@"anonymous"]; + }]]); + + PFUser *user = [PFUser user]; + [OCMExpect([authController logInUserAsyncWithAuthType:@"anonymous"]) andReturn:[BFTask taskWithResult:user]]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + id observer = PFStrictProtocolMock(@protocol(AnonymousUtilsObserver)); + OCMExpect([observer callbackWithUser:user error:nil]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + [PFAnonymousUtils logInWithTarget:observer selector:@selector(callbackWithUser:error:)]; + [self waitForTestExpectations]; + OCMVerifyAll(authController); +} + +@end diff --git a/Tests/Unit/BaseStateTests.m b/Tests/Unit/BaseStateTests.m new file mode 100644 index 000000000..8589606da --- /dev/null +++ b/Tests/Unit/BaseStateTests.m @@ -0,0 +1,311 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFBaseState.h" +#import "PFTestCase.h" + +@interface PFTestBaseStateSubclass : PFBaseState { +@public + __unsafe_unretained id _assignValue; +} + +@property (nonatomic, readwrite, strong) id defaultValue; + +@property (nonatomic, readwrite, unsafe_unretained) id assignValue; +@property (nonatomic, readwrite, weak) id weakValue; +@property (nonatomic, readwrite, strong) id strongValue; +@property (nonatomic, readwrite, copy) id copyValue NS_RETURNS_NOT_RETAINED; +@property (nonatomic, readwrite, copy) id mutableCopyValue NS_RETURNS_NOT_RETAINED; + +@end + +@implementation PFTestBaseStateSubclass + +- (id)init { + self = [super init]; + if (!self) return nil; + + _defaultValue = @"Hello, World!"; + + return self; +} + ++ (NSDictionary *)propertyAttributes { + return @{ + @"defaultValue" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + + @"assignValue" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeAssign], + @"weakValue" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeWeak], + @"strongValue" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeStrong], + @"copyValue" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeCopy], + @"mutableCopyValue" : [PFPropertyAttributes attributesWithAssociationType:PFPropertyInfoAssociationTypeMutableCopy], + }; +} + +- (id)nilValueForProperty:(NSString *)propertyName { + return nil; +} + +@end + +@interface BaseStateTests : PFTestCase + +@end + +@implementation BaseStateTests + +///-------------------------------------- +#pragma mark - Association Types +///-------------------------------------- + +- (void)testDefaultValues { + PFTestBaseStateSubclass *state = [[PFTestBaseStateSubclass alloc] init]; + XCTAssertEqualObjects(state.defaultValue, @"Hello, World!"); + + state.defaultValue = @"Different String"; + + PFTestBaseStateSubclass *newState = [PFTestBaseStateSubclass stateWithState:state]; + XCTAssertNotEqualObjects(newState.defaultValue, @"Hello, World!"); +} + +- (void)testAssignValue { + // Some random value. Don't treat this as an actual object! + __unsafe_unretained id theValue = (__bridge id)(void *)0xDEADBEEF; + + PFTestBaseStateSubclass *state = [[PFTestBaseStateSubclass alloc] init]; + state.assignValue = theValue; + + PFTestBaseStateSubclass *newState = [[PFTestBaseStateSubclass alloc] initWithState:state]; + + // Cannot use dot-syntax here. ARC is dumb and tries to retain it anyway. + __unsafe_unretained id valueA = state->_assignValue; + __unsafe_unretained id valueB = newState->_assignValue; + + XCTAssertEqual(valueA, valueB); + XCTAssertEqual(valueB, theValue); +} + +- (void)testWeakValue { + PFTestBaseStateSubclass *state, *newState; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" + + @autoreleasepool { + id theValue = [NSObject new]; + + state = [[PFTestBaseStateSubclass alloc] init]; + state.weakValue = theValue; + + newState = [[PFTestBaseStateSubclass alloc] initWithState:state]; + + XCTAssertEqual(state.weakValue, newState.weakValue); + XCTAssertEqual(state.weakValue, theValue); + + theValue = nil; + } + + OSMemoryBarrier(); + + XCTAssertEqual(state.weakValue, newState.weakValue); + XCTAssertEqual(state.weakValue, nil); +#pragma clang diagnostic pop +} + +- (void)testStrongValue { + __weak id weakValue; + + PFTestBaseStateSubclass *state, *newState; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" + + @autoreleasepool { + id theValue = [NSObject new]; + weakValue = theValue; + + state = [[PFTestBaseStateSubclass alloc] init]; + state.strongValue = theValue; + + newState = [[PFTestBaseStateSubclass alloc] initWithState:state]; + + XCTAssertEqual(state.strongValue, newState.strongValue); + XCTAssertEqual(state.strongValue, theValue); + } + + OSMemoryBarrier(); + + XCTAssertNotNil(weakValue); + XCTAssertEqual(state.strongValue, newState.strongValue); + XCTAssertEqual(state.strongValue, weakValue); +#pragma clang diagnostic pop +} + +- (void)testCopyValue { + NSMutableString *originalValue = [NSMutableString stringWithFormat:@"Foo"]; + + PFTestBaseStateSubclass *state, *newState; + + state = [[PFTestBaseStateSubclass alloc] init]; + state.copyValue = originalValue; + + newState = [[PFTestBaseStateSubclass alloc] initWithState:state]; + + XCTAssertNotEqual(state.copyValue, originalValue); + + // Copying a NSMutableString gives a NSString, which when copied again returns itself. + XCTAssertEqual(state.copyValue, newState.copyValue); + + XCTAssertEqualObjects(state.copyValue, originalValue); + XCTAssertEqualObjects(state.copyValue, newState.copyValue); + + // Same reason as above. + XCTAssertEqual([state.copyValue copy], [state.copyValue copy]); + XCTAssertEqual([newState.copyValue copy], [newState.copyValue copy]); +} + +- (void)testMutableCopyValue { + NSString *originalValue = @"Bar"; + + PFTestBaseStateSubclass *state, *newState; + + state = [[PFTestBaseStateSubclass alloc] init]; + state.mutableCopyValue = originalValue; + + newState = [[PFTestBaseStateSubclass alloc] initWithState:state]; + + XCTAssertNotEqual(state.mutableCopyValue, newState.mutableCopyValue); + + // Default copy setters won't invoke -mutableCopy. + XCTAssertEqual(originalValue, state.mutableCopyValue); + + XCTAssertEqualObjects(state.mutableCopyValue, newState.mutableCopyValue); + XCTAssertEqualObjects(state.mutableCopyValue, originalValue); + + XCTAssertThrows([state.mutableCopyValue appendString:@"Foo"]); + XCTAssertNoThrow([newState.mutableCopyValue appendString:@"Foo"]); +} + +///-------------------------------------- +#pragma mark - Description +///-------------------------------------- + +- (void)testDescription { + PFTestBaseStateSubclass *state = [[PFTestBaseStateSubclass alloc] init]; + state.strongValue = @15; + + NSString *oldDescription = [state description]; + + state.strongValue = @25; + + NSString *newDescritption = [state description]; + + XCTAssertNotEqualObjects(oldDescription, newDescritption); +} + +- (void)testDebugDescription { + PFTestBaseStateSubclass *state = [[PFTestBaseStateSubclass alloc] init]; + state.strongValue = @[ @1, @2, @3 ]; + + XCTAssertNotEqualObjects([state description], [state debugDescription]); +} + +///-------------------------------------- +#pragma mark - Dictionary Representation +///-------------------------------------- + +- (void)testDictionaryRepresentation { + PFTestBaseStateSubclass *state = [[PFTestBaseStateSubclass alloc] init]; + + NSMutableDictionary *expected = [@{ + @"defaultValue" : @"Hello, World!", + } mutableCopy]; + + XCTAssertEqualObjects(expected, [state dictionaryRepresentation]); + + state.strongValue = @25; + expected[@"strongValue"] = @25; + + XCTAssertEqualObjects(expected, [state dictionaryRepresentation]); +} + +// As this is a method only used when debugging, this simple of a test case should suffice. +- (void)testDebugQuickLookObject { + PFTestBaseStateSubclass *state = [[PFTestBaseStateSubclass alloc] init]; + + XCTAssertEqualObjects([[state dictionaryRepresentation] description], [state debugQuickLookObject]); +} + + +///-------------------------------------- +#pragma mark - Equality +///-------------------------------------- + +- (void)testEquality { + PFTestBaseStateSubclass *state1, *state2; + + state1 = [[PFTestBaseStateSubclass alloc] init]; + state2 = [[PFTestBaseStateSubclass alloc] init]; + + // Reasoning: XCTAssertEqualObjects checks for pointers for us already, so we dont' get 100% coverage without this. + XCTAssertTrue([state1 isEqual:state1]); + XCTAssertEqualObjects(state1, state2); + + XCTAssertNotEqualObjects(state1, nil); + XCTAssertNotEqualObjects(state1, @"Hello, World!"); + + state1.strongValue = @25; + + XCTAssertNotEqualObjects(state1, state2); + + state2.strongValue = @25; + + XCTAssertEqualObjects(state1, state2); +} + +///-------------------------------------- +#pragma mark - Comparison +///-------------------------------------- + +- (void)testCompare { + PFTestBaseStateSubclass *state1, *state2; + + state1 = [[PFTestBaseStateSubclass alloc] init]; + state2 = [[PFTestBaseStateSubclass alloc] init]; + + XCTAssertEqual([state1 compare:state2], NSOrderedSame); + + state1.strongValue = @25; + state2.strongValue = @20; + + XCTAssertEqual([state1 compare:state2], NSOrderedDescending); + XCTAssertEqual([state2 compare:state1], NSOrderedAscending); + + state2.strongValue = @30; + + XCTAssertEqual([state1 compare:state2], NSOrderedAscending); + XCTAssertEqual([state2 compare:state1], NSOrderedDescending); +} + +///-------------------------------------- +#pragma mark - Hashing +///-------------------------------------- + +- (void)testHash { + PFTestBaseStateSubclass *state = [[PFTestBaseStateSubclass alloc] init]; + NSUInteger oldHash = [state hash]; + + state.strongValue = @25; + + XCTAssertNotEqual(oldHash, [state hash]); +} + +@end diff --git a/Tests/Unit/BlockRetryerTests.m b/Tests/Unit/BlockRetryerTests.m new file mode 100644 index 000000000..0b16e97a2 --- /dev/null +++ b/Tests/Unit/BlockRetryerTests.m @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFBlockRetryer.h" +#import "PFTestCase.h" + +@interface BlockRetryerTests : PFTestCase + +@end + +@implementation BlockRetryerTests + +- (void)testInitialDelay { + NSTimeInterval delay = [PFBlockRetryer initialRetryDelay]; + [PFBlockRetryer setInitialRetryDelay:0.1]; + XCTAssertNotEqual(delay, [PFBlockRetryer initialRetryDelay]); +} + +- (void)testRetry { + __block NSUInteger counter = 0; + BFTask *task = [PFBlockRetryer retryBlock:^BFTask *{ + ++counter; + if (counter == 5) { + return [BFTask taskWithResult:@YES]; + } + return [BFTask taskWithError:[NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]]; + } forAttempts:5]; + [task waitUntilFinished]; + + XCTAssertEqual(counter, 5); + XCTAssertEqualObjects(task.result, @YES); +} + +- (void)testRetryWithError { + NSError *error = [NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]; + BFTask *task = [PFBlockRetryer retryBlock:^BFTask *{ + return [BFTask taskWithError:error]; + } forAttempts:5]; + [task waitUntilFinished]; + XCTAssertEqualObjects(task.error, error); +} + +@end diff --git a/Tests/Unit/CloudCodeControllerTests.m b/Tests/Unit/CloudCodeControllerTests.m new file mode 100644 index 000000000..81cb40175 --- /dev/null +++ b/Tests/Unit/CloudCodeControllerTests.m @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "OCMock+Parse.h" +#import "PFCloudCodeController.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFRESTCommand.h" +#import "PFTestCase.h" + +@interface CloudCodeControllerTests : PFTestCase + +@end + +@implementation CloudCodeControllerTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id runnerMock = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFCloudCodeController *controller = [[PFCloudCodeController alloc] initWithCommandRunner:runnerMock]; + XCTAssertNotNil(controller); + XCTAssertEqual(controller.commandRunner, runnerMock); + + controller = [PFCloudCodeController controllerWithCommandRunner:runnerMock]; + XCTAssertNotNil(controller); + XCTAssertEqual(controller.commandRunner, runnerMock); +} + +- (void)testCallCloudFunctionParameters { + id runner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + [runner mockCommandResult:nil forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertNotEqual([command.httpPath rangeOfString:@"yarr"].location, NSNotFound); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); + XCTAssertEqualObjects(command.parameters[@"a"], @1); + + return YES; + }]; + + PFCloudCodeController *controller = [[PFCloudCodeController alloc] initWithCommandRunner:runner]; + [[controller callCloudCodeFunctionAsync:@"yarr" + withParameters:@{ @"a" : @1 } + sessionToken:@"yolo"] waitUntilFinished]; + + OCMVerifyAll(runner); +} + +- (void)testCallCloudFunctionNilResult { + id runner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + [runner mockCommandResult:nil forCommandsPassingTest:^BOOL(id obj) { + return obj != nil; + }]; + + PFCloudCodeController *controller = [[PFCloudCodeController alloc] initWithCommandRunner:runner]; + + XCTestExpectation *nilResultExpectation = [self currentSelectorTestExpectation]; + [[controller callCloudCodeFunctionAsync:@"a" + withParameters:nil + sessionToken:@"c"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertNil(task.result); + [nilResultExpectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCallCloudFunctionResult { + id runner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + [runner mockCommandResult:@{ @"result" : @"yarr" } forCommandsPassingTest:^BOOL(id obj) { + return YES; + }]; + PFCloudCodeController *controller = [[PFCloudCodeController alloc] initWithCommandRunner:runner]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"proper result"]; + [[controller callCloudCodeFunctionAsync:@"a" + withParameters:nil + sessionToken:@"b"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertNotNil(task.result); + XCTAssertEqualObjects(task.result, @"yarr"); + [resultExpectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/CloudCommandTests.m b/Tests/Unit/CloudCommandTests.m new file mode 100644 index 000000000..45909dc41 --- /dev/null +++ b/Tests/Unit/CloudCommandTests.m @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFRESTCloudCommand.h" +#import "PFTestCase.h" + +@interface CloudCommandTests : PFTestCase + +@end + +@implementation CloudCommandTests + +- (void)testFunctionCommand { + PFRESTCloudCommand *command = [PFRESTCloudCommand commandForFunction:@"a" + withParameters:nil + sessionToken:nil]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"functions/a"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNil(command.parameters); + XCTAssertNil(command.sessionToken); + + command = [PFRESTCloudCommand commandForFunction:@"a" + withParameters:@{ @"b" : @"c" } + sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"functions/a"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters[@"b"]); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +@end diff --git a/Tests/Unit/CloudUnitTests.m b/Tests/Unit/CloudUnitTests.m new file mode 100644 index 000000000..dc1748cb2 --- /dev/null +++ b/Tests/Unit/CloudUnitTests.m @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "BFTask+Private.h" +#import "PFCloudCodeController.h" +#import "PFCoreManager.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@protocol CloudCodeMockedObserver + +- (void)callbackWithResult:(id)result error:(id)error; + +@end + +@interface CloudUnitTests : PFUnitTestCase + +@end + +@implementation CloudUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFCloudCodeController *)cloudCodeControllerWithResult:(id)result error:(NSError *)error { + BFTask *task = nil; + if (error) { + task = [BFTask taskWithError:error]; + } else { + task = [BFTask taskWithResult:result]; + } + + id controllerMock = PFClassMock([PFCloudCodeController class]); + OCMStub([controllerMock callCloudCodeFunctionAsync:OCMOCK_ANY + withParameters:OCMOCK_ANY + sessionToken:OCMOCK_ANY]).andReturn(task); + return controllerMock; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testCallFunction { + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:@{ @"a" : @"b" } + error:nil]; + id result = [PFCloud callFunction:@"a" withParameters:nil]; + XCTAssertEqualObjects(result, @{ @"a" : @"b" }); + + NSError *error = nil; + result = [PFCloud callFunction:@"a" withParameters:nil error:&error]; + XCTAssertEqualObjects(result, @{ @"a" : @"b" }); + XCTAssertNil(error); +} + +- (void)testCallFunctionError { + NSError *error = [NSError errorWithDomain:@"ParseTestDomain" code:100500 userInfo:nil]; + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:nil error:error]; + + id result = [PFCloud callFunction:@"a" withParameters:nil]; + XCTAssertNil(result); + + NSError *cloudError = nil; + result = [PFCloud callFunction:@"a" withParameters:nil error:&cloudError]; + XCTAssertEqualObjects(error, cloudError); + XCTAssertNil(result); +} + +- (void)testCallFunctionViaTask { + NSDictionary *result = @{ @"a" : @{@"b" : @"c"} }; + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:result error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFCloud callFunctionInBackground:@"yolo" withParameters:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, result); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCallFunctionViaBlock { + NSDictionary *result = @{ @"a" : @{@"b" : @"c"} }; + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:result error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFCloud callFunctionInBackground:@"yolo" withParameters:nil block:^(id cloudResult, NSError *error) { + XCTAssertEqualObjects(cloudResult, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testCallFunctionViaTargetSelector { + NSDictionary *result = @{ @"a" : @{@"b" : @"c"} }; + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:result error:nil]; + + id mock = PFProtocolMock(@protocol(CloudCodeMockedObserver)); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + OCMStub([mock callbackWithResult:[OCMArg isEqual:result] + error:[OCMArg isNil]]).andCall(expectation, @selector(fulfill)); + + [PFCloud callFunctionInBackground:@"yolo" + withParameters:nil + target:mock + selector:@selector(callbackWithResult:error:)]; + [self waitForTestExpectations]; +} + +- (void)testCallFunctionErrorViaTask { + NSError *error = [NSError errorWithDomain:@"ParseTestDomain" code:100500 userInfo:nil]; + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:nil error:error]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFCloud callFunctionInBackground:@"yolo" withParameters:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.error, error); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCallFunctionErrorViaBlock { + NSError *error = [NSError errorWithDomain:@"ParseTestDomain" code:100500 userInfo:nil]; + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:nil error:error]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFCloud callFunctionInBackground:@"yolo" withParameters:nil block:^(id result, NSError *cloudError) { + XCTAssertNil(result); + XCTAssertEqualObjects(error, cloudError); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testCallFunctionErrorViaTargetSelector { + NSError *error = [NSError errorWithDomain:@"ParseTestDomain" code:100500 userInfo:nil]; + [Parse _currentManager].coreManager.cloudCodeController = [self cloudCodeControllerWithResult:nil error:error]; + + id mock = PFProtocolMock(@protocol(CloudCodeMockedObserver)); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + OCMStub([mock callbackWithResult:[OCMArg isNil] + error:[OCMArg isEqual:error]]).andCall(expectation, @selector(fulfill)); + + [PFCloud callFunctionInBackground:@"yolo" + withParameters:nil + target:mock + selector:@selector(callbackWithResult:error:)]; + [self waitForTestExpectations]; +} + +- (void)testCallFunctionParameters { + id controllerMock = PFClassMock([PFCloudCodeController class]); + [Parse _currentManager].coreManager.cloudCodeController = controllerMock; + + NSDictionary *parameters = @{ @"a" : @{@"b" : @YES} }; + [PFCloud callFunction:@"yolo" withParameters:parameters]; + + OCMVerify([controllerMock callCloudCodeFunctionAsync:[OCMArg isEqual:@"yolo"] + withParameters:[OCMArg isEqual:parameters] + sessionToken:[OCMArg isNil]]); +} + +@end diff --git a/Tests/Unit/CommandResultTests.m b/Tests/Unit/CommandResultTests.m new file mode 100644 index 000000000..386b24d4b --- /dev/null +++ b/Tests/Unit/CommandResultTests.m @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFCommandResult.h" +#import "PFTestCase.h" + +@interface CommandResultTests : PFTestCase + +@end + +@implementation CommandResultTests + +- (void)testConstructors { + NSDictionary *result = @{ @"a" : @"b" }; + NSString *resultString = @"yolo"; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] init]; + + PFCommandResult *commandResult = [[PFCommandResult alloc] initWithResult:result + resultString:resultString + httpResponse:response]; + XCTAssertNotNil(commandResult); + XCTAssertEqualObjects(commandResult.result, result); + XCTAssertEqualObjects(commandResult.resultString, resultString); + XCTAssertEqualObjects(commandResult.httpResponse, response); + + commandResult = [PFCommandResult commandResultWithResult:result + resultString:resultString + httpResponse:response]; + XCTAssertNotNil(commandResult); + XCTAssertEqualObjects(commandResult.result, result); + XCTAssertEqualObjects(commandResult.resultString, resultString); + XCTAssertEqualObjects(commandResult.httpResponse, response); +} + +@end diff --git a/Tests/Unit/CommandURLRequestConstructorTests.m b/Tests/Unit/CommandURLRequestConstructorTests.m new file mode 100644 index 000000000..c30739847 --- /dev/null +++ b/Tests/Unit/CommandURLRequestConstructorTests.m @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCommandRunningConstants.h" +#import "PFCommandURLRequestConstructor.h" +#import "PFHTTPRequest.h" +#import "PFInstallationIdentifierStore.h" +#import "PFRESTCommand.h" +#import "PFTestCase.h" + +@interface CommandURLRequestConstructorTests : PFTestCase + +@end + +@implementation CommandURLRequestConstructorTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedInstallationidentifierStoreProviderWithInstallationIdentifier:(NSString *)identifier { + id providerMock = PFStrictProtocolMock(@protocol(PFInstallationIdentifierStoreProvider)); + id storeMock = PFStrictClassMock([PFInstallationIdentifierStore class]); + OCMStub([providerMock installationIdentifierStore]).andReturn(storeMock); + OCMStub([storeMock installationIdentifier]).andReturn(identifier); + return providerMock; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id providerMock = [self mockedInstallationidentifierStoreProviderWithInstallationIdentifier:nil]; + PFCommandURLRequestConstructor *constructor = [[PFCommandURLRequestConstructor alloc] initWithDataSource:providerMock]; + XCTAssertNotNil(constructor); + XCTAssertEqual((id)constructor.dataSource, providerMock); + + constructor = [PFCommandURLRequestConstructor constructorWithDataSource:providerMock]; + XCTAssertNotNil(constructor); + XCTAssertEqual((id)constructor.dataSource, providerMock); +} + +- (void)testDataURLRequest { + id providerMock = [self mockedInstallationidentifierStoreProviderWithInstallationIdentifier:@"installationId"]; + PFCommandURLRequestConstructor *constructor = [[PFCommandURLRequestConstructor alloc] initWithDataSource:providerMock]; + + PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"yolo" + httpMethod:PFHTTPRequestMethodPOST + parameters:@{ @"a" : @"b" } + sessionToken:@"yarr"]; + command.additionalRequestHeaders = @{ @"CustomHeader" : @"CustomValue" }; + NSURLRequest *request = [constructor dataURLRequestForCommand:command]; + XCTAssertTrue([[request.URL absoluteString] containsString:@"/1/yolo"]); + XCTAssertEqualObjects(request.allHTTPHeaderFields, (@{ PFCommandHeaderNameInstallationId : @"installationId", + PFCommandHeaderNameSessionToken : @"yarr", + PFHTTPRequestHeaderNameContentType : @"application/json; charset=utf8", + @"CustomHeader" : @"CustomValue" })); + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + XCTAssertNotNil(request.HTTPBody); +} + +- (void)testDataURLRequestMethodOverride { + id providerMock = [self mockedInstallationidentifierStoreProviderWithInstallationIdentifier:@"installationId"]; + PFCommandURLRequestConstructor *constructor = [[PFCommandURLRequestConstructor alloc] initWithDataSource:providerMock]; + + PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"yolo" + httpMethod:PFHTTPRequestMethodGET + parameters:@{ @"a" : @"b" } + sessionToken:@"yarr"]; + NSURLRequest *request = [constructor dataURLRequestForCommand:command]; + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + + command = [PFRESTCommand commandWithHTTPPath:@"yolo" + httpMethod:PFHTTPRequestMethodHEAD + parameters:@{ @"a" : @"b" } + sessionToken:@"yarr"]; + request = [constructor dataURLRequestForCommand:command]; + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + + command = [PFRESTCommand commandWithHTTPPath:@"yolo" + httpMethod:PFHTTPRequestMethodGET + parameters:@{ @"a" : @"b" } + sessionToken:@"yarr"]; + request = [constructor dataURLRequestForCommand:command]; + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + + command = [PFRESTCommand commandWithHTTPPath:@"yolo" + httpMethod:PFHTTPRequestMethodGET + parameters:nil + sessionToken:@"yarr"]; + request = [constructor dataURLRequestForCommand:command]; + XCTAssertEqualObjects(request.HTTPMethod, @"GET"); +} + +- (void)testDataURLRequestBodyEncoding { + id providerMock = [self mockedInstallationidentifierStoreProviderWithInstallationIdentifier:@"installationId"]; + PFCommandURLRequestConstructor *constructor = [[PFCommandURLRequestConstructor alloc] initWithDataSource:providerMock]; + + PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"yolo" + httpMethod:PFHTTPRequestMethodPOST + parameters:@{ @"a" : @100500 } + sessionToken:@"yarr"]; + NSURLRequest *request = [constructor dataURLRequestForCommand:command]; + id json = [NSJSONSerialization JSONObjectWithData:request.HTTPBody options:0 error:nil]; + XCTAssertNotNil(json); + XCTAssertEqualObjects(json, @{ @"a" : @100500 }); +} + +- (void)testFileUploadURLRequest { + id providerMock = [self mockedInstallationidentifierStoreProviderWithInstallationIdentifier:@"installationId"]; + PFCommandURLRequestConstructor *constructor = [[PFCommandURLRequestConstructor alloc] initWithDataSource:providerMock]; + + PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"yolo" + httpMethod:PFHTTPRequestMethodPOST + parameters:@{ @"a" : @100500 } + sessionToken:@"yarr"]; + NSURLRequest *request = [constructor fileUploadURLRequestForCommand:command + withContentType:@"boom" + contentSourceFilePath:@"/dev/null"]; + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.allHTTPHeaderFields[PFHTTPRequestHeaderNameContentType], @"boom"); +} + +- (void)testDefaultURLRequestHeaders { + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSDictionary *headers = [PFCommandURLRequestConstructor defaultURLRequestHeadersForApplicationId:@"a" + clientKey:@"b" + bundle:bundle]; + XCTAssertNotNil(headers); + XCTAssertEqualObjects(headers[PFCommandHeaderNameApplicationId], @"a"); + XCTAssertEqualObjects(headers[PFCommandHeaderNameClientKey], @"b"); + XCTAssertNotNil(headers[PFCommandHeaderNameClientVersion]); + XCTAssertNotNil(headers[PFCommandHeaderNameOSVersion]); + XCTAssertNotNil(headers[PFCommandHeaderNameAppBuildVersion]); + XCTAssertNotNil(headers[PFCommandHeaderNameAppDisplayVersion]); +} + +@end diff --git a/Tests/Unit/CommandUnitTests.m b/Tests/Unit/CommandUnitTests.m new file mode 100644 index 000000000..c56b02826 --- /dev/null +++ b/Tests/Unit/CommandUnitTests.m @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "BFTask+Private.h" +#import "PFHTTPRequest.h" +#import "PFJSONSerialization.h" +#import "PFMockURLProtocol.h" +#import "PFRESTCommand.h" +#import "PFURLSessionCommandRunner.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface CommandUnitTests : PFUnitTestCase + +@end + +@implementation CommandUnitTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [Parse _currentManager].commandRunner.initialRetryDelay = 0.001; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testNoRetryOn400StatusCode { + __block NSUInteger retryCount = 0; + [PFMockURLProtocol mockRequestsWithResponse:^PFMockURLResponse *(NSURLRequest *request) { + retryCount++; + NSDictionary *response = @{ @"error" : @"yarr", + @"code" : @100500 }; + NSString *json = [PFJSONSerialization stringFromJSONObject:response]; + return [PFMockURLResponse responseWithString:json statusCode:400 delay:0.0]; + }]; + + PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"login" + httpMethod:PFHTTPRequestMethodPOST + parameters:nil + sessionToken:nil]; + + NSError *error = nil; + PFURLSessionCommandRunner *commandRunner = [PFURLSessionCommandRunner commandRunnerWithDataSource:[Parse _currentManager] + applicationId:[Parse getApplicationId] + clientKey:[Parse getClientKey]]; + [[commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed] waitForResult:&error]; + + XCTAssertEqualObjects(@"yarr", error.userInfo[@"error"]); + XCTAssertEqual(100500, error.code); + XCTAssertEqual(retryCount, 1); + + [PFMockURLProtocol removeAllMocking]; +} + +- (void)testRetryOn500StatusCode { + __block NSUInteger retryCount = 0; + [PFMockURLProtocol mockRequestsWithResponse:^PFMockURLResponse *(NSURLRequest *request) { + retryCount++; + NSDictionary *response = @{ @"error" : @"yarr", + @"code" : @100500 }; + NSString *json = [PFJSONSerialization stringFromJSONObject:response]; + return [PFMockURLResponse responseWithString:json statusCode:500 delay:0.0]; + }]; + + PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"login" + httpMethod:PFHTTPRequestMethodPOST + parameters:nil + sessionToken:nil]; + + NSError *error = nil; + PFURLSessionCommandRunner *commandRunner = [PFURLSessionCommandRunner commandRunnerWithDataSource:[Parse _currentManager] + applicationId:[Parse getApplicationId] + clientKey:[Parse getClientKey]]; + commandRunner.initialRetryDelay = DBL_MIN; + [[commandRunner runCommandAsync:command + withOptions:PFCommandRunningOptionRetryIfFailed] waitForResult:&error]; + + XCTAssertEqualObjects(@"yarr", error.userInfo[@"error"]); + XCTAssertEqual(100500, error.code); + XCTAssertEqual(retryCount, 5); + + [PFMockURLProtocol removeAllMocking]; +} + +- (void)testCacheKeysFromCommand { + NSMutableDictionary *orderedDict = [NSMutableDictionary dictionary]; + for (int i = 1; i <= 30; ++i) { + [orderedDict setObject:[NSString stringWithFormat:@"value%d", i] + forKey:[NSString stringWithFormat:@"key%d", i]]; + } + PFRESTCommand *orderedCommand = [PFRESTCommand commandWithHTTPPath:@"foo" + httpMethod:PFHTTPRequestMethodGET + parameters:orderedDict + sessionToken:nil]; + + NSMutableDictionary *reversedDict = [NSMutableDictionary dictionary]; + for (int i = 30; i >= 1; --i) { + [reversedDict setObject:[NSString stringWithFormat:@"value%d", i] + forKey:[NSString stringWithFormat:@"key%d", i]]; + } + PFRESTCommand *reversedCommand = [PFRESTCommand commandWithHTTPPath:@"foo" + httpMethod:PFHTTPRequestMethodGET + parameters:reversedDict + sessionToken:nil]; + + XCTAssertEqualObjects(orderedCommand.cacheKey, reversedCommand.cacheKey, + @"identifiers should be invariant to dictionary key orders"); +} + +@end diff --git a/Tests/Unit/ConfigCommandTests.m b/Tests/Unit/ConfigCommandTests.m new file mode 100644 index 000000000..590c7d7c2 --- /dev/null +++ b/Tests/Unit/ConfigCommandTests.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFRESTConfigCommand.h" +#import "PFTestCase.h" + +@interface ConfigCommandTests : PFTestCase + +@end + +@implementation ConfigCommandTests + +- (void)testConfigFetchCommand { + PFRESTConfigCommand *command = [PFRESTConfigCommand configFetchCommandWithSessionToken:@"a"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"config"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.sessionToken, @"a"); +} + +- (void)testConfigUpdateCommand { + PFRESTConfigCommand *command = [PFRESTConfigCommand configUpdateCommandWithConfigParameters:@{ @"a" : @"b" } + sessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"config"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPUT); + XCTAssertNotNil(command.parameters[@"params"][@"a"]); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +@end diff --git a/Tests/Unit/ConfigControllerTests.m b/Tests/Unit/ConfigControllerTests.m new file mode 100644 index 000000000..ee06df659 --- /dev/null +++ b/Tests/Unit/ConfigControllerTests.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "OCMock+Parse.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFConfig.h" +#import "PFConfigController.h" +#import "PFFileManager.h" +#import "PFTestCase.h" + +@interface ConfigControllerTests : PFTestCase + +@end + +@implementation ConfigControllerTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructor { + id mockedFileManager = PFClassMock([PFFileManager class]); + id mockedCommandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFConfigController *controller = [[PFConfigController alloc] initWithFileManager:mockedFileManager + commandRunner:mockedCommandRunner]; + + XCTAssertNotNil(controller); + XCTAssertEqual(mockedFileManager, controller.fileManager); + XCTAssertEqual(mockedCommandRunner, controller.commandRunner); +} + +- (void)testCurrentConfigController { + id fileManager = PFClassMock([PFFileManager class]); + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFConfigController *configController = [[PFConfigController alloc] initWithFileManager:fileManager + commandRunner:commandRunner]; + + XCTAssertNotNil([configController currentConfigController]); +} + +- (void)testFetch { + id fileManager = PFClassMock([PFFileManager class]);; + + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + [commandRunner mockCommandResult:@{ @"params" : @{@"testKey" : @"testValue"} } + forCommandsPassingTest:^BOOL(id obj) { + return YES; + }]; + + PFConfigController *configController = [[PFConfigController alloc] initWithFileManager:fileManager + commandRunner:commandRunner]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[configController fetchConfigAsyncWithSessionToken:@"token"] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.result); + + XCTAssertTrue([task.result isKindOfClass:[PFConfig class]]); + XCTAssertEqualObjects(task.result[@"testKey"], @"testValue"); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/ConfigUnitTests.m b/Tests/Unit/ConfigUnitTests.m new file mode 100644 index 000000000..e4e16c462 --- /dev/null +++ b/Tests/Unit/ConfigUnitTests.m @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFConfigController.h" +#import "PFConfig_Private.h" +#import "PFCoreManager.h" +#import "PFCurrentConfigController.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface ConfigUnitTests : PFUnitTestCase + +@property (nonatomic, strong) PFConfigController *mockedConfigController; + +@end + +@implementation ConfigUnitTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [Parse _currentManager].coreManager.configController = self.mockedConfigController; +} + +- (void)tearDown { + self.mockedConfigController = nil; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFConfigController *)mockedConfigController { + if (_mockedConfigController == nil) { + _mockedConfigController = PFStrictClassMock([PFConfigController class]); + + BFTask *mockedTask = [BFTask taskWithResult:[self sampleConfig]]; + OCMStub([_mockedConfigController fetchConfigAsyncWithSessionToken:nil]).andReturn(mockedTask); + + PFCurrentConfigController *mockedCurrentController = PFStrictClassMock([PFCurrentConfigController class]); + OCMStub([_mockedConfigController currentConfigController]).andReturn(mockedCurrentController); + OCMStub([mockedCurrentController getCurrentConfigAsync]).andReturn(mockedTask); + } + + return _mockedConfigController; +} + +- (PFConfig *)sampleConfig { + return [[PFConfig alloc] initWithFetchedConfig:@{ @"params": @{ @"testKey": @"testValue" } }]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testCurrentConfig { + PFConfig *config = [PFConfig currentConfig]; + + XCTAssertEqualObjects(config[@"testKey"], @"testValue"); + XCTAssertEqualObjects([config objectForKey:@"testKey"], @"testValue"); +} + +- (void)testGetConfig { + PFConfig *config = [PFConfig getConfig]; + + XCTAssertEqualObjects(config[@"testKey"], @"testValue"); + XCTAssertEqualObjects(config, [self sampleConfig]); +} + +- (void)testGetConfigInBackground { + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [PFConfig getConfigInBackgroundWithBlock:^(PFConfig *config, NSError *error) { + XCTAssertNotNil(config); + + XCTAssertEqualObjects(config[@"testKey"], @"testValue"); + XCTAssertEqualObjects(config, [self sampleConfig]); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; +} + +- (void)testEquality { + PFConfig *config = [self sampleConfig]; + + PFConfig *currentConfig = [PFConfig currentConfig]; + PFConfig *thirdConfig = [[PFConfig alloc] init]; + + XCTAssertEqual([config hash], [currentConfig hash]); + XCTAssertNotEqual([config hash], [thirdConfig hash]); + + XCTAssertEqualObjects(config, currentConfig); + XCTAssertNotEqualObjects(config, thirdConfig); + XCTAssertNotEqualObjects(config, @"Hello World!"); +} + +@end diff --git a/Tests/Unit/CurrentConfigControllerTests.m b/Tests/Unit/CurrentConfigControllerTests.m new file mode 100644 index 000000000..01a10fdd7 --- /dev/null +++ b/Tests/Unit/CurrentConfigControllerTests.m @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCommandResult.h" +#import "PFConfig.h" +#import "PFConfig_Private.h" +#import "PFCurrentConfigController.h" +#import "PFFileManager.h" +#import "PFTestCase.h" + +@interface CurrentConfigControllerTests : PFTestCase + +@end + +@implementation CurrentConfigControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (NSDictionary *)testConfigDictionary { + return @{ @"params" : @{@"testKey" : @"testValue"} }; +} + +- (PFFileManager *)mockedFileManagerWithConfigPath:(NSString *)path { + id fileManager = PFPartialMock([[PFFileManager alloc] initWithApplicationIdentifier:OCMOCK_ANY + applicationGroupIdentifier:@"com.parse.test"]); + + OCMStub([fileManager parseDataItemPathForPathComponent:OCMOCK_ANY]).andReturn(path); + + return fileManager; +} + +- (NSString *)configPathForSelector:(SEL)cmd { + NSString *configPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:NSStringFromSelector(cmd)] + stringByAppendingPathExtension:@"config"]; + + [[NSFileManager defaultManager] removeItemAtPath:configPath error:NULL]; + + return configPath; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructor { + id mockedFileManager = PFClassMock([PFFileManager class]); + + PFCurrentConfigController *controller = [[PFCurrentConfigController alloc] initWithFileManager:mockedFileManager]; + + XCTAssertNotNil(controller); + XCTAssertEqual(controller.fileManager, mockedFileManager); +} + +- (void)testGetCurrentConfig { + NSString *configPath = [self configPathForSelector:_cmd]; + + NSOutputStream *outputStream = [NSOutputStream outputStreamToFileAtPath:configPath append:NO]; + [outputStream open]; + + NSError *error = nil; + [NSJSONSerialization writeJSONObject:[self testConfigDictionary] + toStream:outputStream + options:0 + error:&error]; + + [outputStream close]; + XCTAssertNil(error); + + PFFileManager *fileManager = [self mockedFileManagerWithConfigPath:configPath]; + PFCurrentConfigController *currentController = [PFCurrentConfigController controllerWithFileManager:fileManager]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[currentController getCurrentConfigAsync] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.result); + XCTAssertTrue([task.result isKindOfClass:[PFConfig class]]); + XCTAssertEqualObjects(task.result[@"testKey"], @"testValue"); + + [expectation fulfill]; + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testSetCurrentConfig { + NSString *configPath = [self configPathForSelector:_cmd]; + PFConfig *testConfig = [[PFConfig alloc] initWithFetchedConfig:[self testConfigDictionary]]; + + PFFileManager *fileManager = [self mockedFileManagerWithConfigPath:configPath]; + PFCurrentConfigController *currentController = [PFCurrentConfigController controllerWithFileManager:fileManager]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[currentController setCurrentConfigAsync:testConfig] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + NSData *data = [NSData dataWithContentsOfFile:configPath]; + XCTAssertNotNil(data); + + NSDictionary *contentsOfFile = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:NULL]; + XCTAssertEqualObjects(contentsOfFile, [self testConfigDictionary]); +} + +- (void)testClearCurrentConfig { + NSString *configPath = [self configPathForSelector:_cmd]; + PFConfig *testConfig = [[PFConfig alloc] initWithFetchedConfig:[self testConfigDictionary]]; + + PFFileManager *fileManager = [self mockedFileManagerWithConfigPath:configPath]; + PFCurrentConfigController *currentController = [PFCurrentConfigController controllerWithFileManager:fileManager]; + XCTestExpectation *saveExpectation = [self expectationWithDescription:@"Save"]; + + [[currentController setCurrentConfigAsync:testConfig] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [saveExpectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + XCTestExpectation *clearExpectation = [self expectationWithDescription:@"Clear"]; + + [[currentController clearCurrentConfigAsync] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [clearExpectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + NSData *data = [NSData dataWithContentsOfFile:configPath]; + XCTAssertNil(data); +} + +- (void)testClearMemoryCachedCurrentConfig { + NSString *configPath = [self configPathForSelector:_cmd]; + PFConfig *testConfig = [[PFConfig alloc] initWithFetchedConfig:[self testConfigDictionary]]; + + PFFileManager *fileManager = [self mockedFileManagerWithConfigPath:configPath]; + PFCurrentConfigController *currentController = [PFCurrentConfigController controllerWithFileManager:fileManager]; + XCTestExpectation *saveExpectation = [self expectationWithDescription:@"Save"]; + + [[currentController setCurrentConfigAsync:testConfig] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [saveExpectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + XCTestExpectation *clearExpectation = [self expectationWithDescription:@"Clear"]; + + [[currentController clearMemoryCachedCurrentConfigAsync] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [clearExpectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + NSData *data = [NSData dataWithContentsOfFile:configPath]; + XCTAssertNotNil(data); + + // Ideally here we would check to ensure that we re-read from the path. However, you cannot re-stub + // the same method using OCMock (Ugh), so for now just assume that it properly removed the current config. +} + +@end diff --git a/Tests/Unit/DateFormatterTests.m b/Tests/Unit/DateFormatterTests.m new file mode 100644 index 000000000..16cb45cb2 --- /dev/null +++ b/Tests/Unit/DateFormatterTests.m @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDateFormatter.h" +#import "PFTestCase.h" + +@interface DateFormatterTests : PFTestCase + +@end + +@implementation DateFormatterTests + +- (void)testConstructors { + PFDateFormatter *formatter = [[PFDateFormatter alloc] init]; + XCTAssertNotNil(formatter); +} + +- (void)testSharedFormatter { + PFDateFormatter *formatter = [PFDateFormatter sharedFormatter]; + XCTAssertNotNil(formatter); + XCTAssertEqual(formatter, [PFDateFormatter sharedFormatter]); +} + +- (void)testDateDeserializationIsInvertible { + PFDateFormatter *formatter = [[PFDateFormatter alloc] init]; + for (int i = 0; i < 5000; ++i) { + NSDate *date = [NSDate dateWithTimeIntervalSince1970:arc4random_uniform(1387152000)]; + + NSString *iso = [formatter preciseStringFromDate:date]; + + NSDate *dateAgain = [formatter dateFromString:iso]; + NSString *isoAgain = [formatter preciseStringFromDate:dateAgain]; + XCTAssertEqualObjects(iso, isoAgain); + } +} + +- (void)testPreciseDateFormatterConversions { + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + + for (int i = 0; i < 5000; ++i) { + NSDate *properDate = [NSDate dateWithTimeIntervalSince1970:arc4random_uniform(1387152000)]; + NSString *properString = [dateFormatter stringFromDate:properDate]; + + NSString *string = [[PFDateFormatter sharedFormatter] preciseStringFromDate:properDate]; + XCTAssertEqualObjects(properString, string); + + NSDate *date = [[PFDateFormatter sharedFormatter] dateFromString:properString]; + XCTAssertEqualObjects(properDate, date); + } +} + +@end diff --git a/Tests/Unit/DecoderTests.m b/Tests/Unit/DecoderTests.m new file mode 100644 index 000000000..b07af89b5 --- /dev/null +++ b/Tests/Unit/DecoderTests.m @@ -0,0 +1,256 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDecoder.h" +#import "PFFieldOperation.h" +#import "PFFile.h" +#import "PFGeoPoint.h" +#import "PFObjectPrivate.h" +#import "PFRelationPrivate.h" +#import "PFTestCase.h" + +@interface DecoderTests : PFTestCase + +@end + +@implementation DecoderTests + +- (void)testConstructors { + PFDecoder *decoder = [[PFDecoder alloc] init]; + XCTAssertNotNil(decoder); +} + +- (void)testDefaultObjectDecoder { + PFDecoder *decoder = [PFDecoder objectDecoder]; + XCTAssertNotNil(decoder); + XCTAssertEqual(decoder, [PFDecoder objectDecoder]); +} + +- (void)testDecodingFieldOperations { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"key" : @{@"__op" : @"Increment", + @"amount" : @100500} }]; + XCTAssertNotNil(decoded); + + PFIncrementOperation *operation = decoded[@"key"]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFIncrementOperation class]); + XCTAssertEqualObjects(operation.amount, @100500); +} + +- (void)testDecodingDates { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"date" : @{@"__type" : @"Date", + @"iso" : @"1970-01-01T00:00:01.000Z"} }]; + XCTAssertNotNil(decoded); + + NSDate *date = decoded[@"date"]; + XCTAssertNotNil(date); + PFAssertIsKindOfClass(date, [NSDate class]); + XCTAssertEqualObjects(date, [NSDate dateWithTimeIntervalSince1970:1.0]); +} + +- (void)testDecodingBytes { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"data" : @{@"__type" : @"Bytes", + @"base64" : @"eW9sbw=="} }]; + XCTAssertNotNil(decoded); + + NSData *data = decoded[@"data"]; + XCTAssertNotNil(data); + PFAssertIsKindOfClass(data, [NSData class]); + + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(string, @"yolo"); +} + +- (void)testDecodingGeoPoints { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"geoPoint" : @{@"__type" : @"GeoPoint", + @"latitude" : @10, + @"longitude" : @20} }]; + XCTAssertNotNil(decoded); + + PFGeoPoint *geoPoint = decoded[@"geoPoint"]; + XCTAssertNotNil(geoPoint); + PFAssertIsKindOfClass(geoPoint, [PFGeoPoint class]); + + XCTAssertEqualObjects(geoPoint, [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]); +} + +- (void)testDecodingRelations { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + NSDictionary *decoded = [decoder decodeObject:@{ @"relation" : @{@"__type" : @"Relation", + @"className" : @"Yolo", + @"objects" : @[ object ]} + }]; + XCTAssertNotNil(decoded); + + PFRelation *relation = decoded[@"relation"]; + XCTAssertNotNil(relation); + PFAssertIsKindOfClass(relation, [PFRelation class]); + + XCTAssertEqualObjects(relation.targetClass, @"Yolo"); + XCTAssertTrue([relation _hasKnownObject:object]); +} + +- (void)testDecodingFiles { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"file" : @{@"__type" : @"File", + @"name" : @"yolo.png", + @"url" : @"http://yarr.com/yolo.png"} }]; + XCTAssertNotNil(decoded); + + PFFile *file = decoded[@"file"]; + XCTAssertNotNil(file); + PFAssertIsKindOfClass(file, [PFFile class]); + + XCTAssertEqualObjects(file.name, @"yolo.png"); + XCTAssertEqualObjects(file.url, @"http://yarr.com/yolo.png"); +} + +- (void)testDecodingPointers { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"pointer1" : @{@"__type" : @"Pointer", + @"className" : @"Yolo", + @"objectId" : @"123"}, + @"pointer2" : @{@"__type" : @"Pointer", + @"className" : @"Yolo1", + @"localId" : @"456"} }]; + XCTAssertNotNil(decoded); + + PFObject *object = decoded[@"pointer1"]; + XCTAssertNotNil(object); + PFAssertIsKindOfClass(object, [PFObject class]); + + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"123"); + + PFObject *localObject = decoded[@"pointer2"]; + XCTAssertNotNil(localObject); + PFAssertIsKindOfClass(localObject, [PFObject class]); + + XCTAssertEqualObjects(localObject.parseClassName, @"Yolo1"); + XCTAssertNil(localObject.objectId); + XCTAssertEqualObjects([localObject getOrCreateLocalId], @"456"); +} + +- (void)testDecodingObjects { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"object" : @{@"__type" : @"Object", + @"className" : @"Yolo", + @"objectId" : @"123"} }]; + XCTAssertNotNil(decoded); + + PFObject *object = decoded[@"object"]; + XCTAssertNotNil(object); + PFAssertIsKindOfClass(object, [PFObject class]); + + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"123"); +} + +- (void)testDecodingUnknownType { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"yarr" : @{@"__type" : @"Yolo", + @"name" : @"Yolo!"} }]; + XCTAssertNotNil(decoded); + + NSDictionary *dictionary = decoded[@"yarr"]; + XCTAssertNotNil(dictionary); + PFAssertIsKindOfClass(dictionary, [NSDictionary class]); + + XCTAssertEqualObjects(dictionary[@"name"], @"Yolo!"); +} + +- (void)testDecodingArrays { + PFDecoder *decoder = [[PFDecoder alloc] init]; + + NSDictionary *decoded = [decoder decodeObject:@{ @"array" : @[ @1, @{@"a" : @"b"} ] }]; + XCTAssertNotNil(decoded); + + NSArray *array = decoded[@"array"]; + XCTAssertNotNil(array); + PFAssertIsKindOfClass(array, [NSArray class]); + + XCTAssertEqualObjects(array[0], @1); + XCTAssertEqualObjects(array[1], @{ @"a" : @"b" }); +} + +///-------------------------------------- +#pragma mark - OfflineDecoder Tests +///-------------------------------------- + +- (void)testOfflineDecoderConstructors { + PFOfflineDecoder *decoder = [PFOfflineDecoder decoderWithOfflineObjects:@{ @"yolo11" : [PFObject objectWithClassName:@"Yolo"] }]; + XCTAssertNotNil(decoder); +} + +- (void)testOfflineDecoderDecoding { + NSDictionary *offlineObjects = @{ @"yolo11" : [BFTask taskWithResult:[PFObject objectWithClassName:@"Yolo"]] }; + PFOfflineDecoder *decoder = [PFOfflineDecoder decoderWithOfflineObjects:offlineObjects]; + + NSArray *decoded = [decoder decodeObject:@[ @{ @"__type" : @"OfflineObject", + @"uuid" : @"yolo11" + }, + @{ @"__type" : @"Object", + @"className" : @"Yarr" + } ]]; + XCTAssertNotNil(decoded); + + PFObject *offlineObject = decoded[0]; + XCTAssertNotNil(offlineObject); + XCTAssertEqual(offlineObject, [offlineObjects[@"yolo11"] result]); + + PFObject *object = decoded[1]; + XCTAssertNotNil(object); + XCTAssertNotEqual(object, offlineObject); + XCTAssertEqualObjects(object.parseClassName, @"Yarr"); +} + +///-------------------------------------- +#pragma mark - KnownParseObjectDecoder Tests +///-------------------------------------- + +- (void)testKnownParseObjectDecoderConstructors { + PFKnownParseObjectDecoder *decoder = [PFKnownParseObjectDecoder decoderWithFetchedObjects:@{ @"a" : [PFObject objectWithClassName:@"Yolo"] }]; + XCTAssertNotNil(decoder); +} + +- (void)testKnownParseObjectDecoderDecoding { + NSDictionary *objects = @{ @"a" : [PFObject objectWithClassName:@"Yolo"] }; + PFKnownParseObjectDecoder *decoder = [PFKnownParseObjectDecoder decoderWithFetchedObjects:objects]; + + NSArray *decoded = [decoder decodeObject:@[ @{ @"__type" : @"Pointer", + @"className" : @"Yolo", + @"objectId" : @"a" }, + @{ @"__type" : @"Pointer", + @"className" : @"Yarr", + @"objectId" : @"b" } ]]; + XCTAssertNotNil(decoded); + + PFObject *knownObject = decoded[0]; + XCTAssertEqual(knownObject, objects[@"a"]); + + PFObject *object = decoded[1]; + XCTAssertNotEqual(knownObject, object); + XCTAssertEqualObjects(object.objectId, @"b"); +} + +@end diff --git a/Tests/Unit/DefaultACLControllerTests.m b/Tests/Unit/DefaultACLControllerTests.m new file mode 100644 index 000000000..e98dcafbe --- /dev/null +++ b/Tests/Unit/DefaultACLControllerTests.m @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFACLPrivate.h" +#import "PFCoreManager.h" +#import "PFCurrentUserController.h" +#import "PFDefaultACLController.h" +#import "PFMacros.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface DefaultACLControllerTests : PFUnitTestCase + +@end + +@implementation DefaultACLControllerTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)tearDown { + [PFDefaultACLController clearDefaultController]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + XCTAssertNotNil([[PFDefaultACLController alloc] init]); +} + +- (void)testSingleton { + PFDefaultACLController *oldController = [PFDefaultACLController defaultController]; + XCTAssertNotNil(oldController); + + [PFDefaultACLController clearDefaultController]; + PFDefaultACLController *newController = [PFDefaultACLController defaultController]; + XCTAssertNotNil(newController); + + XCTAssertNotEqual(oldController, newController); +} + +- (void)testSetDefaultACL { + id mockedACL = PFStrictClassMock([PFACL class]); + + OCMExpect([mockedACL createUnsharedCopy]).andReturnWeak(mockedACL); + OCMExpect([mockedACL setShared:YES]); + + PFDefaultACLController *aclController = [[PFDefaultACLController alloc] init]; + + XCTestExpectation *expecatation = [self currentSelectorTestExpectation]; + [[aclController setDefaultACLAsync:mockedACL withCurrentUserAccess:NO] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedACL); + + return [[aclController getDefaultACLAsync] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedACL); + [expecatation fulfill]; + return nil; + }]; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(mockedACL); +} + +- (void)testSetDefaultACLWithUserAccessWithoutCurrentUser { + id mockedACL = PFStrictClassMock([PFACL class]); + id mockedCurrentUserController = PFStrictClassMock([PFCurrentUserController class]); + + [Parse _currentManager].coreManager.currentUserController = mockedCurrentUserController; + + OCMExpect([mockedACL createUnsharedCopy]).andReturnWeak(mockedACL); + OCMExpect([mockedACL setShared:YES]); + + [OCMStub([mockedCurrentUserController getCurrentObjectAsync]) andReturn:[BFTask taskWithResult:nil]]; + + PFDefaultACLController *aclController = [[PFDefaultACLController alloc] init]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[[aclController setDefaultACLAsync:mockedACL withCurrentUserAccess:YES] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedACL); + + // Test case of nil current user, no modifications to the ACL should be made. + return [aclController getDefaultACLAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedACL); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(mockedACL); +} + +- (void)testSetDefaultACLWithUserAccessWithCurrentUser { + id mockedACL = PFStrictClassMock([PFACL class]); + id mockedCurrentUserController = PFStrictClassMock([PFCurrentUserController class]); + + [Parse _currentManager].coreManager.currentUserController = mockedCurrentUserController; + + OCMExpect([mockedACL createUnsharedCopy]).andReturnWeak(mockedACL); + OCMExpect([mockedACL setShared:YES]); + + PFUser *user = [PFUser user]; + [OCMStub([mockedCurrentUserController getCurrentObjectAsync]) andReturn:[BFTask taskWithResult:user]]; + + PFDefaultACLController *aclController = [[PFDefaultACLController alloc] init]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[[[aclController setDefaultACLAsync:mockedACL withCurrentUserAccess:YES] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedACL); + + OCMExpect([mockedACL createUnsharedCopy]).andReturnWeak(mockedACL); + OCMExpect([mockedACL setShared:YES]); + OCMExpect([mockedACL setReadAccess:YES forUser:user]); + OCMExpect([mockedACL setWriteAccess:YES forUser:user]); + + return [aclController getDefaultACLAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedACL); + + // Ensure that the ACL wasn't changed between fetches. + return [aclController getDefaultACLAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedACL); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(mockedACL); +} + +@end diff --git a/Tests/Unit/DeviceTests.m b/Tests/Unit/DeviceTests.m new file mode 100644 index 000000000..8ea8abd04 --- /dev/null +++ b/Tests/Unit/DeviceTests.m @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDevice.h" +#import "PFTestCase.h" + +@interface DeviceTests : PFTestCase + +@end + +@implementation DeviceTests + +- (void)testCurrentDevice { + PFDevice *device = [PFDevice currentDevice]; + XCTAssertNotNil(device); + XCTAssertEqual(device, [PFDevice currentDevice]); +} + +- (void)testDetailedModel { + PFDevice *device = [PFDevice currentDevice]; + XCTAssertNotNil(device.detailedModel); + XCTAssertNotEqual(device.detailedModel.length, 0); +} + +- (void)testOperationSystemFullVersion { + PFDevice *device = [PFDevice currentDevice]; + XCTAssertNotNil(device.operatingSystemFullVersion); + XCTAssertNotEqual(device.operatingSystemFullVersion.length, 0); + + XCTAssertNotEqualObjects(device.operatingSystemFullVersion, device.operatingSystemVersion); + XCTAssertNotEqualObjects(device.operatingSystemFullVersion, device.operatingSystemBuild); +} + +- (void)testOperatingSystemVersion { + PFDevice *device = [PFDevice currentDevice]; + XCTAssertNotNil(device.operatingSystemVersion); + XCTAssertNotEqual(device.operatingSystemVersion.length, 0); +} + +- (void)testOperationSystemBuild { + PFDevice *device = [PFDevice currentDevice]; + XCTAssertNotNil(device.operatingSystemBuild); + XCTAssertNotEqual(device.operatingSystemBuild.length, 0); +} + +- (void)testJailbroken { + PFDevice *device = [PFDevice currentDevice]; + XCTAssertNoThrow(device.jailbroken); // No chance we can test this. +} + +@end diff --git a/Tests/Unit/ExtensionDataSharingMobileTests.m b/Tests/Unit/ExtensionDataSharingMobileTests.m new file mode 100644 index 000000000..8d68055ef --- /dev/null +++ b/Tests/Unit/ExtensionDataSharingMobileTests.m @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFExtensionDataSharingTestHelper.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFTestCase.h" +#import "Parse_Private.h" + +//TODO: (nlutsenko,richardross) These tests are extremely flaky, we should update and re-enable them. + +@interface ExtensionDataSharingMobileTests : PFTestCase { + PFExtensionDataSharingTestHelper *_testHelper; +} + +@end + +@implementation ExtensionDataSharingMobileTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + _testHelper = [[PFExtensionDataSharingTestHelper alloc] init]; + + [[Parse _currentManager].offlineStore clearDatabase]; + [Parse _clearCurrentManager]; +} + +- (void)tearDown { + [[Parse _currentManager] clearEventuallyQueue]; + [[Parse _currentManager].offlineStore clearDatabase]; + [Parse _currentManager].offlineStore = nil; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + [fileManager removeItemAtPath:[PFExtensionDataSharingTestHelper sharedTestDirectoryPath] error:nil]; + [fileManager removeItemAtPath:[[Parse _currentManager].fileManager parseDefaultDataDirectoryPath] error:nil]; + [fileManager removeItemAtPath:[[Parse _currentManager].fileManager parseLocalSandboxDataDirectoryPath] error:nil]; + + [Parse _clearCurrentManager]; + [Parse _resetDataSharingIdentifiers]; + + _testHelper = nil; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testEnablingDataSharingWithoutAppGroupContainer { + _testHelper.swizzledGroupContainerDirectoryPath = NO; + + XCTAssertThrows([Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo"]); + + _testHelper.runningInExtensionEnvironment = YES; + + XCTAssertThrows([Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo" + containingApplication:@"parentYolo"]); +} + +@end diff --git a/Tests/Unit/ExtensionDataSharingTests.m b/Tests/Unit/ExtensionDataSharingTests.m new file mode 100644 index 000000000..128b15fe2 --- /dev/null +++ b/Tests/Unit/ExtensionDataSharingTests.m @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFExtensionDataSharingTestHelper.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFMultiProcessFileLock.h" +#import "PFTestCase.h" +#import "Parse_Private.h" + +//TODO: (nlutsenko,richardross) These tests are extremely flaky, we should update and re-enable them. + +@interface ExtensionDataSharingTests : PFTestCase { + PFExtensionDataSharingTestHelper *_testHelper; +} + +@end + +@implementation ExtensionDataSharingTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + _testHelper = [[PFExtensionDataSharingTestHelper alloc] init]; +} + +- (void)tearDown { + [[Parse _currentManager] clearEventuallyQueue]; + + NSString *path = [[Parse _currentManager].fileManager parseLocalSandboxDataDirectoryPath]; + + [Parse _clearCurrentManager]; + [Parse _resetDataSharingIdentifiers]; + + // This allows us to delete files while respecting file locks. + NSArray *removalTasks = @[ +#if TARGET_OS_IPHONE + // Doing this on OSX is BAD, as this returns ~/Library/Application Support. Trust me, you don't want to delete this. + [PFFileManager removeItemAtPathAsync:[PFExtensionDataSharingTestHelper sharedTestDirectoryPath]], +#endif + [PFFileManager removeItemAtPathAsync:path] + ]; + [[BFTask taskForCompletionOfAllTasks:removalTasks] waitUntilFinished]; + + + _testHelper = nil; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testEnablingDataSharingFromMainApp { + _testHelper.swizzledGroupContainerDirectoryPath = YES; + _testHelper.runningInExtensionEnvironment = NO; + + [Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo"]; + [Parse _resetDataSharingIdentifiers]; + + _testHelper.runningInExtensionEnvironment = YES; + XCTAssertThrows([Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo"]); + + // Just to make sure that initialization runs smoothly + [Parse setApplicationId:[[NSUUID UUID] UUIDString] clientKey:[[NSUUID UUID] UUIDString]]; +} + +- (void)testEnablingDataSharingFromExtensions { + _testHelper.swizzledGroupContainerDirectoryPath = YES; + _testHelper.runningInExtensionEnvironment = YES; + + [Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo" + containingApplication:@"parentYolo"]; + [Parse _resetDataSharingIdentifiers]; + + _testHelper.runningInExtensionEnvironment = NO; + XCTAssertThrows([Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo" + containingApplication:@"parentYolo"]); + + // Just to make sure that initialization runs smoothly + [Parse setApplicationId:[[NSUUID UUID] UUIDString] clientKey:[[NSUUID UUID] UUIDString]]; +} + +- (void)testMainAppUsesSharedContainer { + _testHelper.swizzledGroupContainerDirectoryPath = YES; + _testHelper.runningInExtensionEnvironment = NO; + + [Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo"]; + [Parse setApplicationId:[[NSUUID UUID] UUIDString] clientKey:@"b"]; + + // Paths are different on iOS and OSX. + NSString *containerPath = [PFExtensionDataSharingTestHelper sharedTestDirectoryPathForGroupIdentifier:@"yolo"]; + [self assertDirectory:containerPath hasContents:@{ @"Parse" : @{ [Parse getApplicationId] : @{ @"applicationId" : [NSNull null] } } } only:NO]; +} + +- (void)testExtensionUsesSharedContainer { + _testHelper.swizzledGroupContainerDirectoryPath = YES; + _testHelper.runningInExtensionEnvironment = YES; + + [Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo" + containingApplication:@"parentYolo"]; + [Parse setApplicationId:[[NSUUID UUID] UUIDString] clientKey:@"b"]; + + // Paths are different on iOS and OSX. + NSString *containerPath = [PFExtensionDataSharingTestHelper sharedTestDirectoryPathForGroupIdentifier:@"yolo"]; + [self assertDirectory:containerPath hasContents:@{ @"Parse" : @{ [Parse getApplicationId] : @{ @"applicationId" : [NSNull null] } } } only:NO]; +} + +- (void)testMigratingDataFromMainSandbox { + NSString *containerPath = [PFExtensionDataSharingTestHelper sharedTestDirectoryPathForGroupIdentifier:@"yolo"]; + + NSString *applicationId = [[NSUUID UUID] UUIDString]; + + [Parse enableLocalDatastore]; + [Parse setApplicationId:applicationId clientKey:@"b"]; + + PFObject *object = [PFObject objectWithClassName:@"TestObject"]; + object[@"yolo"] = @"yarr"; + XCTAssertTrue([object pin]); + + // We are using the same directory on OSX, so this check is irrelevant +#if TARGET_OS_IPHONE + [self assertDirectoryDoesntExist:[containerPath stringByAppendingPathComponent:@"Parse"]]; +#endif + + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [Parse _resetDataSharingIdentifiers]; + + _testHelper.swizzledGroupContainerDirectoryPath = YES; + _testHelper.runningInExtensionEnvironment = NO; + + [Parse enableLocalDatastore]; + [Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo"]; + [Parse setApplicationId:applicationId clientKey:@"b"]; + + PFQuery *query = [[PFQuery queryWithClassName:@"TestObject"] fromLocalDatastore]; + + NSError *error = nil; + NSInteger count = [query countObjects:&error]; + XCTAssertNil(error, @"%@", error); + XCTAssertEqual(1, count); + + [self assertDirectory:containerPath hasContents:@{ @"Parse" : + @{ applicationId : + @{ @"applicationId" : [NSNull null], + @"ParseOfflineStore" : [NSNull null] + } + } + } only:NO]; +} + +- (void)testMigratingDataFromExtensionsSandbox { + NSString *containerPath = [PFExtensionDataSharingTestHelper sharedTestDirectoryPathForGroupIdentifier:@"yolo"]; + + NSString *applicationId = [[NSUUID UUID] UUIDString]; + + [Parse enableLocalDatastore]; + [Parse setApplicationId:applicationId clientKey:@"b"]; + + PFObject *object = [PFObject objectWithClassName:@"TestObject"]; + object[@"yolo"] = @"yarr"; + XCTAssertTrue([object pin]); + + // We are using the same directory on OSX, so this check is irrelevant +#if TARGET_OS_IPHONE + [self assertDirectoryDoesntExist:[containerPath stringByAppendingPathComponent:@"Parse"]]; +#endif + + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [Parse _resetDataSharingIdentifiers]; + + _testHelper.swizzledGroupContainerDirectoryPath = YES; + _testHelper.runningInExtensionEnvironment = YES; + + [Parse enableLocalDatastore]; + [Parse enableDataSharingWithApplicationGroupIdentifier:@"yolo" + containingApplication:@"parentYolo"]; + [Parse setApplicationId:applicationId clientKey:@"b"]; + + PFQuery *query = [[PFQuery queryWithClassName:@"TestObject"] fromLocalDatastore]; + + // We are using the same directory on OSX, but different folders on iOS. + NSError *error = nil; + NSInteger count = [query countObjects:&error]; + + XCTAssertNil(error, @"%@", error); + +#if TARGET_OS_IPHONE + XCTAssertEqual(0, count); +#else + XCTAssertEqual(1, count); +#endif + + [self assertDirectory:containerPath hasContents:@{ @"Parse" : + @{ applicationId : + @{ @"applicationId" : [NSNull null], + @"ParseOfflineStore" : [NSNull null] + } + } + } only:NO]; +} + +@end diff --git a/Tests/Unit/FieldOperationDecoderTests.m b/Tests/Unit/FieldOperationDecoderTests.m new file mode 100644 index 000000000..0be7ca927 --- /dev/null +++ b/Tests/Unit/FieldOperationDecoderTests.m @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDecoder.h" +#import "PFFieldOperation.h" +#import "PFFieldOperationDecoder.h" +#import "PFObject.h" +#import "PFTestCase.h" + +@interface FieldOperationDecoderTests : PFTestCase + +@end + +@implementation FieldOperationDecoderTests + +- (void)testConstructors { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + XCTAssertNotNil(decoder); +} + +- (void)testDefaultDecoder { + XCTAssertNotNil([PFFieldOperationDecoder defaultDecoder]); + XCTAssertEqual([PFFieldOperationDecoder defaultDecoder], [PFFieldOperationDecoder defaultDecoder]); +} + +- (void)testDecodingUnknownOperation { + XCTAssertThrows([[PFFieldOperationDecoder defaultDecoder] decode:@{ @"__op" : @"Yarr" } + withDecoder:[PFDecoder objectDecoder]]); +} + +- (void)testDecodingIncrementOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"Increment", + @"amount" : @100500 }; + PFIncrementOperation *operation = (PFIncrementOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFIncrementOperation class]); + XCTAssertEqualObjects(operation.amount, @100500); +} + +- (void)testDecodingAddOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"Add", + @"objects" : @[ [PFObject objectWithClassName:@"Yolo"] ] }; + PFAddOperation *operation = (PFAddOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFAddOperation class]); + XCTAssertEqualObjects([[operation.objects firstObject] parseClassName], @"Yolo"); +} + +- (void)testDecodingAddUniqueOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"AddUnique", + @"objects" : @[ [PFObject objectWithClassName:@"Yolo"] ] }; + PFAddUniqueOperation *operation = (PFAddUniqueOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFAddUniqueOperation class]); + XCTAssertEqualObjects([[operation.objects firstObject] parseClassName], @"Yolo"); +} + +- (void)testDecodingRemoveOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"Remove", + @"objects" : @[ [PFObject objectWithClassName:@"Yolo"] ] }; + PFRemoveOperation *operation = (PFRemoveOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFRemoveOperation class]); + XCTAssertEqualObjects([[operation.objects firstObject] parseClassName], @"Yolo"); +} + +- (void)testDecodingDeleteOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"Delete" }; + PFDeleteOperation *operation = (PFDeleteOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFDeleteOperation class]); +} + +- (void)testDecodingBatchOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"Batch", + @"ops" : @[ @{@"__op" : @"AddRelation", + @"objects" : @[ [PFObject objectWithClassName:@"Yolo"] ]}, + @{@"__op" : @"RemoveRelation", + @"objects" : @[ [PFObject objectWithClassName:@"Yolo"] ]} ] + }; + PFRelationOperation *operation = (PFRelationOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFRelationOperation class]); + XCTAssertEqualObjects(operation.targetClass, @"Yolo"); + XCTAssertEqualObjects([[operation.relationsToAdd anyObject] parseClassName], @"Yolo"); + XCTAssertEqual(operation.relationsToAdd.count, 1); + XCTAssertEqualObjects([[operation.relationsToRemove anyObject] parseClassName], @"Yolo"); + XCTAssertEqual(operation.relationsToRemove.count, 1); +} + +- (void)testDecodingDecodedBatchOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"Batch", + @"ops" : @[ [PFRelationOperation addRelationToObjects:@[ [PFObject objectWithClassName:@"Yolo"] ]], + [PFRelationOperation removeRelationToObjects:@[ [PFObject objectWithClassName:@"Yolo"] ]] ] + }; + PFRelationOperation *operation = (PFRelationOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFRelationOperation class]); + XCTAssertEqualObjects(operation.targetClass, @"Yolo"); + XCTAssertEqualObjects([[operation.relationsToAdd anyObject] parseClassName], @"Yolo"); + XCTAssertEqual(operation.relationsToAdd.count, 1); + XCTAssertEqualObjects([[operation.relationsToRemove anyObject] parseClassName], @"Yolo"); + XCTAssertEqual(operation.relationsToRemove.count, 1); +} + +- (void)testDecodingAddRelationOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"AddRelation", + @"objects" : @[ [PFObject objectWithClassName:@"Yolo"] ] }; + PFRelationOperation *operation = (PFRelationOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFRelationOperation class]); + XCTAssertEqualObjects(operation.targetClass, @"Yolo"); + XCTAssertEqual(operation.relationsToAdd.count, 1); + XCTAssertEqualObjects([[operation.relationsToAdd anyObject] parseClassName], @"Yolo"); +} + +- (void)testDecodingRemoveRelationOperations { + PFFieldOperationDecoder *decoder = [[PFFieldOperationDecoder alloc] init]; + + NSDictionary *dictionary = @{ @"__op" : @"RemoveRelation", + @"objects" : @[ [PFObject objectWithClassName:@"Yolo"] ] }; + PFRelationOperation *operation = (PFRelationOperation *)[decoder decode:dictionary + withDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(operation); + PFAssertIsKindOfClass(operation, [PFRelationOperation class]); + XCTAssertEqualObjects(operation.targetClass, @"Yolo"); + XCTAssertEqual(operation.relationsToRemove.count, 1); + XCTAssertEqualObjects([[operation.relationsToRemove anyObject] parseClassName], @"Yolo"); +} + +@end diff --git a/Tests/Unit/FieldOperationTests.m b/Tests/Unit/FieldOperationTests.m new file mode 100644 index 000000000..0ce0defd0 --- /dev/null +++ b/Tests/Unit/FieldOperationTests.m @@ -0,0 +1,401 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFFieldOperation.h" +#import "PFObject.h" +#import "PFTestCase.h" + +@interface FieldOperationTests : PFTestCase + +@end + +@implementation FieldOperationTests + +///-------------------------------------- +#pragma mark - FieldOperation +///-------------------------------------- + +- (void)testFieldOperationConstructors { + PFFieldOperation *operation = [[PFFieldOperation alloc] init]; + XCTAssertNotNil(operation); +} + +- (void)testFieldOperationEncoding { + PFFieldOperation *operation = [[PFFieldOperation alloc] init]; + XCTAssertThrows([operation encodeWithObjectEncoder:[PFEncoder objectEncoder]]); +} + +- (void)testFieldOperationMerge { + PFFieldOperation *operation = [[PFFieldOperation alloc] init]; + XCTAssertThrows([operation mergeWithPrevious:nil]); + XCTAssertThrows([operation mergeWithPrevious:[PFSetOperation setWithValue:@1]]); +} + +- (void)testFieldOperationApply { + PFFieldOperation *operation = [[PFFieldOperation alloc] init]; + XCTAssertThrows([operation applyToValue:@1 forKey:@"a"]); +} + +///-------------------------------------- +#pragma mark - SetOperation +///-------------------------------------- + +- (void)testSetOperationConstructors { + PFSetOperation *operation = [[PFSetOperation alloc] initWithValue:@"yarr"]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.value, @"yarr"); + + operation = [PFSetOperation setWithValue:@"yarr"]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.value, @"yarr"); +} + +- (void)testSetOperationDescription { + PFSetOperation *operation = [[PFSetOperation alloc] initWithValue:@"yarr"]; + XCTAssertTrue([[operation description] rangeOfString:@"yarr"].location != NSNotFound); +} + +- (void)testSetOperationConstructorsValidation { + PFAssertThrowsInvalidArgumentException([[PFSetOperation alloc] initWithValue:nil]); + PFAssertThrowsInvalidArgumentException([PFSetOperation setWithValue:nil]); +} + +- (void)testSetOperationMerge { + PFSetOperation *operation = [PFSetOperation setWithValue:@"yarr"]; + XCTAssertEqual(operation, [operation mergeWithPrevious:nil]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[[PFDeleteOperation alloc] init]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFIncrementOperation incrementWithAmount:@1]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFAddOperation addWithObjects:@[ @"yolo" ]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFAddUniqueOperation addUniqueWithObjects:@[ @"yolo" ]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFRemoveOperation removeWithObjects:@[ @"yolo" ]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFRelationOperation addRelationToObjects:@[]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFRelationOperation removeRelationToObjects:@[]]]); +} + +- (void)testSetOperationEncoding { + PFEncoder *encoder = PFStrictClassMock([PFEncoder class]); + OCMStub([encoder encodeObject:[OCMArg isEqual:@"yarr"]]).andReturn(@"yolo"); + + PFSetOperation *operation = [PFSetOperation setWithValue:@"yarr"]; + XCTAssertEqualObjects([operation encodeWithObjectEncoder:encoder], @"yolo"); +} + +///-------------------------------------- +#pragma mark - DeleteOperation +///-------------------------------------- + +- (void)testDeleteOperationConstructors { + PFDeleteOperation *operation = [[PFDeleteOperation alloc] init]; + XCTAssertNotNil(operation); + + operation = [PFDeleteOperation operation]; + XCTAssertNotNil(operation); +} + +- (void)testDeleteOperationDescription { + PFDeleteOperation *operation = [PFDeleteOperation operation]; + XCTAssertTrue([[operation description] rangeOfString:@"delete"].location != NSNotFound); +} + +- (void)testDeleteOperationEncoding { + PFDeleteOperation *operation = [PFDeleteOperation operation]; + + NSDictionary *encoded = [operation encodeWithObjectEncoder:nil]; + XCTAssertEqualObjects(encoded, @{ @"__op" : @"Delete" }); + + encoded = [operation encodeWithObjectEncoder:[PFEncoder objectEncoder]]; + XCTAssertEqualObjects(encoded, @{ @"__op" : @"Delete" }); +} + +- (void)testDeleteOperationMerge { + PFDeleteOperation *operation = [PFDeleteOperation operation]; + XCTAssertEqual(operation, [operation mergeWithPrevious:nil]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFSetOperation setWithValue:@1]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[[PFDeleteOperation alloc] init]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFIncrementOperation incrementWithAmount:@1]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFAddOperation addWithObjects:@[ @"yolo" ]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFAddUniqueOperation addUniqueWithObjects:@[ @"yolo" ]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFRemoveOperation removeWithObjects:@[ @"yolo" ]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFRelationOperation addRelationToObjects:@[]]]); + XCTAssertEqual(operation, [operation mergeWithPrevious:[PFRelationOperation removeRelationToObjects:@[]]]); +} + +///-------------------------------------- +#pragma mark - IncrementOperation +///-------------------------------------- + +- (void)testIncrementOperationConstructors { + PFIncrementOperation *operation = [[PFIncrementOperation alloc] initWithAmount:@100500]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.amount, @100500); + + operation = [PFIncrementOperation incrementWithAmount:@100500]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.amount, @100500); +} + +- (void)testIncrementOperationDescription { + PFIncrementOperation *operation = [PFIncrementOperation incrementWithAmount:@100500]; + XCTAssertTrue([[operation description] rangeOfString:@"100500"].location != NSNotFound); +} + +- (void)testIncrementOperationEncoding { + PFIncrementOperation *operation = [PFIncrementOperation incrementWithAmount:@100500]; + + NSDictionary *properEncodedDictionary = @{ @"__op" : @"Increment", + @"amount" : @100500 }; + + NSDictionary *encoded = [operation encodeWithObjectEncoder:nil]; + XCTAssertEqualObjects(encoded, properEncodedDictionary); + + encoded = [operation encodeWithObjectEncoder:[PFEncoder objectEncoder]]; + XCTAssertEqualObjects(encoded, properEncodedDictionary); +} + +- (void)testIncrementOperationMerge { + PFIncrementOperation *operation = [PFIncrementOperation incrementWithAmount:@50]; + XCTAssertEqual(operation, [operation mergeWithPrevious:nil]); + + PFSetOperation *set = (PFSetOperation *)[operation mergeWithPrevious:[[PFDeleteOperation alloc] init]]; + XCTAssertEqualObjects(set.value, @50); + set = (PFSetOperation *)[operation mergeWithPrevious:[PFSetOperation setWithValue:@1]]; + XCTAssertEqualObjects(set.value, @51); + + PFAssertThrowsInvalidArgumentException([operation mergeWithPrevious:[PFSetOperation setWithValue:@"1"]]); + + //TODO: (nlutsenko) Convert to XCTAssertEqualObjects when PFFieldOperation supports proper `isEqual:` + PFIncrementOperation *increment = (PFIncrementOperation *)[operation mergeWithPrevious:[PFIncrementOperation incrementWithAmount:@1]]; + XCTAssertEqualObjects(increment.amount, @51); + + PFAssertThrowsInconsistencyException([operation mergeWithPrevious:[PFAddOperation addWithObjects:@[ @"yolo" ]]]); + PFAssertThrowsInconsistencyException([operation mergeWithPrevious:[PFAddUniqueOperation addUniqueWithObjects:@[ @"yolo" ]]]); + PFAssertThrowsInconsistencyException([operation mergeWithPrevious:[PFRemoveOperation removeWithObjects:@[ @"yolo" ]]]); + + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation addRelationToObjects:@[]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation removeRelationToObjects:@[]]]); +} + +///-------------------------------------- +#pragma mark - AddOperation +///-------------------------------------- + +- (void)testAddOperationConstructors { + PFAddOperation *operation = [PFAddOperation addWithObjects:@[ @"yarr" ]]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.objects, @[ @"yarr" ]); +} + +- (void)testAddOperationDescription { + PFAddOperation *operation = [PFAddOperation addWithObjects:@[ @"yarr" ]]; + XCTAssertTrue([[operation description] rangeOfString:@"yarr"].location != NSNotFound); +} + +- (void)testAddOperationEncoding { + PFEncoder *encoder = PFStrictClassMock([PFEncoder class]); + OCMStub([encoder encodeObject:[OCMArg isEqual:@[ @"yarr" ]]]).andReturn(@"yolo"); + + PFAddOperation *operation = [PFAddOperation addWithObjects:@[ @"yarr" ]]; + XCTAssertThrows([operation encodeWithObjectEncoder:nil]); + XCTAssertEqualObjects([operation encodeWithObjectEncoder:encoder], (@{ @"__op" : @"Add", + @"objects" : @"yolo" })); +} + +- (void)testAddOperationMerge { + PFAddOperation *operation = [PFAddOperation addWithObjects:@[ @"yarr" ]]; + + XCTAssertEqual(operation, [operation mergeWithPrevious:nil]); + + XCTAssertThrows([operation mergeWithPrevious:[PFSetOperation setWithValue:@1]]); + PFSetOperation *setResult = (PFSetOperation *)[operation mergeWithPrevious:[PFSetOperation setWithValue:@[]]]; + XCTAssertEqualObjects(setResult.value, @[ @"yarr" ]); + + PFSetOperation *deleteResult = (PFSetOperation *)[operation mergeWithPrevious:[[PFDeleteOperation alloc] init]]; + XCTAssertEqualObjects(deleteResult.value, @[ @"yarr" ]); + + XCTAssertThrows([operation mergeWithPrevious:[PFIncrementOperation incrementWithAmount:@1]]); + + PFAddOperation *addResult = (PFAddOperation *)[operation mergeWithPrevious:[PFAddOperation addWithObjects:@[ @"yolo" ]]]; + XCTAssertEqualObjects(addResult.objects, (@[ @"yolo", @"yarr" ])); + + XCTAssertThrows([operation mergeWithPrevious:[PFAddUniqueOperation addUniqueWithObjects:@[ @"yolo" ]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFRemoveOperation removeWithObjects:@[ @"yolo" ]]]); + + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation addRelationToObjects:@[]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation removeRelationToObjects:@[]]]); +} + +///-------------------------------------- +#pragma mark - AddUniqueOperation +///-------------------------------------- + +- (void)testAddUniqueOperationConstructors { + PFAddUniqueOperation *operation = [PFAddUniqueOperation addUniqueWithObjects:@[ @"yarr" ]]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.objects, @[ @"yarr" ]); +} + +- (void)testAddUniqueOperationDescription { + PFAddUniqueOperation *operation = [PFAddUniqueOperation addUniqueWithObjects:@[ @"yarr" ]]; + XCTAssertTrue([[operation description] rangeOfString:@"yarr"].location != NSNotFound); +} + +- (void)testAddUniqueOperationEncoding { + PFEncoder *encoder = PFStrictClassMock([PFEncoder class]); + OCMStub([encoder encodeObject:[OCMArg isEqual:@[ @"yarr" ]]]).andReturn(@"yolo"); + + PFAddUniqueOperation *operation = [PFAddUniqueOperation addUniqueWithObjects:@[ @"yarr" ]]; + XCTAssertThrows([operation encodeWithObjectEncoder:nil]); + XCTAssertEqualObjects([operation encodeWithObjectEncoder:encoder], (@{ @"__op" : @"AddUnique", + @"objects" : @"yolo" })); +} + +- (void)testAddUniqueOperationMerge { + PFAddUniqueOperation *operation = [PFAddUniqueOperation addUniqueWithObjects:@[ @"yarr" ]]; + + XCTAssertEqual(operation, [operation mergeWithPrevious:nil]); + + XCTAssertThrows([operation mergeWithPrevious:[PFSetOperation setWithValue:@1]]); + PFSetOperation *setResult = (PFSetOperation *)[operation mergeWithPrevious:[PFSetOperation setWithValue:@[]]]; + XCTAssertEqualObjects(setResult.value, @[ @"yarr" ]); + + PFSetOperation *deleteResult = (PFSetOperation *)[operation mergeWithPrevious:[[PFDeleteOperation alloc] init]]; + XCTAssertEqualObjects(deleteResult.value, @[ @"yarr" ]); + + XCTAssertThrows([operation mergeWithPrevious:[PFIncrementOperation incrementWithAmount:@1]]); + + PFAddUniqueOperation *addResult = (PFAddUniqueOperation *)[operation mergeWithPrevious:[PFAddUniqueOperation addUniqueWithObjects:@[ @"yolo" ]]]; + XCTAssertEqualObjects(addResult.objects, (@[ @"yarr", @"yolo" ])); + + XCTAssertThrows([operation mergeWithPrevious:[PFAddOperation addWithObjects:@[ @"yolo" ]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFRemoveOperation removeWithObjects:@[ @"yolo" ]]]); + + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation addRelationToObjects:@[]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation removeRelationToObjects:@[]]]); +} + +///-------------------------------------- +#pragma mark - RemoveOperation +///-------------------------------------- + +- (void)testRemoveOperationConstructors { + PFRemoveOperation *operation = [PFRemoveOperation removeWithObjects:@[ @"yarr" ]]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.objects, @[ @"yarr" ]); +} + +- (void)testRemoveOperationDescription { + PFRemoveOperation *operation = [PFRemoveOperation removeWithObjects:@[ @"yarr" ]]; + XCTAssertTrue([[operation description] rangeOfString:@"yarr"].location != NSNotFound); +} + +- (void)testRemoveOperationEncoding { + PFEncoder *encoder = PFStrictClassMock([PFEncoder class]); + OCMStub([encoder encodeObject:[OCMArg isEqual:@[ @"yarr" ]]]).andReturn(@"yolo"); + + PFRemoveOperation *operation = [PFRemoveOperation removeWithObjects:@[ @"yarr" ]]; + XCTAssertThrows([operation encodeWithObjectEncoder:nil]); + XCTAssertEqualObjects([operation encodeWithObjectEncoder:encoder], (@{ @"__op" : @"Remove", + @"objects" : @"yolo" })); +} + +- (void)testRemoveOperationMerge { + PFRemoveOperation *operation = [PFRemoveOperation removeWithObjects:@[ @"yarr" ]]; + + XCTAssertEqual(operation, [operation mergeWithPrevious:nil]); + + XCTAssertThrows([operation mergeWithPrevious:[PFSetOperation setWithValue:@1]]); + PFSetOperation *setResult = (PFSetOperation *)[operation mergeWithPrevious:[PFSetOperation setWithValue:@[ @"yarr" ]]]; + XCTAssertEqualObjects(setResult.value, @[]); + + XCTAssertThrows([operation mergeWithPrevious:[[PFDeleteOperation alloc] init]]); + XCTAssertThrows([operation mergeWithPrevious:[PFIncrementOperation incrementWithAmount:@1]]); + XCTAssertThrows([operation mergeWithPrevious:[PFAddOperation addWithObjects:@[ @"yolo" ]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFAddUniqueOperation addUniqueWithObjects:@[ @"yolo" ]]]); + + PFRemoveOperation *removeResult = (PFRemoveOperation *)[operation mergeWithPrevious:[PFRemoveOperation removeWithObjects:@[ @"yolo" ]]]; + XCTAssertEqualObjects(removeResult.objects, (@[ @"yolo", @"yarr" ])); + + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation addRelationToObjects:@[]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFRelationOperation removeRelationToObjects:@[]]]); +} + +///-------------------------------------- +#pragma mark - RelationOperation +///-------------------------------------- + +- (void)testRelationOperationConstructors { + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + + PFRelationOperation *operation = [PFRelationOperation addRelationToObjects:@[ object ]]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.targetClass, @"Yolo"); + XCTAssertEqualObjects(operation.relationsToAdd, [NSSet setWithObject:object]); + XCTAssertEqual(operation.relationsToRemove.count, 0); + + operation = [PFRelationOperation removeRelationToObjects:@[ object ]]; + XCTAssertNotNil(operation); + XCTAssertEqualObjects(operation.targetClass, @"Yolo"); + XCTAssertEqual(operation.relationsToAdd.count, 0); + XCTAssertEqualObjects(operation.relationsToRemove, [NSSet setWithObject:object]); + + PFObject *badObject = [PFObject objectWithClassName:@"Yarr"]; + PFAssertThrowsInvalidArgumentException([PFRelationOperation addRelationToObjects:(@[ object, badObject ])]); + PFAssertThrowsInvalidArgumentException([PFRelationOperation removeRelationToObjects:(@[ object, badObject ])]); +} + +- (void)testRelationOperationDescription { + PFRelationOperation *operation = [PFRelationOperation addRelationToObjects:@[ [PFObject objectWithClassName:@"Yolo"] ]]; + XCTAssertTrue([[operation description] rangeOfString:@"Yolo"].location != NSNotFound); + + operation = [PFRelationOperation removeRelationToObjects:@[ [PFObject objectWithClassName:@"Yolo"] ]]; + XCTAssertTrue([[operation description] rangeOfString:@"Yolo"].location != NSNotFound); +} + +- (void)testRelationOperationEncoding { + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + PFEncoder *encoder = PFStrictClassMock([PFEncoder class]); + OCMStub([encoder encodeObject:OCMOCK_ANY]).andReturn(@"yolo"); + + PFRelationOperation *operation = [PFRelationOperation addRelationToObjects:@[ object, object ]]; + NSDictionary *encoded = [operation encodeWithObjectEncoder:encoder]; + XCTAssertEqualObjects(encoded, (@{ @"__op" : @"AddRelation", + @"objects" : @[ @"yolo" ] })); + + operation = [PFRelationOperation removeRelationToObjects:@[ object, object ]]; + encoded = [operation encodeWithObjectEncoder:encoder]; + XCTAssertEqualObjects(encoded, (@{ @"__op" : @"RemoveRelation", + @"objects" : @[ @"yolo" ] })); + + PFObject *anotherObject = [PFObject objectWithClassName:@"Yolo"]; + + operation = (PFRelationOperation *)[operation mergeWithPrevious:[PFRelationOperation addRelationToObjects:@[ anotherObject ]]]; + encoded = [operation encodeWithObjectEncoder:encoder]; + XCTAssertEqualObjects(encoded, (@{ @"__op" : @"Batch", + @"ops" : @[ @{@"__op" : @"AddRelation", @"objects" : @[ @"yolo" ]}, + @{@"__op" : @"RemoveRelation", @"objects" : @[ @"yolo" ]} ] })); + + XCTAssertThrows([[PFRelationOperation addRelationToObjects:@[]] encodeWithObjectEncoder:encoder]); + XCTAssertThrows([[PFRelationOperation removeRelationToObjects:@[]] encodeWithObjectEncoder:encoder]); +} + +- (void)testRelationOperationMerge { + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + PFRelationOperation *operation = [PFRelationOperation addRelationToObjects:@[ object ]]; + + XCTAssertEqual(operation, [operation mergeWithPrevious:nil]); + XCTAssertThrows([operation mergeWithPrevious:[[PFDeleteOperation alloc] init]]); + XCTAssertThrows([operation mergeWithPrevious:[PFIncrementOperation incrementWithAmount:@1]]); + XCTAssertThrows([operation mergeWithPrevious:[PFAddOperation addWithObjects:@[ @"yolo" ]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFAddUniqueOperation addUniqueWithObjects:@[ @"yolo" ]]]); + XCTAssertThrows([operation mergeWithPrevious:[PFRemoveOperation removeWithObjects:@[ @"yolo" ]]]); +} + +@end diff --git a/Tests/Unit/FileCommandTests.m b/Tests/Unit/FileCommandTests.m new file mode 100644 index 000000000..81fefcd53 --- /dev/null +++ b/Tests/Unit/FileCommandTests.m @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFRESTFileCommand.h" +#import "PFTestCase.h" + +@interface FileCommandTests : PFTestCase + +@end + +@implementation FileCommandTests + +- (void)testUploadFileCommand { + PFRESTFileCommand *command = [PFRESTFileCommand uploadCommandForFileWithName:@"a" sessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"files/a"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +@end diff --git a/Tests/Unit/FileControllerTests.m b/Tests/Unit/FileControllerTests.m new file mode 100644 index 000000000..c85c88fb1 --- /dev/null +++ b/Tests/Unit/FileControllerTests.m @@ -0,0 +1,522 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import +#import + +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFFileController.h" +#import "PFFileManager.h" +#import "PFMutableFileState.h" +#import "PFUnitTestCase.h" + +@protocol FileControllerDataSource + +@end + +@interface FileControllerTests : PFUnitTestCase + +@end + +@implementation FileControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (NSData *)sampleData { + const char bytes[] = { + 0x0, 0x1, 0x2, 0x3, + 0x4, 0x5, 0x6, 0x7, + 0x8, 0x9, 0xA, 0xB, + 0xC, 0xD, 0xE, 0xF}; + + return [NSData dataWithBytes:bytes length:sizeof(bytes)]; +} + +- (NSString *)temporaryDirectory { + return [NSTemporaryDirectory() stringByAppendingPathComponent:NSStringFromClass([self class])]; +} + +- (id)mockedDataSource { + id mockedDataSource = PFStrictProtocolMock(@protocol(FileControllerDataSource)); + id mockedCommandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + OCMStub([mockedDataSource commandRunner]).andReturn(mockedCommandRunner); + + id mockedFileManager = PFStrictClassMock([PFFileManager class]); + OCMStub([mockedDataSource fileManager]).andReturn(mockedFileManager); + return mockedDataSource; +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [[NSFileManager defaultManager] createDirectoryAtPath:[self temporaryDirectory] + withIntermediateDirectories:YES + attributes:nil + error:NULL]; +} + +- (void)tearDown { + [[NSFileManager defaultManager] removeItemAtPath:[self temporaryDirectory] error:NULL]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id mockedDataSource = [self mockedDataSource]; + + PFFileController *fileController = [[PFFileController alloc] initWithDataSource:mockedDataSource]; + XCTAssertEqual((id)fileController.dataSource, mockedDataSource); + + fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + XCTAssertEqual((id)fileController.dataSource, mockedDataSource); +} + +- (void)testDownload { + id mockedDataSource = [self mockedDataSource]; + + NSString *temporaryPath = [self temporaryDirectory]; + NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; + NSURL *tempPath = [NSURL fileURLWithPath:[temporaryPath stringByAppendingPathComponent:@"sampleData.dat"]]; + NSData *sampleData = [self sampleData]; + [sampleData writeToURL:tempPath atomically:YES]; + + id mockedFileManager = [mockedDataSource fileManager]; + OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); + + id mockedCommandRunner = [mockedDataSource commandRunner]; + OCMStub([mockedCommandRunner runFileDownloadCommandAsyncWithFileURL:tempPath + targetFilePath:[OCMArg isNotNil] + cancellationToken:nil + progressBlock:[OCMArg checkWithBlock:^BOOL(id obj) { + PFProgressBlock block = obj; + if (block) { + block(100); + } + return block != nil; + }]]).andReturn([BFTask taskWithResult:nil]); + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + + PFFileState *fileState = [[PFMutableFileState alloc] initWithName:@"sampleData" + urlString:[tempPath absoluteString] + mimeType:@"application/octet-stream"]; + + __block int progress = -1; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[fileController downloadFileAsyncWithState:fileState + cancellationToken:nil + progressBlock:^(int percentDone) { + XCTAssertTrue(progress <= percentDone); + + progress = percentDone; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + + [expectation fulfill]; + + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testDownloadSharesOperations { + id mockedDataSource = [self mockedDataSource]; + + NSString *temporaryPath = [self temporaryDirectory]; + NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; + NSURL *tempPath = [NSURL fileURLWithPath:[temporaryPath stringByAppendingPathComponent:@"sampleData.dat"]]; + NSData *sampleData = [self sampleData]; + [sampleData writeToURL:tempPath atomically:YES]; + + id mockedFileManager = [mockedDataSource fileManager]; + OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); + + __block BOOL enqueuedFirstDownload = NO; + __block BOOL enqueuedSecondDownload = NO; + + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + __block PFProgressBlock progressBlock = nil; + + id mockedCommandRunner = [mockedDataSource commandRunner]; + OCMStub([mockedCommandRunner runFileDownloadCommandAsyncWithFileURL:tempPath + targetFilePath:[OCMArg isNotNil] + cancellationToken:nil + progressBlock:[OCMArg checkWithBlock:^BOOL(id obj) { + progressBlock = obj; + return progressBlock != nil; + }]]).andReturn(taskCompletionSource.task).andDo(^(NSInvocation *invocation) { + XCTAssertFalse(enqueuedFirstDownload); + enqueuedFirstDownload = YES; + }); + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + + PFFileState *fileState = [[PFMutableFileState alloc] initWithName:@"sampleData" + urlString:[tempPath absoluteString] + mimeType:@"application/octet-stream"]; + + XCTestExpectation *firstExpectation = [self expectationWithDescription:@"downloadFileAsyncWithStateNumber1"]; + [[fileController downloadFileAsyncWithState:fileState cancellationToken:nil progressBlock:^(int percentDone) { + XCTAssertGreaterThan(percentDone, 0); + XCTAssertLessThanOrEqual(percentDone, 100); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + [firstExpectation fulfill]; + + return nil; + }]; + + NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:10.0]; + // Wait till the download operation starts + while (!enqueuedFirstDownload && [timeoutDate timeIntervalSinceNow] > 0.0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + XCTestExpectation *secondExpectation = [self expectationWithDescription:@"downloadFileAsyncWithStateNumber2"]; + [[fileController downloadFileAsyncWithState:fileState cancellationToken:nil progressBlock:^(int percentDone) { + XCTAssertGreaterThan(percentDone, 0); + XCTAssertLessThanOrEqual(percentDone, 100); + enqueuedSecondDownload = YES; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + [secondExpectation fulfill]; + + return nil; + }]; + + // Wait till the second operation is enqueued + timeoutDate = [NSDate dateWithTimeIntervalSinceNow:10.0]; + while (!enqueuedSecondDownload && [timeoutDate timeIntervalSinceNow] > 0.0) { + progressBlock(50); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + [taskCompletionSource trySetResult:nil]; + + [self waitForTestExpectations]; +} + +- (void)testDownloadCancel { + id mockedDataSource = [self mockedDataSource]; + + NSString *temporaryPath = [self temporaryDirectory]; + NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; + NSURL *tempPath = [NSURL fileURLWithPath:[temporaryPath stringByAppendingPathComponent:@"sampleData.dat"]]; + NSData *sampleData = [self sampleData]; + [sampleData writeToURL:tempPath atomically:YES]; + + id mockedFileManager = [mockedDataSource fileManager]; + OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + + id mockedCommandRunner = [mockedDataSource commandRunner]; + OCMStub([mockedCommandRunner runFileDownloadCommandAsyncWithFileURL:tempPath + targetFilePath:[OCMArg isNotNil] + cancellationToken:cancellationTokenSource.token + progressBlock:[OCMArg checkWithBlock:^BOOL(id obj) { + PFProgressBlock block = obj; + if (block) { + block(100); + } + return block != nil; + }]]).andReturn([BFTask cancelledTask]); + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + PFFileState *fileState = [[PFMutableFileState alloc] initWithName:@"sampleData" + urlString:[tempPath absoluteString] + mimeType:@"application/octet-stream"]; + + __block int progress = -1; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + + [[fileController downloadFileAsyncWithState:fileState + cancellationToken:cancellationTokenSource.token + progressBlock:^(int percentDone) { + XCTAssertTrue(progress <= percentDone); + progress = percentDone; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [cancellationTokenSource cancel]; + [self waitForTestExpectations]; +} + +- (void)testDownloadStream { + id mockedDataSource = [self mockedDataSource]; + + NSString *temporaryPath = [self temporaryDirectory]; + NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; + NSURL *tempPath = [NSURL fileURLWithPath:[temporaryPath stringByAppendingPathComponent:@"sampleData.dat"]]; + NSData *sampleData = [self sampleData]; + [sampleData writeToURL:tempPath atomically:YES]; + + id mockedFileManager = [mockedDataSource fileManager]; + OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); + + id mockedCommandRunner = [mockedDataSource commandRunner]; + OCMStub([mockedCommandRunner runFileDownloadCommandAsyncWithFileURL:tempPath + targetFilePath:[OCMArg isNotNil] + cancellationToken:nil + progressBlock:[OCMArg checkWithBlock:^BOOL(id obj) { + PFProgressBlock block = obj; + if (block) { + block(100); + } + return block != nil; + }]]).andReturn([BFTask taskWithResult:nil]); + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + PFFileState *fileState = [[PFMutableFileState alloc] initWithName:@"sampleData" + urlString:[tempPath absoluteString] + mimeType:@"application/octet-stream"]; + + __block int progress = -1; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[fileController downloadFileStreamAsyncWithState:fileState + cancellationToken:nil + progressBlock:^(int percentDone) { + XCTAssertTrue(progress <= percentDone); + + progress = percentDone; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + PFAssertIsKindOfClass(task.result, [NSInputStream class]); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testDownloadStreamSharesOperations { + id mockedDataSource = [self mockedDataSource]; + + NSString *temporaryPath = [self temporaryDirectory]; + NSURL *tempPath = [NSURL fileURLWithPath:[temporaryPath stringByAppendingPathComponent:@"sampleData.dat"]]; + NSData *sampleData = [self sampleData]; + [sampleData writeToURL:tempPath atomically:YES]; + + __block BOOL enqueuedFirstDownload = NO; + __block BOOL enqueuedSecondDownload = NO; + + BFTaskCompletionSource *taskCompletionSource = [BFTaskCompletionSource taskCompletionSource]; + __block PFProgressBlock progressBlock = nil; + + id mockedCommandRunner = [mockedDataSource commandRunner]; + OCMStub([mockedCommandRunner runFileDownloadCommandAsyncWithFileURL:tempPath + targetFilePath:[OCMArg isNotNil] + cancellationToken:nil + progressBlock:[OCMArg checkWithBlock:^BOOL(id obj) { + progressBlock = obj; + return progressBlock != nil; + }]]).andReturn(taskCompletionSource.task).andDo(^(NSInvocation *invocation) { + XCTAssertFalse(enqueuedFirstDownload); + enqueuedFirstDownload = YES; + }); + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + + PFFileState *fileState = [[PFMutableFileState alloc] initWithName:@"sampleData" + urlString:[tempPath absoluteString] + mimeType:@"application/octet-stream"]; + + XCTestExpectation *firstExpectation = [self expectationWithDescription:@"downloadFileAsyncWithStateNumber1"]; + [[fileController downloadFileStreamAsyncWithState:fileState cancellationToken:nil progressBlock:^(int percentDone) { + XCTAssertGreaterThan(percentDone, 0); + XCTAssertLessThanOrEqual(percentDone, 100); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + PFAssertIsKindOfClass(task.result, [NSInputStream class]); + [firstExpectation fulfill]; + return nil; + }]; + + NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:10.0]; + // Wait till the download operation starts + while (!enqueuedFirstDownload && [timeoutDate timeIntervalSinceNow] > 0.0) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + XCTestExpectation *secondExpectation = [self expectationWithDescription:@"downloadFileAsyncWithStateNumber2"]; + [[fileController downloadFileStreamAsyncWithState:fileState cancellationToken:nil progressBlock:^(int percentDone) { + XCTAssertGreaterThan(percentDone, 0); + XCTAssertLessThanOrEqual(percentDone, 100); + enqueuedSecondDownload = YES; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + PFAssertIsKindOfClass(task.result, [NSInputStream class]); + [secondExpectation fulfill]; + return nil; + }]; + + // Wait till the second operation is enqueued + timeoutDate = [NSDate dateWithTimeIntervalSinceNow:10.0]; + while (!enqueuedSecondDownload && [timeoutDate timeIntervalSinceNow] > 0.0) { + progressBlock(50); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + [taskCompletionSource trySetResult:nil]; + + [self waitForTestExpectations]; +} + +- (void)testUpload { + id mockedDataSource = [self mockedDataSource]; + + NSString *temporaryPath = [self temporaryDirectory]; + NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; + NSURL *tempPath = [NSURL fileURLWithPath:[temporaryPath stringByAppendingPathComponent:@"sampleData.dat"]]; + NSData *sampleData = [self sampleData]; + [sampleData writeToURL:tempPath atomically:YES]; + + id mockedFileManager = [mockedDataSource fileManager]; + OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); + + + + NSDictionary *result = (@{ @"name": @"sampleData", @"url": [tempPath absoluteString] }); + PFCommandResult *commandResult = [PFCommandResult commandResultWithResult:result + resultString:nil + httpResponse:nil]; + + id mockedCommandRunner = [mockedDataSource commandRunner]; + [OCMStub(([[mockedCommandRunner ignoringNonObjectArgs] runFileUploadCommandAsync:[OCMArg isNotNil] + withContentType:@"application/octet-stream" + contentSourceFilePath:[tempPath path] + options:0 + cancellationToken:nil + progressBlock:[OCMArg isNotNil]])) andReturn:[BFTask taskWithResult:commandResult]]; + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + PFFileState *fileState = [[PFMutableFileState alloc] initWithName:@"sampleData" + urlString:nil + mimeType:@"application/octet-stream"]; + + __block int progress = -1; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[fileController uploadFileAsyncWithState:fileState + sourceFilePath:[tempPath path] + sessionToken:@"session-token" + cancellationToken:nil + progressBlock:^(int percentDone) { + XCTAssertTrue(progress <= percentDone); + progress = percentDone; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testUploadCancel { + id mockedDataSource = PFStrictProtocolMock(@protocol(FileControllerDataSource)); + id mockedCommandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id mockedFileManager = PFStrictClassMock([PFFileManager class]); + + NSString *temporaryPath = [self temporaryDirectory]; + NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; + NSURL *tempPath = [NSURL fileURLWithPath:[temporaryPath stringByAppendingPathComponent:@"sampleData.dat"]]; + NSData *sampleData = [self sampleData]; + [sampleData writeToURL:tempPath atomically:YES]; + + OCMStub([mockedDataSource fileManager]).andReturn(mockedFileManager); + OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); + OCMStub([mockedDataSource commandRunner]).andReturn(mockedCommandRunner); + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [OCMStub(([[mockedCommandRunner ignoringNonObjectArgs] runFileUploadCommandAsync:[OCMArg isNotNil] + withContentType:@"application/octet-stream" + contentSourceFilePath:[tempPath path] + options:0 + cancellationToken:cancellationTokenSource.token + progressBlock:[OCMArg isNotNil]])) andReturn:[BFTask cancelledTask]]; + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + PFFileState *fileState = [[PFMutableFileState alloc] initWithName:@"sampleData" + urlString:nil + mimeType:@"application/octet-stream"]; + + __block int progress = -1; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[fileController uploadFileAsyncWithState:fileState + sourceFilePath:[tempPath path] + sessionToken:@"session-token" + cancellationToken:cancellationTokenSource.token + progressBlock:^(int percentDone) { + XCTAssertTrue(progress <= percentDone); + progress = percentDone; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + + [cancellationTokenSource cancel]; + [self waitForTestExpectations]; +} + +- (void)testClearCaches { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFFileManagerProvider)); + id mockedFileManager = PFStrictClassMock([PFFileManager class]); + + NSString *temporaryPath = [self temporaryDirectory]; + NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; + + OCMStub([mockedDataSource fileManager]).andReturn(mockedFileManager); + OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[fileController clearFileCacheAsync] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + [expectation fulfill]; + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testStagedDirectoryPath { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFFileManagerProvider)); + id mockedFileManager = PFStrictClassMock([PFFileManager class]); + + NSString *temporaryPath = [self temporaryDirectory]; + + OCMStub([mockedDataSource fileManager]).andReturn(mockedFileManager); + OCMStub([mockedFileManager parseLocalSandboxDataDirectoryPath]).andReturn(temporaryPath); + + PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; + + XCTAssertEqualObjects([temporaryPath stringByAppendingPathComponent:@"PFFileStaging"], + [fileController stagedFilesDirectoryPath]); +} + +@end diff --git a/Tests/Unit/FileStateTests.m b/Tests/Unit/FileStateTests.m new file mode 100644 index 000000000..3bd0d78f1 --- /dev/null +++ b/Tests/Unit/FileStateTests.m @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableFileState.h" +#import "PFTestCase.h" + +@interface FileStateTests : PFTestCase + +@end + +@implementation FileStateTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFFileState *)sampleFileState { + return [[PFFileState alloc] initWithName:@"yarr" urlString:@"http://yolo" mimeType:@"boom"]; +} + +- (void)assertFileState:(PFFileState *)state equalToState:(PFFileState *)differentState { + XCTAssertEqualObjects(state, differentState); + + XCTAssertEqualObjects(state.name, differentState.name); + XCTAssertEqualObjects(state.urlString, differentState.urlString); + XCTAssertEqualObjects(state.mimeType, differentState.mimeType); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testInit { + PFFileState *state = [[PFFileState alloc] init]; + XCTAssertNil(state.name); + XCTAssertNil(state.urlString); + XCTAssertNil(state.mimeType); + + state = [[PFMutableFileState alloc] init]; + XCTAssertNil(state.name); + XCTAssertNil(state.urlString); + XCTAssertNil(state.mimeType); +} + +- (void)testInitWithState { + PFFileState *sampleState = [self sampleFileState]; + + PFFileState *state = [[PFFileState alloc] initWithState:sampleState]; + [self assertFileState:state equalToState:sampleState]; + + state = [[PFMutableFileState alloc] initWithState:sampleState]; + [self assertFileState:state equalToState:sampleState]; +} + +- (void)testInitWithProperties { + PFFileState *sampleState = [self sampleFileState]; + + PFFileState *state = [[PFFileState alloc] initWithName:sampleState.name + urlString:sampleState.urlString + mimeType:sampleState.mimeType]; + [self assertFileState:state equalToState:sampleState]; + + state = [[PFMutableFileState alloc] initWithName:sampleState.name + urlString:sampleState.urlString + mimeType:sampleState.mimeType]; + [self assertFileState:state equalToState:sampleState]; +} + +- (void)testWithEmptyProperties { + PFFileState *state = [[PFFileState alloc] initWithName:nil + urlString:nil + mimeType:nil]; + XCTAssertEqualObjects(state.name, @"file"); + XCTAssertNil(state.urlString); + XCTAssertNil(state.mimeType); + + state = [[PFMutableFileState alloc] initWithName:nil + urlString:nil + mimeType:nil]; + XCTAssertEqualObjects(state.name, @"file"); + XCTAssertNil(state.urlString); + XCTAssertNil(state.mimeType); +} + +- (void)testCopying { + PFFileState *sampleState = [self sampleFileState]; + [self assertFileState:[sampleState copy] equalToState:sampleState]; + + sampleState = [[PFMutableFileState alloc] initWithState:sampleState]; + [self assertFileState:[sampleState copy] equalToState:sampleState]; +} + +- (void)testMutableCopying { + PFMutableFileState *state = [[self sampleFileState] mutableCopy]; + state.name = @"a"; + XCTAssertEqualObjects(state.name, @"a"); +} + +- (void)testMutableAccessors { + PFMutableFileState *state = [[PFMutableFileState alloc] init]; + state.name = @"a"; + XCTAssertEqualObjects(state.name, @"a"); + state.urlString = @"b"; + XCTAssertEqualObjects(state.urlString, @"b"); + state.mimeType = @"c"; + XCTAssertEqualObjects(state.mimeType, @"c"); +} + +@end diff --git a/Tests/Unit/FileUnitTests.m b/Tests/Unit/FileUnitTests.m new file mode 100644 index 000000000..53673e5e6 --- /dev/null +++ b/Tests/Unit/FileUnitTests.m @@ -0,0 +1,451 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCoreManager.h" +#import "PFFile.h" +#import "PFFileController.h" +#import "PFFileManager.h" +#import "PFFileState.h" +#import "PFFile_Private.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +static NSData *dataFromInputStream(NSInputStream *inputStream) { + NSMutableData *results = [[NSMutableData alloc] init]; + + [inputStream open]; + + while (inputStream.streamError == nil && inputStream.hasBytesAvailable) { + uint8_t buffer[1024]; + size_t bytesRead = [inputStream read:buffer maxLength:1024]; + + if (bytesRead == -1) { + break; + } + + [results appendBytes:buffer length:bytesRead]; + } + + [inputStream close]; + + return results; +} + +@interface FileUnitTestsInvocationVerifier : NSObject + +@end + +@implementation FileUnitTestsInvocationVerifier + +- (void)verifyObject:(id)object error:(NSError *)error { + +} + +@end + +@interface FileUnitTests : PFUnitTestCase + +@end + +@implementation FileUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (NSString *)sampleFilePath { + NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"sampleData.dat"]; + [[self sampleData] writeToFile:path atomically:YES]; + + return path; +} + +- (NSString *)sampleStagingPath { + return [NSTemporaryDirectory() stringByAppendingPathComponent:@"staged-files"]; +} + +- (NSData *)sampleData { + const uint8_t bytes[] = { + [0 ... 255] = 0x7F + }; + + return [NSData dataWithBytes:bytes length:sizeof(bytes)]; +} + +- (void)clearStagingAndTemporaryFiles { + [[NSFileManager defaultManager] removeItemAtPath:[self sampleFilePath] error:NULL]; + [[NSFileManager defaultManager] removeItemAtPath:[self sampleStagingPath] error:NULL]; + [[NSFileManager defaultManager] createDirectoryAtPath:[self sampleStagingPath] + withIntermediateDirectories:YES + attributes:nil + error:NULL]; +} + +- (PFFileController *)mockedFileController { + id mockedFileController = PFStrictClassMock([PFFileController class]); + + NSString *stagedDirectory = [self sampleStagingPath]; + [self clearStagingAndTemporaryFiles]; + + OCMStub([mockedFileController stagedFilesDirectoryPath]).andReturn(stagedDirectory); + + return mockedFileController; +} + +- (PFProgressBlock)progressValidationBlock { + __block int currentProgress = 0; + return [^(int progress) { + XCTAssertLessThanOrEqual(currentProgress, progress); + currentProgress = progress; + } copy]; +} + +- (XCTestExpectation *)expectationForSelector:(SEL)cmd { + return [self expectationWithDescription:NSStringFromSelector(cmd)]; +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [Parse _currentManager].coreManager.fileController = [self mockedFileController]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testContructors { + [self clearStagingAndTemporaryFiles]; + PFFile *file = [PFFile fileWithData:[NSData data]]; + XCTAssertEqualObjects(file.name, @"file"); + XCTAssertNil(file.url); + XCTAssertTrue(file.isDirty); + XCTAssertTrue(file.isDataAvailable); + + [self clearStagingAndTemporaryFiles]; + file = [PFFile fileWithData:[NSData data] contentType:@"content-type"]; + XCTAssertEqualObjects(file.name, @"file"); + XCTAssertNil(file.url); + XCTAssertTrue(file.isDirty); + XCTAssertTrue(file.isDataAvailable); + + [self clearStagingAndTemporaryFiles]; + file = [PFFile fileWithName:@"name" data:[NSData data]]; + XCTAssertEqualObjects(file.name, @"name"); + XCTAssertNil(file.url); + XCTAssertTrue(file.isDirty); + XCTAssertTrue(file.isDataAvailable); + + [self clearStagingAndTemporaryFiles]; + file = [PFFile fileWithName:nil contentsAtPath:[self sampleFilePath]]; + XCTAssertEqualObjects(file.name, @"file"); + XCTAssertNil(file.url); + XCTAssertTrue(file.isDirty); + XCTAssertTrue(file.isDataAvailable); + + [self clearStagingAndTemporaryFiles]; + NSError *error = nil; + file = [PFFile fileWithName:nil contentsAtPath:[self sampleFilePath] error:&error]; + XCTAssertNil(error); + XCTAssertEqualObjects(file.name, @"file"); + XCTAssertNil(file.url); + XCTAssertTrue(file.isDirty); + XCTAssertTrue(file.isDataAvailable); + + [self clearStagingAndTemporaryFiles]; + file = [PFFile fileWithName:nil data:[NSData data] contentType:@"content-type"]; + XCTAssertEqualObjects(file.name, @"file"); + XCTAssertNil(file.url); + XCTAssertTrue(file.isDirty); + XCTAssertTrue(file.isDataAvailable); + + [self clearStagingAndTemporaryFiles]; + file = [PFFile fileWithName:nil data:[NSData data] contentType:@"content-type" error:&error]; + XCTAssertNil(error); + XCTAssertEqualObjects(file.name, @"file"); + XCTAssertNil(file.url); + XCTAssertTrue(file.isDirty); + XCTAssertTrue(file.isDataAvailable); +} + +- (void)testConstructorWithTooLargeData { + NSMutableData *data = [NSMutableData dataWithLength:(10 * 1048576 + 1)]; + PFAssertThrowsInvalidArgumentException([PFFile fileWithData:data]); +} + +- (void)testUploading { + id mockedFileController = [Parse _currentManager].coreManager.fileController; + PFFileState *expectedState = [[PFFileState alloc] initWithName:@"file" + urlString:nil + mimeType:@"application/octet-stream"]; + + OCMStub([mockedFileController uploadFileAsyncWithState:expectedState + sourceFilePath:[OCMArg isNotNil] + sessionToken:nil + cancellationToken:[OCMArg isNotNil] + progressBlock:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained PFFileState *state = nil; + __unsafe_unretained PFProgressBlock progressBlock = nil; + + [invocation getArgument:&state atIndex:2]; + [invocation getArgument:&progressBlock atIndex:6]; + + if (progressBlock) { + progressBlock(100); + } + + __autoreleasing BFTask *resultTask = [BFTask taskWithResult:state]; + [invocation setReturnValue:&resultTask]; + }); + + NSError *error = nil; + XCTestExpectation *expectation = nil; + + PFFile *file = [PFFile fileWithData:[self sampleData] contentType:@"application/octet-stream"]; + + XCTAssertTrue([file save]); + XCTAssertTrue([file save:&error]); + XCTAssertNil(error); + + expectation = [self expectationForSelector:@selector(saveInBackground)]; + [[file saveInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.completed); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + expectation = [self expectationForSelector:@selector(saveInBackgroundWithProgressBlock:)]; + [[file saveInBackgroundWithProgressBlock:[self progressValidationBlock]] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.completed); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + expectation = [self expectationForSelector:@selector(saveInBackgroundWithBlock:)]; + [file saveInBackgroundWithBlock:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + + expectation = [self expectationForSelector:@selector(saveInBackgroundWithBlock:progressBlock:)]; + [file saveInBackgroundWithBlock:^(BOOL success, NSError *error) { + XCTAssertTrue(success); + [expectation fulfill]; + } progressBlock:[self progressValidationBlock]]; + [self waitForTestExpectations]; + + expectation = [self expectationForSelector:@selector(saveInBackgroundWithTarget:selector:)]; + id verifier = PFStrictClassMock([FileUnitTestsInvocationVerifier class]); + OCMStub([verifier verifyObject:@YES error:nil]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + [file saveInBackgroundWithTarget:verifier selector:@selector(verifyObject:error:)]; + + [self waitForTestExpectations]; +} + +- (void)testDownloading{ + id mockedFileController = [Parse _currentManager].coreManager.fileController; + PFFileState *expectedState = [[PFFileState alloc] initWithName:@"file" + urlString:@"http://some.place" + mimeType:nil]; + + NSString *cachedPath = [self sampleFilePath]; + + OCMStub([mockedFileController cachedFilePathForFileState:expectedState]).andReturn(cachedPath); + OCMStub([mockedFileController downloadFileAsyncWithState:expectedState + cancellationToken:OCMOCK_ANY + progressBlock:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained PFProgressBlock progressBlock = nil; + [invocation getArgument:&progressBlock atIndex:4]; + if (progressBlock) { + progressBlock(100); + } + + [[self sampleData] writeToFile:cachedPath atomically:YES]; + __autoreleasing BFTask *results = [BFTask taskWithResult:nil]; + [invocation setReturnValue:&results]; + }); + + OCMStub([mockedFileController downloadFileStreamAsyncWithState:expectedState + cancellationToken:OCMOCK_ANY + progressBlock:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained PFProgressBlock progressBlock = nil; + [invocation getArgument:&progressBlock atIndex:4]; + if (progressBlock) { + progressBlock(100); + } + + [[self sampleData] writeToFile:cachedPath atomically:YES]; + __autoreleasing BFTask *results = [BFTask taskWithResult:[NSInputStream inputStreamWithFileAtPath:cachedPath]]; + [invocation setReturnValue:&results]; + }); + +#define wait_next [self waitForTestExpectations]; \ + [[NSFileManager defaultManager] removeItemAtPath:cachedPath error:NULL] + + NSError *error = nil; + XCTestExpectation *expectation = nil; + + NSData *expectedData = [self sampleData]; + PFFile *file = [PFFile fileWithName:@"file" url:@"http://some.place"]; + + XCTAssertEqualObjects([file getData], expectedData); + + [[NSFileManager defaultManager] removeItemAtPath:cachedPath error:NULL]; + XCTAssertEqualObjects([file getData:&error], expectedData); + XCTAssertNil(error); + + [[NSFileManager defaultManager] removeItemAtPath:cachedPath error:NULL]; + XCTAssertEqualObjects(dataFromInputStream([file getDataStream]), expectedData); + + [[NSFileManager defaultManager] removeItemAtPath:cachedPath error:NULL]; + XCTAssertEqualObjects(dataFromInputStream([file getDataStream:&error]), expectedData); + XCTAssertNil(error); + + [[NSFileManager defaultManager] removeItemAtPath:cachedPath error:NULL]; + expectation = [self expectationForSelector:@selector(getDataInBackground)]; + [[file getDataInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, expectedData); + [expectation fulfill]; + return nil; + }]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataInBackgroundWithProgressBlock:)]; + [[file getDataInBackgroundWithProgressBlock:[self progressValidationBlock]] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, expectedData); + [expectation fulfill]; + return nil; + }]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataInBackgroundWithBlock:)]; + [file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) { + XCTAssertEqualObjects(data, expectedData); + [expectation fulfill]; + }]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataInBackgroundWithBlock:progressBlock:)]; + [file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) { + XCTAssertEqualObjects(data, expectedData); + [expectation fulfill]; + } progressBlock:[self progressValidationBlock]]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataStreamInBackground)]; + [[file getDataStreamInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(dataFromInputStream(task.result), expectedData); + [expectation fulfill]; + return nil; + }]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataStreamInBackgroundWithBlock:)]; + [file getDataStreamInBackgroundWithBlock:^(NSInputStream *inputStream, NSError *error) { + XCTAssertEqualObjects(dataFromInputStream(inputStream), expectedData); + [expectation fulfill]; + }]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataStreamInBackgroundWithProgressBlock:)]; + [[file getDataStreamInBackgroundWithProgressBlock:[self progressValidationBlock]] + continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(dataFromInputStream(task.result), expectedData); + [expectation fulfill]; + return nil; + }]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataStreamInBackgroundWithBlock:progressBlock:)]; + [file getDataStreamInBackgroundWithBlock:^(NSInputStream *inputStream, NSError *error) { + XCTAssertEqualObjects(dataFromInputStream(inputStream), expectedData); + [expectation fulfill]; + } progressBlock:[self progressValidationBlock]]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataInBackgroundWithTarget:selector:)]; + id verifier = PFStrictClassMock([FileUnitTestsInvocationVerifier class]); + OCMStub([verifier verifyObject:expectedData error:nil]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + [file getDataInBackgroundWithTarget:verifier selector:@selector(verifyObject:error:)]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataDownloadStreamInBackground)]; + [[file getDataDownloadStreamInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(dataFromInputStream(task.result), expectedData); + [expectation fulfill]; + return nil; + }]; + + wait_next; + expectation = [self expectationForSelector:@selector(getDataDownloadStreamInBackgroundWithProgressBlock:)]; + [[file getDataDownloadStreamInBackgroundWithProgressBlock:[self progressValidationBlock]] + continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(dataFromInputStream(task.result), expectedData); + [expectation fulfill]; + return nil; + }]; + + wait_next; +} + +- (void)testCancel { + id mockedFileController = [Parse _currentManager].coreManager.fileController; + PFFileState *expectedState = [[PFFileState alloc] initWithName:@"file" + urlString:@"http://some.place" + mimeType:nil]; + + NSString *cachedPath = [self sampleFilePath]; + + OCMStub([mockedFileController cachedFilePathForFileState:expectedState]).andReturn(cachedPath); + OCMStub([mockedFileController downloadFileAsyncWithState:expectedState + cancellationToken:OCMOCK_ANY + progressBlock:OCMOCK_ANY])._andDo(^(NSInvocation *invocation) { + __unsafe_unretained PFProgressBlock progressBlock = nil; + [invocation getArgument:&progressBlock atIndex:4]; + if (progressBlock) { + progressBlock(100); + } + + [[self sampleData] writeToFile:cachedPath atomically:YES]; + __autoreleasing BFTask *results = [BFTask cancelledTask]; + [invocation setReturnValue:&results]; + }); + + XCTestExpectation *expectation = nil; + PFFile *file = [PFFile fileWithName:@"file" url:@"http://some.place"]; + + [[NSFileManager defaultManager] removeItemAtPath:cachedPath error:NULL]; + expectation = [self currentSelectorTestExpectation]; + [[file getDataInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + + return nil; + }]; + + [file cancel]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/GeoPointLocationTests.m b/Tests/Unit/GeoPointLocationTests.m new file mode 100644 index 000000000..171a3c0e1 --- /dev/null +++ b/Tests/Unit/GeoPointLocationTests.m @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "CLLocationManager+TestAdditions.h" +#import "PFCoreManager.h" +#import "PFGeoPoint.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface GeoPointLocationTests : PFUnitTestCase + +@end + +@implementation GeoPointLocationTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [CLLocationManager reset]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testGeoPointForCurrentLocation { + [CLLocationManager setMockingEnabled:YES]; + + __block NSInteger returnedGeoPoints = 0; + // Simulate a delayed response from locationManager, make sure PFGeoPoints are + // returned for all requests. + [CLLocationManager setReturnLocation:NO]; + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + XCTAssertEqualWithAccuracy([geoPoint latitude], CL_DEFAULT_LATITUDE, 0.00001, + @"Current location should have been set to fakeLocation"); + XCTAssertEqualWithAccuracy([geoPoint longitude], CL_DEFAULT_LONGITUDE, 0.00001, + @"Current location should have been set to fakeLocation"); + XCTAssertNil(error, @"No error should have been found"); + if (geoPoint) { + returnedGeoPoints++; + } + }]; + [CLLocationManager setReturnLocation:YES]; + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + if (geoPoint) { + returnedGeoPoints++; + } + }]; + XCTAssertEqual(2, returnedGeoPoints, @"Both blocks should have been called"); + + returnedGeoPoints = 0; + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + if (geoPoint) { + returnedGeoPoints++; + } + }]; + XCTAssertEqual(1, returnedGeoPoints, @"Only the final block should have been called"); +} + +- (void)testGeoLocationManager { + [CLLocationManager setMockingEnabled:YES]; + + // Short-circuits the locationManager so it calls the delegate's -locationManager:didFailWithError: + [CLLocationManager setWillFail:YES]; + __block NSInteger errorCount = 0; + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + if (error) { + errorCount++; + } + }]; + XCTAssertEqual(1, errorCount, @"Failure is passed back as an error"); + + [CLLocationManager setWillFail:NO]; + __block NSInteger geoPointCount = 0; + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + if (geoPoint) { + geoPointCount++; + } + }]; + XCTAssertEqual(1, geoPointCount, @"CLLocationManager should be passing back locations"); +} + +- (void)testGeoPointForCurrentLocationNested { + // Short-circuits the locationManager so it calls the delegate's -locationManager:didFailWithError: + [CLLocationManager setWillFail:YES]; + __block NSInteger errorCount = 0; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + if (error) { + errorCount++; + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + errorCount++; + [expectation fulfill]; + }]; + } + }]; + [self waitForTestExpectations]; + XCTAssertEqual(2, errorCount, @"Failure is passed back as an error"); +} + +- (void)testGeoPointForCurrentLocationFromBackgroundThread { + [CLLocationManager setMockingEnabled:YES]; + [CLLocationManager setReturnLocation:YES]; + [CLLocationManager setWillFail:NO]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [PFGeoPoint geoPointForCurrentLocationInBackground:^(PFGeoPoint *geoPoint, NSError *error) { + [expectation fulfill]; + }]; + }); + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/GeoPointUnitTests.m b/Tests/Unit/GeoPointUnitTests.m new file mode 100644 index 000000000..10a4e4d6a --- /dev/null +++ b/Tests/Unit/GeoPointUnitTests.m @@ -0,0 +1,243 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFGeoPoint.h" +#import "PFGeoPointPrivate.h" +#import "PFTestCase.h" + + +@interface GeoPointUnitTests : PFTestCase + +@end + +@implementation GeoPointUnitTests + +- (void)testDefaults { + PFGeoPoint *point = [PFGeoPoint geoPoint]; + + // Check default values + XCTAssertEqualWithAccuracy([point latitude], 0.0, 0.00001, @"Latitude should be 0.0"); + XCTAssertEqualWithAccuracy([point longitude], 0.0, 0.00001, @"Longitude should be 0.0"); +} + +- (void)testGeoPointFromLocation { + CLLocation *location = [[CLLocation alloc] initWithLatitude:10.0 longitude:20.0]; + + PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLocation:location]; + XCTAssertEqual(geoPoint.latitude, location.coordinate.latitude); + XCTAssertEqual(geoPoint.longitude, location.coordinate.longitude); + + geoPoint = [PFGeoPoint geoPointWithLocation:nil]; + XCTAssertEqual(geoPoint.latitude, 0); + XCTAssertEqual(geoPoint.longitude, 0); +} + +- (void)testGeoPointDictionaryEncoding { + PFGeoPoint *point = [PFGeoPoint geoPointWithLatitude:10 longitude:20]; + + NSDictionary *dictionary = [point encodeIntoDictionary]; + XCTAssertNotNil(dictionary); + + PFGeoPoint *pointFromDictionary = [PFGeoPoint geoPointWithDictionary:dictionary]; + XCTAssertEqualObjects(pointFromDictionary, point); + XCTAssertEqual(point.latitude, pointFromDictionary.latitude); + XCTAssertEqual(point.longitude, pointFromDictionary.longitude); +} + +- (void)testGeoExceptions { + PFGeoPoint *point = [PFGeoPoint geoPointWithLatitude:34.0 longitude:24.0]; + + // Setter exceptions + PFAssertThrowsInvalidArgumentException([point setLatitude:90.001]); + PFAssertThrowsInvalidArgumentException([point setLatitude:-90.001]); + PFAssertThrowsInvalidArgumentException([point setLongitude:180.001]); + PFAssertThrowsInvalidArgumentException([point setLongitude:-180.001]); +} + +- (void)testGeoPointEquality { + PFGeoPoint *pointA = [PFGeoPoint geoPointWithLatitude:10.2 longitude:11.3]; + PFGeoPoint *pointB = [PFGeoPoint geoPointWithLatitude:10.2 longitude:11.3]; + + XCTAssertTrue([pointA isEqual:pointB]); + XCTAssertTrue([pointB isEqual:pointA]); + + XCTAssertFalse([pointA isEqual:@YES]); + XCTAssertTrue([pointA isEqual:pointA]); +} + +- (void)testGeoPointHash { + PFGeoPoint *pointA = [PFGeoPoint geoPointWithLatitude:10.2 longitude:11.3]; + PFGeoPoint *pointB = [PFGeoPoint geoPointWithLatitude:10.2 longitude:11.3]; + XCTAssertEqual([pointA hash], [pointB hash]); +} + +- (void)testGeoUtilityDistance { + double D2R = M_PI / 180.0; + PFGeoPoint *pointA = [PFGeoPoint geoPoint]; + PFGeoPoint *pointB = [PFGeoPoint geoPoint]; + + // Zero + XCTAssertEqualWithAccuracy([pointA distanceInRadiansTo:pointB], 0.0, 0.000001, + @"Origin points with non-zero distance."); + XCTAssertEqualWithAccuracy([pointB distanceInRadiansTo:pointA], 0.0, 0.000001, + @"Origin points with non-zero distance."); + // Wrap Long + [pointA setLongitude:179.0]; + [pointB setLongitude:-179.0]; + XCTAssertEqualWithAccuracy([pointA distanceInRadiansTo:pointB], 2.0 * D2R, 0.000001, + @"Long wrap angular distance error."); + XCTAssertEqualWithAccuracy([pointB distanceInRadiansTo:pointA], 2.0 * D2R, 0.000001, + @"Long wrap angular distance error."); + + // North South Lat + [pointA setLatitude:89.0]; + [pointA setLongitude:0.0]; + [pointB setLatitude:-89.0]; + [pointB setLongitude:0.0]; + + XCTAssertEqualWithAccuracy([pointA distanceInRadiansTo:pointB], 178.0 * D2R, 0.000001, + @"NS pole wrap error"); + XCTAssertEqualWithAccuracy([pointB distanceInRadiansTo:pointA], 178.0 * D2R, 0.000001, + @"NS pole wrap error"); + + // Long wrap Lat + [pointA setLatitude:89.0]; + [pointA setLongitude:0.0]; + [pointB setLatitude:-89.0]; + [pointB setLongitude:179.9999]; + + XCTAssertEqualWithAccuracy([pointA distanceInRadiansTo:pointB], 180 * D2R, 0.00001, + @"Lat wrap error."); + XCTAssertEqualWithAccuracy([pointB distanceInRadiansTo:pointA], 180 * D2R, 0.00001, + @"Lat wrap error."); + + [pointA setLatitude:79.0]; + [pointA setLongitude:90.0]; + [pointB setLatitude:-79.0]; + [pointB setLongitude:-90.0]; + + XCTAssertEqualWithAccuracy([pointA distanceInRadiansTo:pointB], 180.0 * D2R, 0.00001, + @"Lat long wrap"); + XCTAssertEqualWithAccuracy([pointB distanceInRadiansTo:pointA], 180.0 * D2R, 0.00001, + @"Lat long wrap"); + + // Wrap near pole - somewhat ill conditioned case due to pole proximity + [pointA setLatitude:85.0]; + [pointA setLongitude:90.0]; + [pointB setLatitude:85.0]; + [pointB setLongitude:-90.0]; + + XCTAssertEqualWithAccuracy([pointA distanceInRadiansTo:pointB], 10.0 * D2R, 0.00001, + @"Pole proximity fail"); + XCTAssertEqualWithAccuracy([pointB distanceInRadiansTo:pointA], 10.0 * D2R, 0.00001, + @"Pole proximity fail"); + + // Reference cities + // Sydney Australia + [pointA setLatitude:-34.0]; + [pointA setLongitude:151.0]; + + // Buenos Aires + [pointB setLatitude:-34.5]; + [pointB setLongitude:-58.35]; + + XCTAssertEqualWithAccuracy([pointA distanceInRadiansTo:pointB], 1.85, 0.01, + @"Sydney to Buenos Aires Fail"); + XCTAssertEqualWithAccuracy([pointB distanceInRadiansTo:pointA], 1.85, 0.01, + @"Sydney to Buenos Aires Fail"); + + // [SAC] 38.52 -121.50 Sacramento,CA + PFGeoPoint *sacramento = [PFGeoPoint geoPointWithLatitude:38.52 longitude:-121.50]; + + // [HNL] 21.35 -157.93 Honolulu Int,HI + PFGeoPoint *honolulu = [PFGeoPoint geoPointWithLatitude:21.35 longitude:-157.93]; + + // [51Q] 37.75 -122.68 San Francisco,CA + PFGeoPoint *sanfran = [PFGeoPoint geoPointWithLatitude:37.75 longitude:-122.68]; + + // Vorkuta 67.509619,64.085999 + PFGeoPoint *vorkuta = [PFGeoPoint geoPointWithLatitude:67.509619 longitude:64.085999]; + + // London + PFGeoPoint *london = [PFGeoPoint geoPointWithLatitude:51.501904 longitude:-0.115356]; + + // Northampton + PFGeoPoint *northampton = [PFGeoPoint geoPointWithLatitude:52.241256 longitude:-0.895386]; + + // Powell St BART station + PFGeoPoint *powell = [PFGeoPoint geoPointWithLatitude:37.78507 longitude:-122.407007]; + + // Apple store + PFGeoPoint *astore = [PFGeoPoint geoPointWithLatitude:37.785809 longitude:-122.406363]; + + // Self + XCTAssertEqualWithAccuracy([honolulu distanceInKilometersTo:honolulu], 0.0, 0.000001, @"Self distance"); + + // SAC to HNL + XCTAssertEqualWithAccuracy([sacramento distanceInKilometersTo:honolulu], 3964.8, 10.0, @"SAC to HNL"); + XCTAssertEqualWithAccuracy([sacramento distanceInMilesTo:honolulu], 2463.6, 10.0, @"SAC to HNL"); + + // Semi-local + XCTAssertEqualWithAccuracy([london distanceInKilometersTo:northampton], 98.4, 1.0, @"London Northampton"); + XCTAssertEqualWithAccuracy([london distanceInMilesTo:northampton], 61.2, 1.0, @"London Northampton"); + + XCTAssertEqualWithAccuracy([london distanceInKilometersTo:northampton], 98.4, 1.0, @"London Northampton"); + XCTAssertEqualWithAccuracy([london distanceInMilesTo:northampton], 61.2, 1.0, @"London Northampton"); + + XCTAssertEqualWithAccuracy([sacramento distanceInKilometersTo:sanfran], 134.5, 2.0, @"Sacramento San Fran"); + XCTAssertEqualWithAccuracy([sacramento distanceInMilesTo:sanfran], 84.8, 2.0, @"Sacramento San Fran"); + + // Very local + XCTAssertEqualWithAccuracy([powell distanceInKilometersTo:astore], 0.1, 0.05, @"Powell station and Apple store"); + + // Far (for error tolerance's sake) + XCTAssertEqualWithAccuracy([sacramento distanceInKilometersTo:vorkuta], 8303.8, 100.0, @"Sacramento to Vorkuta"); + XCTAssertEqualWithAccuracy([sacramento distanceInMilesTo:vorkuta], 5159.7, 100.0, @"Sacramento to Vorkuta"); +} + +- (void)testNSCopying { + PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]; + PFGeoPoint *geoPointCopy = [geoPoint copy]; + + XCTAssertEqual(geoPointCopy.latitude, geoPoint.latitude, @"Latitude should be the same."); + XCTAssertEqual(geoPointCopy.longitude, geoPoint.longitude, @"Longitude should be the same."); +} + +- (void)testNSCoding { + PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]; + + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:geoPoint]; + XCTAssertTrue([data length] > 0, @"Encoded data should not be empty"); + + PFGeoPoint *decodedGeoPoint = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + XCTAssertEqual(decodedGeoPoint.latitude, geoPoint.latitude, @"Latitude should be the same."); + XCTAssertEqual(decodedGeoPoint.longitude, geoPoint.longitude, @"Longitude should be the same."); +} + +- (void)testGeoPointDescription { + PFGeoPoint *point = [PFGeoPoint geoPoint]; + XCTAssertNotNil([point description]); + + point = [PFGeoPoint geoPointWithLatitude:10 longitude:20]; + NSString *description = [point description]; + XCTAssertNotNil(description); + + point.latitude = 20; + XCTAssertNotNil([point description]); + XCTAssertNotEqualObjects(description, [point description]); +} + +- (void)testGeoPointForCurrentLocation { + // Make sure we don't crash on nil block + [PFGeoPoint geoPointForCurrentLocationInBackground:nil]; +} + +@end diff --git a/Tests/Unit/HashTests.m b/Tests/Unit/HashTests.m new file mode 100644 index 000000000..cd12fb015 --- /dev/null +++ b/Tests/Unit/HashTests.m @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestCase.h" + +#import "PFHash.h" + +@interface HashTests : PFTestCase + +@end + +@implementation HashTests + +- (void)testMD5SimpleHash { + XCTAssertEqualObjects(@"5eb63bbbe01eeed093cb22bb8f5acdc3", PFMD5HashFromString(@"hello world")); + XCTAssertEqualObjects(@"5eb63bbbe01eeed093cb22bb8f5acdc3", + PFMD5HashFromData([@"hello world" dataUsingEncoding:NSUTF8StringEncoding])); +} + +- (void)testMD5HashFromUnicode { + XCTAssertEqualObjects(@"9c853e20bb12ff256734a992dd224f17", PFMD5HashFromString(@"foo א")); + XCTAssertEqualObjects(@"9c853e20bb12ff256734a992dd224f17", + PFMD5HashFromData([@"foo א" dataUsingEncoding:NSUTF8StringEncoding])); + + XCTAssertEqualObjects(@"9c853e20bb12ff256734a992dd224f17", PFMD5HashFromString(@"foo \327\220")); + XCTAssertEqualObjects(@"9c853e20bb12ff256734a992dd224f17", + PFMD5HashFromData([@"foo \327\220" dataUsingEncoding:NSUTF8StringEncoding])); +} + +@end diff --git a/Tests/Unit/IncrementUnitTests.m b/Tests/Unit/IncrementUnitTests.m new file mode 100644 index 000000000..d6279e8c7 --- /dev/null +++ b/Tests/Unit/IncrementUnitTests.m @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObject.h" +#import "PFTestCase.h" + +@interface IncrementUnitTests : PFTestCase + +@end + +@implementation IncrementUnitTests + +- (void)testIncrementWorksOnlyOnNumbers { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + object[@"score"] = @"foo"; + + PFAssertThrowsInvalidArgumentException([object incrementKey:@"score"]); + PFAssertThrowsInvalidArgumentException([object incrementKey:@"score" byAmount:@1]); +} + +@end diff --git a/Tests/Unit/InstallationIdentifierUnitTests.m b/Tests/Unit/InstallationIdentifierUnitTests.m new file mode 100644 index 000000000..f0726c5d6 --- /dev/null +++ b/Tests/Unit/InstallationIdentifierUnitTests.m @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInstallationIdentifierStore_Private.h" +#import "PFTestCase.h" +#import "Parse_Private.h" + +@interface InstallationIdentifierUnitTests : PFTestCase + +@end + +@implementation InstallationIdentifierUnitTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)tearDown { + [[Parse _currentManager] clearEventuallyQueue]; + [[Parse _currentManager].installationIdentifierStore clearInstallationIdentifier]; + [Parse _clearCurrentManager]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testNewInstallationIdentifierIsLowercase { + [Parse setApplicationId:@"b" clientKey:@"c"]; + PFInstallationIdentifierStore *store = [Parse _currentManager].installationIdentifierStore; + NSString *installationId = store.installationIdentifier; + XCTAssertEqualObjects(installationId, [installationId lowercaseString]); +} + +- (void)testCachedInstallationId { + [Parse setApplicationId:@"b" clientKey:@"c"]; + PFInstallationIdentifierStore *store = [Parse _currentManager].installationIdentifierStore; + + [store _clearCachedInstallationIdentifier]; + NSString *first = [store.installationIdentifier copy]; + NSString *second = [store.installationIdentifier copy]; + XCTAssertEqualObjects(first, second, @"installationId should be the same on different calls"); + [store _clearCachedInstallationIdentifier]; + NSString *third = [store.installationIdentifier copy]; + XCTAssertEqualObjects(first, third, @"installationId should be the same after clearing cache"); + [store clearInstallationIdentifier]; + NSString *fourth = store.installationIdentifier; + XCTAssertNotEqualObjects(first, fourth, @"clearing from disk should cause a new installationId"); +} + +@end diff --git a/Tests/Unit/InstallationUnitTests.m b/Tests/Unit/InstallationUnitTests.m new file mode 100644 index 000000000..8e29a9b2e --- /dev/null +++ b/Tests/Unit/InstallationUnitTests.m @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFInstallation.h" +#import "PFTestCase.h" +#import "Parse.h" + +@interface InstallationUnitTests : PFTestCase + +@end + +@implementation InstallationUnitTests + ++ (void)setUp { + [super setUp]; + + [Parse setApplicationId:@"a" clientKey:@"a"]; +} + +- (void)testInstallationImmutableFieldsCannotBeChanged { + PFInstallation *installation = [PFInstallation currentInstallation]; + installation.deviceToken = @"11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306"; + + PFAssertThrowsInvalidArgumentException(installation[@"deviceType"] = @"android", + @"Should throw an exception for trying to change deviceType."); + PFAssertThrowsInvalidArgumentException(installation[@"installationId"] = @"a" + @"Should throw an exception for trying to change installationId."); +} + +- (void)testInstallationImmutableFieldsCannotBeDeleted { + PFInstallation *installation = [PFInstallation currentInstallation]; + installation.deviceToken = @"11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306"; + + PFAssertThrowsInvalidArgumentException([installation removeObjectForKey:@"deviceType"], + @"Should throw an exception for trying to delete deviceType."); + PFAssertThrowsInvalidArgumentException([installation removeObjectForKey:@"installationId"], + @"Should throw an exception for trying to delete installationId."); +} + +@end diff --git a/Tests/Unit/KeyValueCacheTests.m b/Tests/Unit/KeyValueCacheTests.m new file mode 100644 index 000000000..ff4197f83 --- /dev/null +++ b/Tests/Unit/KeyValueCacheTests.m @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFKeyValueCache_Private.h" +#import "PFMacros.h" +#import "PFTestCase.h" +#import "TestCache.h" +#import "TestFileManager.h" + +@interface KeyValueCacheTests : PFTestCase +@end + +@implementation KeyValueCacheTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (NSString *)sampleDirectoryPath { + return [NSTemporaryDirectory() stringByAppendingPathComponent:@"KeyValueCacheDir"]; +} + +- (NSURL *)sampleDirectoryURL { + return [NSURL URLWithString:[self sampleDirectoryPath]]; +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + PFKeyValueCache *cache = [[PFKeyValueCache alloc] initWithCacheDirectoryPath:[self sampleDirectoryPath]]; + XCTAssertNotNil(cache); + XCTAssertEqualObjects(cache.fileManager, [NSFileManager defaultManager]); + XCTAssertNotNil(cache.memoryCache); + XCTAssertEqualObjects(cache.cacheDirectoryPath, [self sampleDirectoryPath]); + XCTAssertEqual(cache.maxDiskCacheBytes, 1024 * 1024 * 10); + XCTAssertEqual(cache.maxDiskCacheRecords, 1000); + XCTAssertEqual(cache.maxMemoryCacheBytesPerRecord, 1024 * 1024); + + id mockedFileManager = [TestFileManager fileManager]; + OCMExpect([mockedFileManager createDirectoryAtURL:OCMOCK_ANY withIntermediateDirectories:YES attributes:nil error:NULL]); + + NSCache *mockedCache = [TestCache cache]; + cache = [[PFKeyValueCache alloc] initWithCacheDirectoryURL:[self sampleDirectoryURL] + fileManager:mockedFileManager + memoryCache:mockedCache]; + + XCTAssertNotNil(cache); + XCTAssertEqualObjects(mockedFileManager, cache.fileManager); + XCTAssertEqualObjects(mockedCache, cache.memoryCache); + XCTAssertEqualObjects(cache.cacheDirectoryPath, [self sampleDirectoryPath]); + XCTAssertEqual(cache.maxDiskCacheBytes, 1024 * 1024 * 10); + XCTAssertEqual(cache.maxDiskCacheRecords, 1000); + XCTAssertEqual(cache.maxMemoryCacheBytesPerRecord, 1024 * 1024); +} + +- (void)testBasicMemoryCaching { + PFKeyValueCache *cache = [[PFKeyValueCache alloc] initWithCacheDirectoryURL:nil + fileManager:nil + memoryCache:[TestCache cache]]; + [cache setObject:@"value" forKey:@"key1"]; + [cache setObject:@"value" forKey:@"key2"]; + + XCTAssertEqualObjects([cache objectForKey:@"key1" maxAge:INFINITY], @"value"); + XCTAssertEqualObjects([cache objectForKey:@"key2" maxAge:INFINITY], @"value"); + + [cache removeObjectForKey:@"key1"]; + + XCTAssertEqualObjects([cache objectForKey:@"key1" maxAge:INFINITY], nil); + XCTAssertEqualObjects([cache objectForKey:@"key2" maxAge:INFINITY], @"value"); +} + +- (void)testBasicDiskCaching { + NSFileManager *mockedFileManager = [TestFileManager fileManager]; + PFKeyValueCache *cache = [[PFKeyValueCache alloc] initWithCacheDirectoryURL:[self sampleDirectoryURL] + fileManager:mockedFileManager + memoryCache:nil]; + + [cache setObject:@"value" forKey:@"key1"]; + [cache setObject:@"value" forKey:@"key2"]; + + XCTAssertEqualObjects([cache objectForKey:@"key1" maxAge:INFINITY], @"value"); + XCTAssertEqualObjects([cache objectForKey:@"key2" maxAge:INFINITY], @"value"); + + [cache removeObjectForKey:@"key1"]; + + XCTAssertEqualObjects([cache objectForKey:@"key1" maxAge:INFINITY], nil); + XCTAssertEqualObjects([cache objectForKey:@"key2" maxAge:INFINITY], @"value"); + + [cache removeAllObjects]; +} + +- (void)testMemoryCacheOldEntryEviction { + PFKeyValueCache *cache = [[PFKeyValueCache alloc] initWithCacheDirectoryURL:nil + fileManager:nil + memoryCache:[TestCache cache]]; + + [cache setObject:@"value" forKey:@"key1"]; + XCTAssertNil([cache objectForKey:@"key1" maxAge:0]); + + [cache waitForOutstandingOperations]; +} + +- (void)testDiskCacheOldEntryEviction { + NSFileManager *mockedFileManager = [TestFileManager fileManager]; + PFKeyValueCache *cache = [[PFKeyValueCache alloc] initWithCacheDirectoryURL:[self sampleDirectoryURL] + fileManager:mockedFileManager + memoryCache:nil]; + [cache setObject:@"value" forKey:@"key1"]; + XCTAssertNil([cache objectForKey:@"key1" maxAge:0]); + + [cache waitForOutstandingOperations]; +} + +- (void)testMaxFileCompaction { + NSCache *mockedCache = [TestCache cache]; + NSFileManager *mockedFileManager = [TestFileManager fileManager]; + PFKeyValueCache *cache = [[PFKeyValueCache alloc] initWithCacheDirectoryURL:[self sampleDirectoryURL] + fileManager:mockedFileManager + memoryCache:mockedCache]; + cache.maxDiskCacheRecords = 1; + + [cache setObject:@"value" forKey:@"key1"]; + [cache setObject:@"value" forKey:@"key2"]; + + [cache waitForOutstandingOperations]; + + XCTAssertNotNil([cache objectForKey:@"key1" maxAge:INFINITY]); + XCTAssertNotNil([cache objectForKey:@"key2" maxAge:INFINITY]); + + [mockedCache removeAllObjects]; + + XCTAssertNil([cache objectForKey:@"key1" maxAge:INFINITY]); + XCTAssertNotNil([cache objectForKey:@"key2" maxAge:INFINITY]); + + [cache waitForOutstandingOperations]; +} + +- (void)testMaxSizeCompaction { + NSCache *mockedCache = [TestCache cache]; + NSFileManager *mockedFileManager = [TestFileManager fileManager]; + PFKeyValueCache *cache = [[PFKeyValueCache alloc] initWithCacheDirectoryURL:[self sampleDirectoryURL] + fileManager:mockedFileManager + memoryCache:mockedCache]; + cache.maxDiskCacheBytes = 5; + + [cache setObject:@"value" forKey:@"key1"]; + [cache setObject:@"value" forKey:@"key2"]; + + XCTAssertNotNil([cache objectForKey:@"key1" maxAge:INFINITY]); + XCTAssertNotNil([cache objectForKey:@"key2" maxAge:INFINITY]); + + [mockedCache removeAllObjects]; + + XCTAssertNil([cache objectForKey:@"key1" maxAge:INFINITY]); + XCTAssertNotNil([cache objectForKey:@"key2" maxAge:INFINITY]); + + [cache waitForOutstandingOperations]; +} +@end diff --git a/Tests/Unit/KeychainStoreTests.m b/Tests/Unit/KeychainStoreTests.m new file mode 100644 index 000000000..1c8dec980 --- /dev/null +++ b/Tests/Unit/KeychainStoreTests.m @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFKeychainStore.h" +#import "PFTestCase.h" + +@interface KeychainStoreTests : PFTestCase + +@property (nonatomic, strong) PFKeychainStore *testStore; + +@end + +@implementation KeychainStoreTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + self.testStore = [[PFKeychainStore alloc] initWithService:@"test"]; +} + +- (void)tearDown { + [self.testStore removeAllObjects]; + self.testStore = nil; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testSetObject { + BOOL result = [self.testStore setObject:@"yarr" forKey:@"blah"]; + XCTAssertTrue(result, @"Set should succeed"); +} + +- (void)testSetObjectSubscript { + BOOL result = [self.testStore setObject:@"yarrValue" forKeyedSubscript:@"yarrKey1"]; + XCTAssertTrue(result, @"Set should succeed"); +} + +- (void)testGetObject { + NSString *key = @"yarrKey"; + NSString *value = @"yarrValue"; + self.testStore[key] = value; + + NSString *retrievedValue = [self.testStore objectForKey:key]; + XCTAssertEqualObjects(value, retrievedValue, @"Values should be equal after get"); +} + +- (void)testGetObjectSubscript { + NSString *key = @"yarrKey"; + NSString *value = @"yarrValue"; + self.testStore[key] = value; + + NSString *retrievedValue = self.testStore[key]; + XCTAssertEqualObjects(value, retrievedValue, @"Values should be equal after get"); +} + +- (void)testSetGetComplexObject { + NSArray *complexObject = @[ @{ @"key1" : @"value1"}, @"string2", @100500, [NSNull null] ]; + + self.testStore[@"complexObject"] = complexObject; + + NSArray *retrievedComplexObject = self.testStore[@"complexObject"]; + XCTAssertTrue([retrievedComplexObject isKindOfClass:[NSArray class]], @"Complex object should properly retrieve"); + + for (NSUInteger i = 0; i < [retrievedComplexObject count]; i++) { + id object = [complexObject objectAtIndex:i]; + id retrievedObject = [retrievedComplexObject objectAtIndex:i]; + + XCTAssertTrue([object isEqual:retrievedObject], + @"Keychain store should properly retrieve objects of class - %@", [object class]); + + switch (i) { + case 0: + { + NSDictionary *dictionary = object; + NSDictionary *retrievedDictionary = retrievedObject; + + XCTAssertEqualObjects(dictionary[@"key1"], retrievedDictionary[@"key1"], + @"Keychain store should properly retrieve dictionary values"); + } + break; + case 1: + { + XCTAssertEqualObjects(object, retrievedObject, + @"Keychain store should properly retrieve NSString objects"); + } + break; + case 2: + { + XCTAssertEqualObjects(object, retrievedObject, + @"Keychain store should properly retrieve NSNumber objects"); + } + break; + case 3: + { + XCTAssertEqual(retrievedObject, [NSNull null], @"Keychain store should properly retrieve NSNull"); + } + break; + } + } + + [self.testStore removeAllObjects]; +} + +- (void)testRemoveObject { + self.testStore[@"key1"] = @"value1"; + XCTAssertNotNil(self.testStore[@"key1"], @"There should be no value after removal"); + + [self.testStore removeObjectForKey:@"key1"]; + XCTAssertNil(self.testStore[@"key1"], @"There should be no value after removal"); +} + +- (void)testRemoveObjectSubscript { + self.testStore[@"key1"] = @"value1"; + XCTAssertNotNil(self.testStore[@"key1"], @"There should be no value after removal"); + + self.testStore[@"key1"] = nil; + XCTAssertNil(self.testStore[@"key1"], @"There should be no value after removal"); +} + +- (void)testRemoveAllObjects { + self.testStore[@"key1"] = @"value1"; + self.testStore[@"key2"] = @"value2"; + XCTAssertNotNil(self.testStore[@"key1"], @"Value should be saved"); + XCTAssertNotNil(self.testStore[@"key2"], @"Value should be saved"); + + [self.testStore removeAllObjects]; + + XCTAssertNil(self.testStore[@"key1"], @"There should be no value after remove all"); + XCTAssertNil(self.testStore[@"key2"], @"There should be no value after remove all"); +} + +- (void)testThreadSafeSetObject { + dispatch_apply(100, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i) { + XCTAssertTrue([self.testStore setObject:@"yarr" forKey:@"pirate"]); + }); +} + +- (void)testThreadSafeRemoveObject { + dispatch_apply(100, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i) { + XCTAssertTrue([self.testStore setObject:@"yarr" forKey:[@(i) stringValue]]); + XCTAssertTrue([self.testStore removeObjectForKey:[@(i) stringValue]]); + }); +} + +- (void)testThreadSafeRemoveAllObjects { + dispatch_apply(100, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i) { + XCTAssertTrue([self.testStore setObject:@"yarr" forKey:@"pirate1"]); + XCTAssertTrue([self.testStore setObject:@"yarr" forKey:@"pirate2"]); + XCTAssertTrue([self.testStore removeAllObjects]); + }); +} + +@end diff --git a/Tests/Unit/LocationManagerMobileTests.m b/Tests/Unit/LocationManagerMobileTests.m new file mode 100644 index 000000000..221708238 --- /dev/null +++ b/Tests/Unit/LocationManagerMobileTests.m @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import + +#import "PFLocationManager.h" +#import "PFUnitTestCase.h" + +/*! + We do this because OCMock does not allow you to stub -respondsToSelector:, so we force it to bend to our will using a + protocol mock. + + TODO: (richardross) Update this to use a traditional mock once OCMock supports it. + */ +@protocol PFTestCLLocationManagerInterfaceWithoutAlwaysAuth + +@property (assign, nonatomic) id delegate; + +- (void)startUpdatingLocation; +- (void)stopUpdatingLocation; + +@end + +@protocol PFTestCLLocationManagerInterfaceWithAlwaysAuth + +- (void)requestWhenInUseAuthorization; +- (void)requestAlwaysAuthorization; + +@end + +@interface LocationManagerMobileTests : PFUnitTestCase + +@end + +@implementation LocationManagerMobileTests + +- (void)testAddBlockWithForegroundAuthorization { + CLLocation *expectedLocation = [[CLLocation alloc] initWithLatitude:13.37 longitude:1337]; + + id mockedApplication = PFStrictClassMock([UIApplication class]); + id mockedBundle = PFStrictClassMock([NSBundle class]); + id mockedSystemLocationManager = PFStrictProtocolMock(@protocol(PFTestCLLocationManagerInterfaceWithAlwaysAuth)); + + __block __weak id delegate = nil; + + OCMStub([mockedSystemLocationManager setDelegate:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id argument = nil; + [invocation getArgument:&argument atIndex:2]; + + delegate = argument; + }); + + OCMExpect([mockedSystemLocationManager startUpdatingLocation]).andDo(^(NSInvocation *invoke) { + [delegate locationManager:mockedSystemLocationManager didUpdateLocations:@[ expectedLocation ]]; + }); + + OCMExpect([mockedSystemLocationManager stopUpdatingLocation]); + OCMExpect([mockedSystemLocationManager requestWhenInUseAuthorization]); + + OCMStub([mockedApplication applicationState]).andReturn(UIApplicationStateActive); + OCMStub([mockedBundle objectForInfoDictionaryKey:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqualToString:@"NSLocationWhenInUseUsageDescription"]; + }]]).andReturn(@"foreground"); + + PFLocationManager *locationManager = [[PFLocationManager alloc] initWithSystemLocationManager:mockedSystemLocationManager + application:mockedApplication + bundle:mockedBundle]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [locationManager addBlockForCurrentLocation:^(CLLocation *location, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(location.coordinate.latitude, 13.37); + XCTAssertEqual(location.coordinate.longitude, 1337); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; + + OCMVerifyAll(mockedSystemLocationManager); +} + +- (void)testAddBlockWithBackgroundAuthorization { + CLLocation *expectedLocation = [[CLLocation alloc] initWithLatitude:13.37 longitude:1337]; + + id mockedApplication = PFStrictClassMock([UIApplication class]); + id mockedBundle = PFStrictClassMock([NSBundle class]); + id mockedSystemLocationManager = PFStrictProtocolMock(@protocol(PFTestCLLocationManagerInterfaceWithAlwaysAuth)); + + __block __weak id delegate = nil; + + OCMStub([mockedSystemLocationManager setDelegate:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id argument = nil; + [invocation getArgument:&argument atIndex:2]; + + delegate = argument; + }); + + OCMExpect([mockedSystemLocationManager startUpdatingLocation]).andDo(^(NSInvocation *invoke) { + [delegate locationManager:mockedSystemLocationManager didUpdateLocations:@[ expectedLocation ]]; + }); + + OCMExpect([mockedSystemLocationManager stopUpdatingLocation]); + OCMExpect([mockedSystemLocationManager requestAlwaysAuthorization]); + + OCMStub([mockedApplication applicationState]).andReturn(UIApplicationStateActive); + OCMStub([mockedBundle objectForInfoDictionaryKey:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqualToString:@"NSLocationWhenInUseUsageDescription"]; + }]]).andReturn(nil); + + PFLocationManager *locationManager = [[PFLocationManager alloc] initWithSystemLocationManager:mockedSystemLocationManager + application:mockedApplication + bundle:mockedBundle]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [locationManager addBlockForCurrentLocation:^(CLLocation *location, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(location.coordinate.latitude, 13.37); + XCTAssertEqual(location.coordinate.longitude, 1337); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; + + OCMVerifyAll(mockedSystemLocationManager); +} + +@end diff --git a/Tests/Unit/LocationManagerTests.m b/Tests/Unit/LocationManagerTests.m new file mode 100644 index 000000000..042d9c821 --- /dev/null +++ b/Tests/Unit/LocationManagerTests.m @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFLocationManager.h" +#import "PFTestCase.h" +#import "PFTestSwizzlingUtilities.h" + +@protocol PFTestCLLocationManager + +@property (assign, nonatomic) id delegate; + +- (void)startUpdatingLocation; +- (void)stopUpdatingLocation; + +@end + +@interface LocationManagerTests : PFTestCase +@end + +@implementation LocationManagerTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + PFLocationManager *locationManager = [[PFLocationManager alloc] init]; + XCTAssertNotNil(locationManager); + + id mockedSystemLocationManager = PFStrictClassMock([CLLocationManager class]); + OCMStub([mockedSystemLocationManager setDelegate:OCMOCK_ANY]); + locationManager = [[PFLocationManager alloc] initWithSystemLocationManager:mockedSystemLocationManager]; + + XCTAssertNotNil(locationManager); + +#if TARGET_OS_IPHONE + + id mockedBundle = PFStrictClassMock([NSBundle class]); + id mockedApplication = PFStrictClassMock([UIApplication class]); + + locationManager = [[PFLocationManager alloc] initWithSystemLocationManager:mockedSystemLocationManager + application:mockedApplication + bundle:mockedBundle]; + + XCTAssertNotNil(locationManager); + +#endif +} + +- (void)testAddBlockWithoutAnyAutorization { + CLLocation *expectedLocation = [[CLLocation alloc] initWithLatitude:13.37 longitude:1337]; + + id mockedSystemLocationManager = PFStrictProtocolMock(@protocol(PFTestCLLocationManager)); + __block __weak id delegate = nil; + + OCMStub([mockedSystemLocationManager setDelegate:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id argument = nil; + [invocation getArgument:&argument atIndex:2]; + + delegate = argument; + }); + + OCMExpect([mockedSystemLocationManager startUpdatingLocation]).andDo(^(NSInvocation *invoke) { + [delegate locationManager:mockedSystemLocationManager didUpdateLocations:@[ expectedLocation ]]; + }); + + OCMExpect([mockedSystemLocationManager stopUpdatingLocation]); + + PFLocationManager *locationManager = [[PFLocationManager alloc] initWithSystemLocationManager:mockedSystemLocationManager]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [locationManager addBlockForCurrentLocation:^(CLLocation *location, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(location.coordinate.latitude, 13.37); + XCTAssertEqual(location.coordinate.longitude, 1337); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; + + OCMVerifyAll(mockedSystemLocationManager); +} + +- (void)testFailWithError { + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain code:13337 userInfo:nil]; + id mockedSystemLocationManager = PFStrictProtocolMock(@protocol(PFTestCLLocationManager)); + __block __weak id delegate = nil; + + OCMStub([mockedSystemLocationManager setDelegate:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id argument = nil; + [invocation getArgument:&argument atIndex:2]; + + delegate = argument; + }); + + OCMExpect([mockedSystemLocationManager startUpdatingLocation]).andDo(^(NSInvocation *invoke) { + [delegate locationManager:mockedSystemLocationManager didFailWithError:expectedError]; + }); + + OCMExpect([mockedSystemLocationManager stopUpdatingLocation]); + + PFLocationManager *locationManager = [[PFLocationManager alloc] initWithSystemLocationManager:mockedSystemLocationManager]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [locationManager addBlockForCurrentLocation:^(CLLocation *location, NSError *error) { + XCTAssertEqualObjects(error, expectedError); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; + + OCMVerifyAll(mockedSystemLocationManager); +} + +@end diff --git a/Tests/Unit/ObjectBatchCommandTests.m b/Tests/Unit/ObjectBatchCommandTests.m new file mode 100644 index 000000000..97f0f083c --- /dev/null +++ b/Tests/Unit/ObjectBatchCommandTests.m @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFObjectState.h" +#import "PFRESTObjectBatchCommand.h" +#import "PFRESTObjectCommand.h" +#import "PFTestCase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ObjectBatchCommandTests : PFTestCase + +@end + +@implementation ObjectBatchCommandTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (NSArray *)sampleObjectCommands { + NSMutableArray *array = [NSMutableArray arrayWithCapacity:25]; + while (array.count < 25) { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"a" objectId:nil isComplete:NO]; + PFRESTCommand *createCommand = [PFRESTObjectCommand createObjectCommandForObjectState:state + changes:@{ @"k" : @"v" } + operationSetUUID:nil + sessionToken:nil]; + [array addObject:createCommand]; + + state = [PFObjectState stateWithParseClassName:@"Capitan" objectId:@"yolo" isComplete:NO]; + PFRESTCommand *updateCommand = [PFRESTObjectCommand updateObjectCommandForObjectState:state + changes:@{ @"k1" : @"v" } + operationSetUUID:@"asd" + sessionToken:nil]; + [array addObject:updateCommand]; + + state = [PFObjectState stateWithParseClassName:@"Capitan" objectId:@"blah" isComplete:NO]; + PFRESTCommand *deleteCommand = [PFRESTObjectCommand deleteObjectCommandForObjectState:state + withSessionToken:nil]; + [array addObject:deleteCommand]; + } + return array; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testBatchCommand { + NSArray *commands = [self sampleObjectCommands]; + PFRESTObjectBatchCommand *command = [PFRESTObjectBatchCommand batchCommandWithCommands:commands + sessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"batch"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertEqual([command.parameters[@"requests"] count], commands.count); + XCTAssertNotNil([command.parameters[@"requests"] firstObject][@"method"]); + XCTAssertNotNil([command.parameters[@"requests"] firstObject][@"path"]); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +- (void)testBatchCommandValidation { + XCTAssertNotEqual(PFRESTObjectBatchCommandSubcommandsLimit, 0); + + NSMutableArray *array = [[self sampleObjectCommands] mutableCopy]; + while (array.count < PFRESTObjectBatchCommandSubcommandsLimit) { + [array addObjectsFromArray:[self sampleObjectCommands]]; + } + + PFAssertThrowsInvalidArgumentException([PFRESTObjectBatchCommand batchCommandWithCommands:array sessionToken:@"a"]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/Unit/ObjectBatchControllerTests.m b/Tests/Unit/ObjectBatchControllerTests.m new file mode 100644 index 000000000..a862c9c64 --- /dev/null +++ b/Tests/Unit/ObjectBatchControllerTests.m @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "BFTask+Private.h" +#import "OCMock+Parse.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFHTTPRequest.h" +#import "PFObject.h" +#import "PFObjectBatchController.h" +#import "PFRESTCommand.h" +#import "PFUnitTestCase.h" + +@interface ObjectBatchControllerTests : PFUnitTestCase + +@end + +@implementation ObjectBatchControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFCommandRunnerProvider)); + id runner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + OCMStub(dataSource.commandRunner).andReturn(runner); + return dataSource; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id dataSource = [self mockedDataSource]; + + PFObjectBatchController *controller = [[PFObjectBatchController alloc] initWithDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, dataSource); + + controller = [PFObjectBatchController controllerWithDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, dataSource); +} + +- (void)testFetchAll { + id dataSource = [self mockedDataSource]; + id commandRunner = dataSource.commandRunner; + + NSDictionary *result = @{ @"results" : @[ @{@"objectId" : @"abc", @"a" : @"b"} ] }; + [commandRunner mockCommandResult:result forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertEqualObjects(command.parameters, (@{@"where" : @{ @"objectId" : @{ @"$in": @[ @"abc" ] } }, + @"limit" : @"1" })); + XCTAssertNil(command.sessionToken); + return YES; + }]; + + PFObjectBatchController *controller = [[PFObjectBatchController alloc] initWithDataSource:dataSource]; + + PFObject *object = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:@"abc"]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller fetchObjectsAsync:@[ object ] withSessionToken:nil] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertFalse(task.cancelled); + XCTAssertFalse(task.faulted); + XCTAssertNil(task.result); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + XCTAssertEqualObjects(object[@"a"], @"b"); + XCTAssertTrue([object isDataAvailable]); + OCMVerifyAll(commandRunner); +} + +- (void)testFetchAllWithoutObjects { + id dataSource = [self mockedDataSource]; + PFObjectBatchController *controller = [PFObjectBatchController controllerWithDataSource:dataSource]; + + XCTAssertEqualObjects([[controller fetchObjectsAsync:@[] withSessionToken:nil] waitForResult:nil], @[]); + XCTAssertNil([[controller fetchObjectsAsync:nil withSessionToken:nil] waitForResult:nil]); +} + +- (void)testFetchAllWithMissingObjects { + id dataSource = [self mockedDataSource]; + id commandRunner = dataSource.commandRunner; + + NSDictionary *result = @{ @"results" : @[ @{@"objectId" : @"abc", @"a" : @"b"} ] }; + [commandRunner mockCommandResult:result forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertEqualObjects(command.parameters, (@{@"where" : @{ @"objectId" : @{ @"$in": @[ @"abc", @"def" ] } }, + @"limit" : @"2" })); + XCTAssertNil(command.sessionToken); + return YES; + }]; + + PFObjectBatchController *controller = [[PFObjectBatchController alloc] initWithDataSource:dataSource]; + + PFObject *object = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:@"abc"]; + PFObject *missingObject = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:@"def"]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller fetchObjectsAsync:@[ object, missingObject ] + withSessionToken:nil] continueWithBlock:^id(BFTask *task) { + NSError *error = task.error; + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, kPFErrorObjectNotFound); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + XCTAssertEqualObjects(object[@"a"], @"b"); + XCTAssertTrue([object isDataAvailable]); + OCMVerifyAll(commandRunner); +} + +- (void)testUniqueObjects { + PFObject *object = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:@"123"]; + PFObject *fetchedObject = [PFObject objectWithClassName:@"Yarr"]; + fetchedObject.objectId = @"yarr"; + + NSArray *array = [PFObjectBatchController uniqueObjectsArrayFromArray:@[ object, object, fetchedObject] + omitObjectsWithData:NO]; + XCTAssertEqual(array.count, 2); + XCTAssertTrue([array containsObject:object]); + XCTAssertTrue([array containsObject:fetchedObject]); +} + +- (void)testUniqueObjectsOmittingFetched { + PFObject *object = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:@"yolo"]; + PFObject *fetchedObject = [PFObject objectWithClassName:@"Yarr"]; + fetchedObject.objectId = @"yarr"; + + NSArray *array = [PFObjectBatchController uniqueObjectsArrayFromArray:@[ object, object, fetchedObject] + omitObjectsWithData:YES]; + XCTAssertEqualObjects(array, @[ object ]); +} + +- (void)testUniqueObjectsWithoutObjects { + XCTAssertNil([PFObjectBatchController uniqueObjectsArrayFromArray:nil omitObjectsWithData:NO]); + XCTAssertNil([PFObjectBatchController uniqueObjectsArrayFromArray:nil omitObjectsWithData:YES]); + XCTAssertEqualObjects([PFObjectBatchController uniqueObjectsArrayFromArray:@[] omitObjectsWithData:NO], @[]); + XCTAssertEqualObjects([PFObjectBatchController uniqueObjectsArrayFromArray:@[] omitObjectsWithData:YES], @[]); +} + +- (void)testUniqueObjectsValidation { + PFObject *object = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:nil]; + PFAssertThrowsInvalidArgumentException([PFObjectBatchController uniqueObjectsArrayFromArray:@[ object ] + omitObjectsWithData:NO]); + PFAssertThrowsInvalidArgumentException([PFObjectBatchController uniqueObjectsArrayFromArray:@[ object ] + omitObjectsWithData:YES]); + + object = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:@"123"]; + PFObject *object2 = [PFObject objectWithoutDataWithClassName:@"Yolo" objectId:@"321"]; + PFAssertThrowsInvalidArgumentException(([PFObjectBatchController uniqueObjectsArrayFromArray:@[ object, object2 ] + omitObjectsWithData:NO])); + PFAssertThrowsInvalidArgumentException(([PFObjectBatchController uniqueObjectsArrayFromArray:@[ object, object2 ] + omitObjectsWithData:YES])); +} + +@end diff --git a/Tests/Unit/ObjectCommandTests.m b/Tests/Unit/ObjectCommandTests.m new file mode 100644 index 000000000..43b0dedc8 --- /dev/null +++ b/Tests/Unit/ObjectCommandTests.m @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFObjectState.h" +#import "PFRESTObjectCommand.h" +#import "PFTestCase.h" + +@interface ObjectCommandTests : PFTestCase + +@end + +@implementation ObjectCommandTests + +- (void)testGetObjectCommand { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"Yolo" objectId:@"yarr" isComplete:NO]; + PFRESTObjectCommand *command = [PFRESTObjectCommand fetchObjectCommandForObjectState:state + withSessionToken:@"Capitan"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"classes/Yolo/yarr"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.sessionToken, @"Capitan"); +} + +- (void)testGetObjectCommandValidation { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"a" objectId:@"" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand fetchObjectCommandForObjectState:state + withSessionToken:@"yolo"]); + + state = [PFObjectState stateWithParseClassName:@"" objectId:@"a" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand fetchObjectCommandForObjectState:state + withSessionToken:@"yolo"]); + + state = [PFObjectState stateWithParseClassName:@"" objectId:@"" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand fetchObjectCommandForObjectState:state + withSessionToken:@"yolo"]); +} + +- (void)testCreateObjectCommand { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"Abc" objectId:nil isComplete:NO]; + PFRESTObjectCommand *command = [PFRESTObjectCommand createObjectCommandForObjectState:state + changes:@{ @"key" : @"value" } + operationSetUUID:@"uuid" + sessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"classes/Abc"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertEqualObjects(command.parameters[@"key"], @"value"); + XCTAssertEqualObjects(command.operationSetUUID, @"uuid"); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); + + command = [PFRESTObjectCommand createObjectCommandForObjectState:state + changes:nil + operationSetUUID:@"uuid" + sessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"classes/Abc"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.operationSetUUID, @"uuid"); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +- (void)testCreateObjectCommandValidation { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"" objectId:nil isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand createObjectCommandForObjectState:state + changes:@{} + operationSetUUID:@"a" + sessionToken:@"b"]); +} + +- (void)testUpdateObjectCommand { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"Abc" objectId:@"d" isComplete:NO]; + PFRESTObjectCommand *command = [PFRESTObjectCommand updateObjectCommandForObjectState:state + changes:@{ @"key" : @"value" } + operationSetUUID:@"uuid" + sessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"classes/Abc/d"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPUT); + XCTAssertEqualObjects(command.parameters[@"key"], @"value"); + XCTAssertEqualObjects(command.operationSetUUID, @"uuid"); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +- (void)testUpdateObjectCommandValidation { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"" objectId:@"d" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand updateObjectCommandForObjectState:state + changes:@{} + operationSetUUID:@"b" + sessionToken:@"c"]); + + state = [PFObjectState stateWithParseClassName:@"Abc" objectId:@"" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand updateObjectCommandForObjectState:state + changes:@{} + operationSetUUID:@"b" + sessionToken:@"c"]); + + state = [PFObjectState stateWithParseClassName:@"" objectId:@"" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand updateObjectCommandForObjectState:state + changes:@{} + operationSetUUID:@"b" + sessionToken:@"c"]); +} + +- (void)testDeleteObjectCommand { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"Abc" objectId:@"yarr" isComplete:NO]; + PFRESTObjectCommand *command = [PFRESTObjectCommand deleteObjectCommandForObjectState:state + withSessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"classes/Abc/yarr"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodDELETE); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +- (void)testDeleteObjectCommandValidation { + PFObjectState *state = [PFObjectState stateWithParseClassName:@"" objectId:@"yarr" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand deleteObjectCommandForObjectState:state + withSessionToken:@"yolo"]); + + state = [PFObjectState stateWithParseClassName:@"" objectId:@"" isComplete:NO]; + PFAssertThrowsInvalidArgumentException([PFRESTObjectCommand deleteObjectCommandForObjectState:state + withSessionToken:@"yolo"]); +} + +@end diff --git a/Tests/Unit/ObjectEstimatedDataTests.m b/Tests/Unit/ObjectEstimatedDataTests.m new file mode 100644 index 000000000..02cb02f15 --- /dev/null +++ b/Tests/Unit/ObjectEstimatedDataTests.m @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFieldOperation.h" +#import "PFObjectEstimatedData.h" +#import "PFOperationSet.h" +#import "PFTestCase.h" + +@interface ObjectEstimatedDataTests : PFTestCase + +@end + +@implementation ObjectEstimatedDataTests + +- (void)testConstructors { + PFObjectEstimatedData *data = [[PFObjectEstimatedData alloc] init]; + XCTAssertNotNil(data); + XCTAssertNotNil([data allKeys]); + XCTAssertEqualObjects([data allKeys], @[]); + + data = [[PFObjectEstimatedData alloc] initWithServerData:nil operationSetQueue:nil]; + XCTAssertNotNil(data); + XCTAssertNotNil([data allKeys]); + XCTAssertEqualObjects([data allKeys], @[]); + + data = [[PFObjectEstimatedData alloc] initWithServerData:@{ @"a" : @"b" } operationSetQueue:nil]; + XCTAssertNotNil(data); + XCTAssertNotNil([data allKeys]); + XCTAssertEqualObjects([data allKeys], @[ @"a" ]); + XCTAssertEqualObjects(data[@"a"], @"b"); + + data = [PFObjectEstimatedData estimatedDataFromServerData:@{ @"a" : @"b" } operationSetQueue:nil]; + XCTAssertNotNil(data); + XCTAssertNotNil([data allKeys]); + XCTAssertEqualObjects([data allKeys], @[ @"a" ]); + XCTAssertEqualObjects(data[@"a"], @"b"); + + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + operationSet[@"c"] = [PFSetOperation setWithValue:@"d"]; + + data = [PFObjectEstimatedData estimatedDataFromServerData:@{ @"a" : @"b" } + operationSetQueue:@[ operationSet ]]; + XCTAssertNotNil(data); + XCTAssertNotNil([data allKeys]); + XCTAssertEqualObjects([data allKeys], (@[ @"a", @"c" ])); + XCTAssertEqualObjects(data[@"a"], @"b"); + XCTAssertEqualObjects(data[@"c"], @"d"); +} + +- (void)testObjectForKey { + PFObjectEstimatedData *data = [PFObjectEstimatedData estimatedDataFromServerData:@{ @"a" : @"b" } + operationSetQueue:nil]; + XCTAssertEqualObjects([data objectForKey:@"a"], @"b"); + XCTAssertEqualObjects(data[@"a"], @"b"); +} + +- (void)testEnumeration { + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + operationSet[@"c"] = [PFSetOperation setWithValue:@"d"]; + PFObjectEstimatedData *data = [PFObjectEstimatedData estimatedDataFromServerData:@{ @"a" : @"b" } + operationSetQueue:@[ operationSet ]]; + + __block NSUInteger counter = 0; + [data enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + if (counter == 0) { + XCTAssertEqualObjects(key, @"a"); + XCTAssertEqualObjects(obj, @"b"); + } else if (counter == 1) { + XCTAssertEqualObjects(key, @"c"); + XCTAssertEqualObjects(obj, @"d"); + } else { + XCTFail(); + } + counter++; + }]; +} + +- (void)testAllKeys { + PFObjectEstimatedData *data = [PFObjectEstimatedData estimatedDataFromServerData:@{ @"a" : @"b" } + operationSetQueue:nil]; + XCTAssertEqualObjects([data allKeys], @[ @"a" ]); +} + +- (void)testDictionaryRepresentation { + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + operationSet[@"c"] = [PFSetOperation setWithValue:@"d"]; + PFObjectEstimatedData *data = [PFObjectEstimatedData estimatedDataFromServerData:@{ @"a" : @"b" } + operationSetQueue:@[ operationSet ]]; + NSDictionary *dictionary = data.dictionaryRepresentation; + XCTAssertEqualObjects(dictionary, (@{ @"a" : @"b", @"c" : @"d" })); + XCTAssertNotEqual(dictionary, data.dictionaryRepresentation); +} + +@end diff --git a/Tests/Unit/ObjectFileCoderTests.m b/Tests/Unit/ObjectFileCoderTests.m new file mode 100644 index 000000000..0ea3d1991 --- /dev/null +++ b/Tests/Unit/ObjectFileCoderTests.m @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDecoder.h" +#import "PFEncoder.h" +#import "PFJSONSerialization.h" +#import "PFObject.h" +#import "PFObjectFileCoder.h" +#import "PFTestCase.h" + +@interface ObjectFileCoderTests : PFTestCase + +@end + +@implementation ObjectFileCoderTests + +- (void)testDataFromObject { + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + object.objectId = @"100500"; + object[@"yarr"] = @"pff"; + + NSData *data = [PFObjectFileCoder dataFromObject:object usingEncoder:[PFEncoder objectEncoder]]; + NSDictionary *dictionary = [PFJSONSerialization JSONObjectFromData:data]; + XCTAssertEqualObjects(dictionary, (@{ @"classname" : @"Yolo", + @"data" : @{@"objectId" : @"100500"} })); +} + +- (void)testObjectFromData { + NSDictionary *dictionary = @{ @"classname" : @"Yolo", + @"data" : @{@"objectId" : @"100500", @"yarr" : @"pff"} }; + NSData *data = [PFJSONSerialization dataFromJSONObject:dictionary]; + + PFObject *object = [PFObjectFileCoder objectFromData:data usingDecoder:[PFDecoder objectDecoder]]; + XCTAssertNotNil(object); + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"100500"); + XCTAssertEqualObjects(object[@"yarr"], @"pff"); + XCTAssertFalse(object.isDirty); +} + +@end diff --git a/Tests/Unit/ObjectFileCodingLogicTests.m b/Tests/Unit/ObjectFileCodingLogicTests.m new file mode 100644 index 000000000..b98609a0f --- /dev/null +++ b/Tests/Unit/ObjectFileCodingLogicTests.m @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDateFormatter.h" +#import "PFDecoder.h" +#import "PFObject.h" +#import "PFObjectFileCodingLogic.h" +#import "PFTestCase.h" + +@interface ObjectFileCodingLogicTests : PFTestCase + +@end + +@implementation ObjectFileCodingLogicTests + +- (void)testConstructors { + PFObjectFileCodingLogic *logic = [[PFObjectFileCodingLogic alloc] init]; + XCTAssertNotNil(logic); + + logic = [PFObjectFileCodingLogic codingLogic]; + XCTAssertNotNil(logic); + + XCTAssertNotEqual([PFObjectFileCodingLogic codingLogic], [PFObjectFileCodingLogic codingLogic]); +} + +- (void)testUpdateObject { + NSDictionary *dictionary = @{ @"className" : @"Yolo", + @"data" : @{@"objectId" : @"100500", @"slogan" : @"yarr"} }; + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + + PFObjectFileCodingLogic *logic = [PFObjectFileCodingLogic codingLogic]; + [logic updateObject:object fromDictionary:dictionary usingDecoder:[PFDecoder objectDecoder]]; + + XCTAssertNotNil(object); + XCTAssertEqualObjects(object.objectId, @"100500"); + XCTAssertEqualObjects(object[@"slogan"], @"yarr"); + XCTAssertTrue(object.isDataAvailable); +} + +- (void)testUpdateObjectWithLegacyKeys { + NSDictionary *dictionary = @{ @"classname" : @"Yolo", + @"id" : @"100500", + @"created_at" : [[PFDateFormatter sharedFormatter] preciseStringFromDate:[NSDate date]], + @"updated_at" : [[PFDateFormatter sharedFormatter] preciseStringFromDate:[NSDate date]], + @"pointers" : @{@"yarr" : @[ @"Pirate", @"pff" ]}, + @"data" : @{@"a" : @"b"} }; + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + + PFObjectFileCodingLogic *logic = [PFObjectFileCodingLogic codingLogic]; + [logic updateObject:object fromDictionary:dictionary usingDecoder:[PFDecoder objectDecoder]]; + + XCTAssertNotNil(object); + XCTAssertEqualObjects(object.objectId, @"100500"); + XCTAssertEqualObjects(object.createdAt, [[PFDateFormatter sharedFormatter] dateFromString:dictionary[@"created_at"]]); + XCTAssertEqualObjects(object.updatedAt, [[PFDateFormatter sharedFormatter] dateFromString:dictionary[@"updated_at"]]); + XCTAssertEqualObjects(object[@"a"], @"b"); + XCTAssertTrue(object.isDataAvailable); + + PFObject *pointer = object[@"yarr"]; + XCTAssertEqualObjects(pointer.parseClassName, @"Pirate"); + XCTAssertEqualObjects(pointer.objectId, @"pff"); + XCTAssertFalse(pointer.isDataAvailable); +} + +@end diff --git a/Tests/Unit/ObjectFilePersistenceControllerTests.m b/Tests/Unit/ObjectFilePersistenceControllerTests.m new file mode 100644 index 000000000..251e6ba65 --- /dev/null +++ b/Tests/Unit/ObjectFilePersistenceControllerTests.m @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFFileManager.h" +#import "PFObject.h" +#import "PFObjectFilePersistenceController.h" +#import "PFUnitTestCase.h" + +@interface ObjectFilePersistenceControllerTests : PFUnitTestCase + +@end + +@implementation ObjectFilePersistenceControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFFileManagerProvider)); + OCMStub([dataSource fileManager]).andReturn(PFStrictClassMock([PFFileManager class])); + return dataSource; +} + +- (NSString *)testFilePathForSelector:(SEL)cmd { + NSString *configPath = [NSTemporaryDirectory() stringByAppendingPathComponent:NSStringFromSelector(cmd)]; + [[NSFileManager defaultManager] removeItemAtPath:configPath error:NULL]; + return configPath; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id dataSource = [self mockedDataSource]; + PFObjectFilePersistenceController *controller = [[PFObjectFilePersistenceController alloc] initWithDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, dataSource); + + controller = [PFObjectFilePersistenceController controllerWithDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, dataSource); + + PFAssertThrowsInconsistencyException([PFObjectFilePersistenceController new]); +} + +- (void)testLoadPersistentObject { + id dataSource = [self mockedDataSource]; + id fileManager = [dataSource fileManager]; + + NSString *path = [self testFilePathForSelector:_cmd]; + OCMStub([fileManager parseDataItemPathForPathComponent:@"object"]).andReturn(path); + + PFObjectFilePersistenceController *controller = [PFObjectFilePersistenceController controllerWithDataSource:dataSource]; + + NSDictionary *dictionary = @{ @"classname" : @"Yolo", + @"data" : @{@"objectId" : @"100500", @"yarr" : @"pff"} }; + NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; + [data writeToFile:path atomically:YES]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller loadPersistentObjectAsyncForKey:@"object"] continueWithSuccessBlock:^id(BFTask *task) { + PFObject *object = task.result; + XCTAssertNotNil(object); + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"100500"); + XCTAssertEqualObjects(object[@"yarr"], @"pff"); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testPersistObjectForKey { + id dataSource = [self mockedDataSource]; + id fileManager = [dataSource fileManager]; + + NSString *path = [self testFilePathForSelector:_cmd]; + OCMStub([fileManager parseDataItemPathForPathComponent:@"object"]).andReturn(path); + + PFObjectFilePersistenceController *controller = [PFObjectFilePersistenceController controllerWithDataSource:dataSource]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + object.objectId = @"100500"; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller persistObjectAsync:object forKey:@"object"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertNil(task.result); + NSData *data = [NSData dataWithContentsOfFile:path]; + XCTAssertNotNil(data); + + NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + XCTAssertNotNil(dictionary); + XCTAssertEqualObjects(dictionary[@"classname"], @"Yolo"); + XCTAssertEqualObjects(dictionary[@"data"][@"objectId"], @"100500"); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/ObjectLocalIdStoreTests.m b/Tests/Unit/ObjectLocalIdStoreTests.m new file mode 100644 index 000000000..bd2e54cc6 --- /dev/null +++ b/Tests/Unit/ObjectLocalIdStoreTests.m @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreManager.h" +#import "PFDecoder.h" +#import "PFFileManager.h" +#import "PFInternalUtils.h" +#import "PFJSONSerialization.h" +#import "PFObjectLocalIdStore.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface ObjectLocalIdStoreTests : PFUnitTestCase + +@end + +@implementation ObjectLocalIdStoreTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [[Parse _currentManager].coreManager.objectLocalIdStore clear]; +} + +- (void)tearDown { + [[Parse _currentManager].coreManager.objectLocalIdStore clear]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFFileManagerProvider)); + [OCMStub(dataSource.fileManager) andReturn:PFStrictClassMock([PFFileManager class])]; + return dataSource; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id dataSource = [self mockedDataSource]; + + PFObjectLocalIdStore *store = [[PFObjectLocalIdStore alloc] initWithDataSource:dataSource]; + XCTAssertNotNil(store); + XCTAssertEqual((id)store.dataSource, dataSource); + + store = [PFObjectLocalIdStore storeWithDataSource:dataSource]; + XCTAssertNotNil(store); + XCTAssertEqual((id)store.dataSource, dataSource); +} + +- (void)testRetain { + PFObjectLocalIdStore *manager = [Parse _currentManager].coreManager.objectLocalIdStore; + + NSString *localId1 = [manager createLocalId]; + XCTAssertNotNil(localId1); + [manager retainLocalIdOnDisk:localId1]; // refcount = 1 + XCTAssertNil([manager objectIdForLocalId:localId1]); + + NSString *localId2 = [manager createLocalId]; + XCTAssertNotNil(localId2); + [manager retainLocalIdOnDisk:localId2]; // refcount = 1 + XCTAssertNil([manager objectIdForLocalId:localId2]); + + [manager retainLocalIdOnDisk:localId1]; // refcount = 2 + XCTAssertNil([manager objectIdForLocalId:localId1]); + XCTAssertNil([manager objectIdForLocalId:localId2]); + + [manager releaseLocalIdOnDisk:localId1]; // refcount = 1 + XCTAssertNil([manager objectIdForLocalId:localId1]); + XCTAssertNil([manager objectIdForLocalId:localId2]); + + NSString *objectId1 = @"objectId1"; + [manager setObjectId:objectId1 forLocalId:localId1]; + XCTAssertEqualObjects(objectId1, [manager objectIdForLocalId:localId1]); + XCTAssertNil([manager objectIdForLocalId:localId2]); + + [manager retainLocalIdOnDisk:localId1]; // refcount = 2 + XCTAssertEqualObjects(objectId1, [manager objectIdForLocalId:localId1]); + XCTAssertNil([manager objectIdForLocalId:localId2]); + + NSString *objectId2 = @"objectId2"; + [manager setObjectId:objectId2 forLocalId:localId2]; + XCTAssertEqualObjects(objectId1, [manager objectIdForLocalId:localId1]); + XCTAssertEqualObjects(objectId2, [manager objectIdForLocalId:localId2]); + + [manager releaseLocalIdOnDisk:localId1]; // refcount = 1 + XCTAssertEqualObjects(objectId1, [manager objectIdForLocalId:localId1]); + XCTAssertEqualObjects(objectId2, [manager objectIdForLocalId:localId2]); + + [manager releaseLocalIdOnDisk:localId1]; // refcount = 0 + XCTAssertEqualObjects(objectId1, [manager objectIdForLocalId:localId1]); + XCTAssertEqualObjects(objectId2, [manager objectIdForLocalId:localId2]); + + [manager clearInMemoryCache]; + XCTAssertNil([manager objectIdForLocalId:localId1]); + XCTAssertEqualObjects(objectId2, [manager objectIdForLocalId:localId2]); + + [manager releaseLocalIdOnDisk:localId2]; // refcount = 0 + XCTAssertNil([manager objectIdForLocalId:localId1]); + XCTAssertNil([manager objectIdForLocalId:localId2]); + + [manager clearInMemoryCache]; + XCTAssertNil([manager objectIdForLocalId:localId1]); + XCTAssertNil([manager objectIdForLocalId:localId2]); + + XCTAssertFalse([[Parse _currentManager].coreManager.objectLocalIdStore clear]); +} + +- (void)testRetainAfterRelease { + PFObjectLocalIdStore *manager = [Parse _currentManager].coreManager.objectLocalIdStore; + + NSString *localId = [manager createLocalId]; + [manager setObjectId:@"venus" forLocalId:localId]; + [manager retainLocalIdOnDisk:localId]; + [manager clearInMemoryCache]; + XCTAssertEqualObjects(@"venus", [manager objectIdForLocalId:localId]); +} + +- (void)testLongSerialization { + long long expected = 0x8000000000000000L; + NSDictionary *object = @{ @"hugeNumber": @(expected) }; + + NSString *json = [PFJSONSerialization stringFromJSONObject:object]; + + NSDictionary *parsed = [PFJSONSerialization JSONObjectFromString:json]; + object = [[PFDecoder objectDecoder] decodeObject:parsed]; + long long actual = [[object objectForKey:@"hugeNumber"] longLongValue]; + XCTAssertEqual(expected, actual, @"The number should be parsed correctly."); +} + +@end diff --git a/Tests/Unit/ObjectOfflineTests.m b/Tests/Unit/ObjectOfflineTests.m new file mode 100644 index 000000000..ef918d75e --- /dev/null +++ b/Tests/Unit/ObjectOfflineTests.m @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFObject.h" +#import "PFOfflineStore.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface ObjectOfflineTests : PFUnitTestCase + +@end + +@implementation ObjectOfflineTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedOfflineStore { + id store = PFStrictClassMock([PFOfflineStore class]); + [Parse _currentManager].offlineStore = store; + return store; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testFetchFromLocalDatastore { + id store = [self mockedOfflineStore]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + + [OCMExpect([store fetchObjectLocallyAsync:object]) andReturn:[BFTask taskWithResult:nil]]; + XCTAssertNoThrow([object fetchFromLocalDatastore]); + + OCMVerifyAll(store); +} + +- (void)testFetchFromLocalDatastoreWithError { + id store = [self mockedOfflineStore]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + + NSError *expectedError = [NSError errorWithDomain:@"YoloTest" code:100500 userInfo:nil]; + [OCMExpect([store fetchObjectLocallyAsync:object]) andReturn:[BFTask taskWithError:expectedError]]; + + NSError *error = nil; + XCTAssertNoThrow([object fetchFromLocalDatastore:&error]); + XCTAssertEqualObjects(error, expectedError); + + OCMVerifyAll(store); +} + +- (void)testFetchFromLocalDatastoreViaTask { + id store = [self mockedOfflineStore]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + [OCMExpect([store fetchObjectLocallyAsync:object]) andReturn:[BFTask taskWithResult:object]]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[object fetchFromLocalDatastoreInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, object); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(store); +} + +- (void)testFetchFromLocalDatastoreViaBlock { + id store = [self mockedOfflineStore]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + [OCMExpect([store fetchObjectLocallyAsync:object]) andReturn:[BFTask taskWithResult:object]]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [object fetchFromLocalDatastoreInBackgroundWithBlock:^(PFObject *resultObject, NSError *error) { + XCTAssertEqual(resultObject, object); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(store); +} + +@end diff --git a/Tests/Unit/ObjectPinTests.m b/Tests/Unit/ObjectPinTests.m new file mode 100644 index 000000000..d8648c026 --- /dev/null +++ b/Tests/Unit/ObjectPinTests.m @@ -0,0 +1,540 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreManager.h" +#import "PFObjectPrivate.h" +#import "PFPinningObjectStore.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface ObjectPinTests : PFUnitTestCase + +@end + +@implementation ObjectPinTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockPinObjects:(NSArray *)objects withPinName:(NSString *)pinName error:(NSError *)error { + PFPinningObjectStore *store = PFStrictClassMock([PFPinningObjectStore class]); + [Parse _currentManager].coreManager.pinningObjectStore = store; + + BFTask *task = (error ? [BFTask taskWithError:error] : [BFTask taskWithResult:@YES]); + OCMExpect([store pinObjectsAsync:objects + withPinName:pinName + includeChildren:YES]).andReturn(task); + + return store; +} + +- (id)mockUnpinObjects:(NSArray *)objects withPinName:(NSString *)pinName error:(NSError *)error { + PFPinningObjectStore *store = PFStrictClassMock([PFPinningObjectStore class]); + [Parse _currentManager].coreManager.pinningObjectStore = store; + + BFTask *task = (error ? [BFTask taskWithError:error] : [BFTask taskWithResult:@YES]); + OCMExpect([store unpinObjectsAsync:objects withPinName:pinName]).andReturn(task); + + return store; +} + +- (id)mockUnpinAllObjectsWithPinName:(NSString *)pinName error:(NSError *)error { + PFPinningObjectStore *store = PFStrictClassMock([PFPinningObjectStore class]); + [Parse _currentManager].coreManager.pinningObjectStore = store; + + BFTask *task = (error ? [BFTask taskWithError:error] : [BFTask taskWithResult:@YES]); + OCMExpect([store unpinAllObjectsAsyncWithPinName:pinName]).andReturn(task); + + return store; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +#pragma mark Pinning + +- (void)testPinObject { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:PFObjectDefaultPin error:nil]; + + XCTAssertTrue([object pin]); + OCMVerifyAll(mock); +} + +- (void)testPinObjectWithError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:PFObjectDefaultPin error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([object pin:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testPinObjectViaTask { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[object pinInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testPinObjectViaBlock { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [object pinInBackgroundWithBlock:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testPinObjectWithName { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:@"Pirates" error:nil]; + + XCTAssertTrue([object pinWithName:@"Pirates"]); + OCMVerifyAll(mock); +} + +- (void)testPinObjectWithNameError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:@"Pirates" error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([object pinWithName:@"Pirates" error:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testPinObjectWithNameViaTask { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[object pinInBackgroundWithName:@"Pirates"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testPinObjectWithNameViaBlock { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockPinObjects:@[ object ] withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [object pinInBackgroundWithName:@"Pirates" block:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +#pragma mark Pinning Many Objects + +- (void)testPinAll { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:PFObjectDefaultPin error:nil]; + + XCTAssertTrue([PFObject pinAll:objects]); + OCMVerifyAll(mock); +} + +- (void)testPinAllWithError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:PFObjectDefaultPin error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([PFObject pinAll:objects error:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testPinAllViaTask { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFObject pinAllInBackground:objects] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testPinAllViaBlock { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFObject pinAllInBackground:objects block:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testPinAllWithName { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:@"Pirates" error:nil]; + + XCTAssertTrue([PFObject pinAll:objects withName:@"Pirates"]); + OCMVerifyAll(mock); +} + +- (void)testPinAllWithNameError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:@"Pirates" error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([PFObject pinAll:objects withName:@"Pirates" error:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testPinAllWithNameViaTask { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFObject pinAllInBackground:objects withName:@"Pirates"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testPinAllWithNameViaBlock { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockPinObjects:objects withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFObject pinAllInBackground:objects withName:@"Pirates" block:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +#pragma mark Unpinning + +- (void)testUnpinObject { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:PFObjectDefaultPin error:nil]; + + XCTAssertTrue([object unpin]); + OCMVerifyAll(mock); +} + +- (void)testUnpinObjectWithError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:PFObjectDefaultPin error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([object unpin:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testUnpinObjectViaTask { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[object unpinInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinObjectViaBlock { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [object unpinInBackgroundWithBlock:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinObjectWithName { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:@"Pirates" error:nil]; + + XCTAssertTrue([object unpinWithName:@"Pirates"]); + OCMVerifyAll(mock); +} + +- (void)testUnpinObjectWithNameError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:@"Pirates" error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([object unpinWithName:@"Pirates" error:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testUnpinObjectWithNameViaTask { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[object unpinInBackgroundWithName:@"Pirates"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinObjectWithNameViaBlock { + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + id mock = [self mockUnpinObjects:@[ object ] withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [object unpinInBackgroundWithName:@"Pirates" block:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +#pragma mark Unpinning Many Objects + +- (void)testUnpinAll { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:PFObjectDefaultPin error:nil]; + + XCTAssertTrue([PFObject unpinAll:objects]); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllWithError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:PFObjectDefaultPin error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([PFObject unpinAll:objects error:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllViaTask { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFObject unpinAllInBackground:objects] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinAllWithBlock { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFObject unpinAllInBackground:objects block:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinAllWithName { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:@"Pirates" error:nil]; + + XCTAssertTrue([PFObject unpinAll:objects withName:@"Pirates"]); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllWithNameError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:@"Pirates" error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([PFObject unpinAll:objects withName:@"Pirates" error:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllWithNameViaTask { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFObject unpinAllInBackground:objects withName:@"Pirates"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinAllWithNameViaBlock { + NSArray *objects = @[ [PFObject objectWithClassName:@"Yarr"] ]; + id mock = [self mockUnpinObjects:objects withPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFObject unpinAllInBackground:objects withName:@"Pirates" block:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjects { + id mock = [self mockUnpinAllObjectsWithPinName:PFObjectDefaultPin error:nil]; + XCTAssertTrue([PFObject unpinAllObjects]); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjectsWithError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + id mock = [self mockUnpinAllObjectsWithPinName:PFObjectDefaultPin error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([PFObject unpinAllObjects:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjectsViaTask { + id mock = [self mockUnpinAllObjectsWithPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFObject unpinAllObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjectsViaBlock { + id mock = [self mockUnpinAllObjectsWithPinName:PFObjectDefaultPin error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFObject unpinAllObjectsInBackgroundWithBlock:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjectsWithName { + id mock = [self mockUnpinAllObjectsWithPinName:@"Pirates" error:nil]; + XCTAssertTrue([PFObject unpinAllObjectsWithName:@"Pirates"]); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjectsWithNameError { + NSError *expectedError = [NSError errorWithDomain:@"Yolo!" code:100500 userInfo:nil]; + id mock = [self mockUnpinAllObjectsWithPinName:@"Pirates" error:expectedError]; + + NSError *error = nil; + XCTAssertFalse([PFObject unpinAllObjectsWithName:@"Pirates" error:&error]); + XCTAssertEqualObjects(error, expectedError); + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjectsWithNameViaTask { + id mock = [self mockUnpinAllObjectsWithPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFObject unpinAllObjectsInBackgroundWithName:@"Pirates"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +- (void)testUnpinAllObjectsWithNameViaBlock { + id mock = [self mockUnpinAllObjectsWithPinName:@"Pirates" error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFObject unpinAllObjectsInBackgroundWithName:@"Pirates" block:^(BOOL success, NSError *error){ + XCTAssertTrue(success); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(mock); +} + +@end diff --git a/Tests/Unit/ObjectStateTests.m b/Tests/Unit/ObjectStateTests.m new file mode 100644 index 000000000..0b869ee64 --- /dev/null +++ b/Tests/Unit/ObjectStateTests.m @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFEncoder.h" +#import "PFFieldOperation.h" +#import "PFMutableObjectState.h" +#import "PFOperationSet.h" +#import "PFTestCase.h" + +@interface ObjectStateTests : PFTestCase + +@end + +@implementation ObjectStateTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFObjectState *)sampleObjectState { + PFMutableObjectState *state = [PFMutableObjectState stateWithParseClassName:@"Yarr"]; + state.objectId = @"yolo"; + state.complete = YES; + return state; +} + +- (void)assertObjectState:(PFObjectState *)state equalToState:(PFObjectState *)differentState { + XCTAssertEqualObjects(state.parseClassName, differentState.parseClassName); + XCTAssertEqualObjects(state.objectId, differentState.objectId); + XCTAssertEqual(state.complete, differentState.complete); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testInit { + PFObjectState *state = [[PFObjectState alloc] init]; + XCTAssertNil(state.parseClassName); + XCTAssertNil(state.objectId); + XCTAssertFalse(state.complete); + + state = [[PFMutableObjectState alloc] init]; + XCTAssertNil(state.parseClassName); + XCTAssertNil(state.objectId); + XCTAssertFalse(state.complete); +} + +- (void)testInitWithParseClassName { + PFObjectState *state = [[PFObjectState alloc] initWithParseClassName:@"Yarr"]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + + state = [PFObjectState stateWithParseClassName:@"Yarr"]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + + state = [[PFMutableObjectState alloc] initWithParseClassName:@"Yarr"]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + + state = [PFMutableObjectState stateWithParseClassName:@"Yarr"]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + + state = [PFObjectState stateWithParseClassName:@"Yarr" objectId:@"a" isComplete:YES]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + XCTAssertEqualObjects(state.objectId, @"a"); + XCTAssertTrue(state.complete); + + state = [[PFObjectState alloc] initWithParseClassName:@"Yarr" objectId:@"a" isComplete:YES]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + XCTAssertEqualObjects(state.objectId, @"a"); + XCTAssertTrue(state.complete); + + state = [PFMutableObjectState stateWithParseClassName:@"Yarr" objectId:@"a" isComplete:YES]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + XCTAssertEqualObjects(state.objectId, @"a"); + XCTAssertTrue(state.complete); + + state = [[PFMutableObjectState alloc] initWithParseClassName:@"Yarr" objectId:@"a" isComplete:YES]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + XCTAssertEqualObjects(state.objectId, @"a"); + XCTAssertTrue(state.complete); +} + +- (void)testInitWithState { + PFObjectState *sampleState = [self sampleObjectState]; + + PFObjectState *state = [[PFObjectState alloc] initWithState:sampleState]; + [self assertObjectState:state equalToState:sampleState]; + + state = [PFObjectState stateWithState:sampleState]; + [self assertObjectState:state equalToState:sampleState]; + + state = [[PFMutableObjectState alloc] initWithState:sampleState]; + [self assertObjectState:state equalToState:sampleState]; + + state = [PFMutableObjectState stateWithState:sampleState]; + [self assertObjectState:state equalToState:sampleState]; +} + +- (void)testCopying { + PFObjectState *sampleState = [self sampleObjectState]; + PFObjectState *stateCopy = [sampleState copy]; + XCTAssertNotEqual(sampleState, stateCopy); + [self assertObjectState:stateCopy equalToState:sampleState]; + + PFMutableObjectState *mutableState = [PFMutableObjectState stateWithState:sampleState]; + stateCopy = [mutableState copy]; + XCTAssertNotEqual(mutableState, stateCopy); + [self assertObjectState:stateCopy equalToState:sampleState]; +} + +- (void)testMutableCopying { + PFObjectState *sampleState = [self sampleObjectState]; + PFObjectState *stateCopy = [sampleState mutableCopy]; + + XCTAssertNotEqual(sampleState, stateCopy); + [self assertObjectState:stateCopy equalToState:sampleState]; + + PFMutableObjectState *state = [PFMutableObjectState stateWithState:sampleState]; + stateCopy = [state mutableCopy]; + XCTAssertNotEqual(state, stateCopy); + [self assertObjectState:stateCopy equalToState:sampleState]; +} + +- (void)testMutableAccessors { + PFMutableObjectState *mutableState = [[PFMutableObjectState alloc] init]; + + mutableState.parseClassName = @"Yolo"; + XCTAssertEqualObjects(mutableState.parseClassName, @"Yolo"); + + mutableState.objectId = @"yarr"; + XCTAssertEqualObjects(mutableState.objectId, @"yarr"); + + mutableState.complete = YES; + XCTAssertTrue(mutableState.complete); + + NSString *isoDate = @"1970-01-01T00:00:00Z"; + [mutableState setCreatedAtFromString:isoDate]; + [mutableState setUpdatedAtFromString:isoDate]; + + XCTAssertEqualObjects(mutableState.createdAt, [NSDate dateWithTimeIntervalSince1970:0]); + XCTAssertEqualObjects(mutableState.updatedAt, [NSDate dateWithTimeIntervalSince1970:0]); +} + +- (void)testServerData { + PFMutableObjectState *mutableState = [[PFMutableObjectState alloc] init]; + XCTAssertEqualObjects(mutableState.serverData, @{}); + + [mutableState setServerDataObject:@"foo" forKey:@"bar"]; + XCTAssertEqualObjects(mutableState.serverData, @{ @"bar": @"foo" }); + + [mutableState removeServerDataObjectForKey:@"bar"]; + XCTAssertEqualObjects(mutableState.serverData, @{}); + + [mutableState setServerDataObject:@"foo" forKey:@"bar"]; + [mutableState removeServerDataObjectsForKeys:@[ @"bar" ]]; + + XCTAssertEqualObjects(mutableState.serverData, @{}); + mutableState.serverData = @{ @"foo": @"bar" }; + + XCTAssertEqualObjects(mutableState.serverData, @{ @"foo": @"bar" }); +} + +- (void)testEncode { + PFMutableObjectState *mutableState = [[PFMutableObjectState alloc] init]; + mutableState.objectId = @"objectId"; + mutableState.createdAt = [NSDate dateWithTimeIntervalSince1970:0]; + mutableState.updatedAt = [NSDate dateWithTimeIntervalSince1970:5]; + mutableState.serverData = @{ @"a": @"b" }; + + NSDictionary *expected = @{ + @"objectId": @"objectId", + @"createdAt": @"1970-01-01T00:00:00.000Z", + @"updatedAt": @"1970-01-01T00:00:05.000Z", + @"a": @"b" + }; + + NSDictionary *actual = [mutableState dictionaryRepresentationWithObjectEncoder:[PFEncoder objectEncoder]]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testApply { + PFMutableObjectState *objectState = [[PFMutableObjectState alloc] init]; + objectState.objectId = @"betterObjectId"; + objectState.createdAt = [NSDate dateWithTimeIntervalSince1970:17]; + objectState.updatedAt = [NSDate dateWithTimeIntervalSince1970:25]; + objectState.serverData = @{ @"a": @"b", @"c": @"d" }; + + PFMutableObjectState *secondState = [[PFMutableObjectState alloc] init]; + secondState.objectId = @"anObjectId"; + secondState.serverData = @{ @"a": @"d", @"e": @"f" }; + + [secondState applyState:objectState]; + + XCTAssertEqualObjects(secondState.objectId, @"betterObjectId"); + XCTAssertEqualObjects(secondState.createdAt, [NSDate dateWithTimeIntervalSince1970:17]); + XCTAssertEqualObjects(secondState.updatedAt, [NSDate dateWithTimeIntervalSince1970:25]); + XCTAssertEqualObjects(secondState.serverData, (@{ @"a": @"b", @"c": @"d", @"e": @"f" })); +} + +- (void)testApplyOperation { + PFMutableObjectState *objectState = [[PFMutableObjectState alloc] init]; + objectState.serverData = @{ + @"a": @13, + @"b": @"someValue", + @"c": @15 + }; + + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + operationSet[@"a"] = [[PFIncrementOperation alloc] initWithAmount:@10]; + operationSet[@"b"] = [[PFDeleteOperation alloc] init]; + operationSet[@"c"] = [[PFSetOperation alloc] initWithValue:@25]; + + [objectState applyOperationSet:operationSet]; + XCTAssertEqualObjects(objectState.serverData, (@{ @"a": @23, @"c": @25 })); +} + +@end diff --git a/Tests/Unit/ObjectSubclassPropertiesTests.m b/Tests/Unit/ObjectSubclassPropertiesTests.m new file mode 100644 index 000000000..39902f960 --- /dev/null +++ b/Tests/Unit/ObjectSubclassPropertiesTests.m @@ -0,0 +1,324 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObject+Subclass.h" +#import "PFObjectPrivate.h" +#import "PFRelation.h" +#import "PFSubclassing.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +// A test class used to verify that dynamic sythesizers work for properties of the +// object and ivars with copy and retain semantics. Properties that are NSNumbers +// should also support accessors and mutators which automatically unbox the value into +// its corresponding primitive. +@interface PFTestObject : PFObject { +@public + id ivarId; + int ivarInt; + bool ivarCXXBool; + unsigned short ivarShort; + BOOL usedNativeAccessor; + BOOL usedNativeMutator; +} + +@property (atomic, retain) id idProperty; +@property (atomic, copy) NSString *stringCopyProperty; +@property (atomic, assign) short shortProperty; +@property (atomic, assign) unsigned short ushortProperty; +@property (atomic, assign) int intProperty; +@property (atomic, assign) uint uintProperty; +@property (atomic, assign) long longProperty; +@property (atomic, assign) unsigned long ulongProperty; +@property (atomic, assign) BOOL boolProperty; +@property (atomic, retain) id ivarId; +@property (atomic, assign) int ivarInt; +@property (atomic, assign) unsigned short ivarShort; +@property (atomic, copy) NSString *stringWithNativeAccessor; +@property (atomic, copy) NSString *stringWithNativeMutator; +@property (atomic, assign) float floatProperty; +@property (atomic, assign) double doubleProperty; +@property (atomic, assign) int x; +@property (atomic, assign) int PascalCaseProperty; +@property (atomic, assign) bool ivarCXXBool; +@property (atomic, assign) bool cxxBool; +@property (atomic, strong) PFRelation *relation; + +@end + +@implementation PFTestObject + +@dynamic idProperty; +@dynamic stringCopyProperty; +@dynamic shortProperty; +@dynamic ushortProperty; +@dynamic intProperty; +@dynamic uintProperty; +@dynamic longProperty; +@dynamic ulongProperty; +@dynamic boolProperty; +@dynamic ivarId; +@dynamic ivarInt; +@dynamic ivarShort; +@dynamic stringWithNativeAccessor; +@dynamic stringWithNativeMutator; +@dynamic floatProperty; +@dynamic doubleProperty; +@dynamic x; +@dynamic PascalCaseProperty; +@dynamic cxxBool; +@dynamic ivarCXXBool; +@dynamic relation; + ++ (NSString *)parseClassName { + return @"Test"; +} + +- (NSString *)stringWithNativeAccessor { + usedNativeAccessor = YES; + return [self objectForKey:@"stringWithNativeAccessor"]; +} + +- (void)setStringWithNativeMutator:(NSString *)aString { + usedNativeMutator = YES; + [self setObject:aString forKey:@"stringWithNativeMutator"]; +} +@end + +///-------------------------------------- +#pragma mark - ObjectSubclassPropertiesTests +///-------------------------------------- + +@interface ObjectSubclassPropertiesTests : PFUnitTestCase + +@end + +@implementation ObjectSubclassPropertiesTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [PFTestObject registerSubclass]; +} + +- (void)tearDown { + [PFObject unregisterSubclass:[PFTestObject class]]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testDynamicObjectProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + NSString *idValue = @"foo"; + object.idProperty = idValue; + XCTAssertEqualObjects(@"foo", [object objectForKey:@"idProperty"]); + XCTAssertEqualObjects(@"foo", object.idProperty); + XCTAssertEqual(idValue, object.idProperty, @"Should use retain semantics"); +} + +- (void)testDynamicObjectPropertyWithCopySemantics { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + // Must use a mutable string so -copy isn't optimized to return self; + NSMutableString *stringValue = [NSMutableString stringWithString:@"stringValue"]; + object.stringCopyProperty = stringValue; + XCTAssertEqualObjects(stringValue, object.stringCopyProperty); + XCTAssertNotEqual(stringValue, object.stringCopyProperty, + @"Should use copy semantics"); +} + +- (void)testBoxedIntegerProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + // Boxed primitives + object.shortProperty = 1; + XCTAssertEqualObjects(@1, [object objectForKey:@"shortProperty"]); + XCTAssertEqual((short)1, object.shortProperty); + + object.ushortProperty = 2; + XCTAssertEqualObjects(@2, [object objectForKey:@"ushortProperty"]); + XCTAssertEqual((unsigned short)2, object.ushortProperty); + + object.intProperty = 3; + XCTAssertEqualObjects(@3, [object objectForKey:@"intProperty"]); + XCTAssertEqual((int)3, object.intProperty); + + object.uintProperty = 4; + XCTAssertEqualObjects(@4, [object objectForKey:@"uintProperty"]); + XCTAssertEqual((unsigned int)4, object.uintProperty); + + object.longProperty = 5; + XCTAssertEqualObjects(@5, [object objectForKey:@"longProperty"]); + XCTAssertEqual((long)5, object.longProperty); + + object.ulongProperty = 6; + XCTAssertEqualObjects(@6, [object objectForKey:@"ulongProperty"]); + XCTAssertEqual((unsigned long)6, object.ulongProperty); +} + +- (void)testBoxedFloatingPointProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + object.floatProperty = 1.5; + XCTAssertEqualObjects(@1.5f, [object objectForKey:@"floatProperty"]); + XCTAssertEqual(1.5f, object.floatProperty); + + object.doubleProperty = 1.75; + XCTAssertEqualObjects(@1.75, [object objectForKey:@"doubleProperty"]); + XCTAssertEqual(1.75, object.doubleProperty); +} + +- (void)testBoxedBooleanProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + object.boolProperty = YES; + XCTAssertTrue(object.boolProperty); + XCTAssertEqualObjects(@YES, [object objectForKey:@"boolProperty"]); + object.boolProperty = NO; + + // HOORAY! Boxing makes if statements work like most users expect they would + XCTAssertFalse(object.boolProperty); +} + +- (void)testShortNameProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + object.x = 1; + XCTAssertEqual(object.x, 1); + XCTAssertEqualObjects(@1, [object objectForKey:@"x"]); +} + +- (void)testPascalCaseProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + object.PascalCaseProperty = 1; + XCTAssertEqual(object.PascalCaseProperty, 1); + XCTAssertEqualObjects(@1, [object objectForKey:@"PascalCaseProperty"]); +} + +- (void)testIvarObjectProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + object->ivarId = @"Hello,"; + XCTAssertEqualObjects(@"Hello,", object.ivarId); + + object.ivarId = @"World!"; + XCTAssertEqualObjects(@"World!", object->ivarId); + + XCTAssertNil([object objectForKey:@"ivarId"]); +} + +- (void)testIvarPrimitiveProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + object->ivarInt = 5; + XCTAssertEqual(5, object.ivarInt); + + object.ivarInt = 6; + XCTAssertEqual(6, object->ivarInt); + + XCTAssertNil([object objectForKey:@"ivarInt"]); + + // Test something that's not a bus width + object->ivarShort = 42; + XCTAssertEqual((unsigned short)42, object.ivarShort); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wconversion" + object.ivarShort = 0xCAFEBABE; + XCTAssertEqual((unsigned short)0xBABE, object->ivarShort); +#pragma clang diagnostic pop + + XCTAssertNil([object objectForKey:@"ivarShort"]); +} + +- (void)testCXXBoolProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + object.cxxBool = true; + XCTAssertTrue(object.cxxBool); + XCTAssertEqualObjects(@YES, object[@"cxxBool"]); + object.cxxBool = false; + + XCTAssertFalse(object.cxxBool); +} + +- (void)testCXXBoolIvarProperties { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + + object->ivarCXXBool = true; + XCTAssertTrue(object.ivarCXXBool); + + object->ivarCXXBool = false; + XCTAssertFalse(object->ivarCXXBool); + + XCTAssertNil(object[@"ivarCXXBool"]); +} + +- (void)testDynamicPropertiesHonorPartialImplementations { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + object.stringWithNativeAccessor = @"Hello, world!"; + XCTAssertEqualObjects(@"Hello, world!", object.stringWithNativeAccessor); + XCTAssertTrue(object->usedNativeAccessor); + + object.stringWithNativeMutator = @"Hello, world!"; + XCTAssertEqualObjects(@"Hello, world!", object.stringWithNativeMutator); + XCTAssertTrue(object->usedNativeAccessor); +} + +- (void)testObjectPropertiesAreRemovedWhenNilled { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + object.stringCopyProperty = @"Hello, world!"; + XCTAssertTrue([[object allKeys] containsObject:@"stringCopyProperty"]); + object.stringCopyProperty = nil; + XCTAssertFalse([[object allKeys] containsObject:@"stringCopyProperty"]); +} + +// I'm not so sure the mutator is a good idea, but it'd be good to ensure that at least the +// accessors don't choke. Still, it's nice not to blow up if people do crazy things with NSNull. +- (void)testObjectPropertiesDontChokeOnNSNull { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + object.stringCopyProperty = (NSString *)[NSNull null]; + XCTAssertTrue([[object allKeys] containsObject:@"stringCopyProperty"]); + XCTAssertEqual((NSString *)nil, object.stringCopyProperty); + object.stringCopyProperty = nil; + XCTAssertFalse([[object allKeys] containsObject:@"stringCopyProperty"]); +} + +- (void)testBoxedPropertiesDontChokeOnNSNull { + PFTestObject *object = [[PFTestObject alloc] initWithClassName:@"Test"]; + object[@"intProperty"] = [NSNull null]; + XCTAssertEqual(0, object.intProperty); +} + +- (void)testRelationPropertiesCreateRelations { + PFTestObject *object = [PFTestObject object]; + + id relation = object.relation; + XCTAssertTrue([relation isKindOfClass:[PFRelation class]]); +} + +- (void)testRelationPropertiesAreReadOnly { + XCTAssertThrows([PFTestObject object].relation = [[PFRelation alloc] init], + @"Relations are read-only and should not be assignable"); +} + +@end diff --git a/Tests/Unit/ObjectSubclassTests.m b/Tests/Unit/ObjectSubclassTests.m new file mode 100644 index 000000000..03a5d7347 --- /dev/null +++ b/Tests/Unit/ObjectSubclassTests.m @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObject+Subclass.h" +#import "PFObjectPrivate.h" +#import "PFSubclassing.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +@interface TheFlash : PFObject { + NSString *flashName; +} + ++ (NSString *)parseClassName; + +@property (atomic, copy) NSString *flashName; +@property (atomic, copy, readonly) NSString *realName; +@end + +@implementation TheFlash + +@dynamic flashName; +@dynamic realName; + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + self.flashName = @"The Flash"; + + return self; +} + ++ (NSString *)parseClassName { + return @"Person"; +} + +@end + +@interface BarryAllen : TheFlash + +@end + +@implementation BarryAllen + ++ (NSString *)parseClassName { + return @"TheFlash"; +} + +@end + +@interface ClassWithDirtyingConstructor : PFObject +@end + +@implementation ClassWithDirtyingConstructor + +- (instancetype)init { + self = [super init]; + if (!self) return nil; + + self[@"Bar"] = @"Foo"; + + return self; +} + ++ (NSString *)parseClassName { + return @"ClassWithDirtyingConstructor"; +} + +@end + +@interface UtilityClass : PFObject +@end + +@implementation UtilityClass +@end + +@interface DescendantOfUtility : UtilityClass +@end + +@implementation DescendantOfUtility ++ (NSString *)parseClassName { + return @"Descendant"; +} +@end + +///-------------------------------------- +#pragma mark - ObjectSubclassTests +///-------------------------------------- + +@interface ObjectSubclassTests : PFUnitTestCase + +@end + +@implementation ObjectSubclassTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)tearDown { + [PFObject unregisterSubclass:[TheFlash class]]; + [PFObject unregisterSubclass:[BarryAllen class]]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testExplicitConstructor { + TheFlash *flash = [TheFlash alloc]; + XCTAssertThrows(flash = [flash init], @"Cannot init an unregistered subclass"); + + [TheFlash registerSubclass]; + flash = [[TheFlash alloc] init]; + XCTAssertEqualObjects(@"Person", TheFlash.parseClassName); + XCTAssertEqualObjects(@"The Flash", ((TheFlash*)flash).flashName); +} + +- (void)testSubclassConstructor { + PFObject *theFlash = [PFObject objectWithClassName:@"Person"]; + XCTAssertFalse([theFlash isKindOfClass:[TheFlash class]], @"We're living the past."); + + [TheFlash registerSubclass]; + theFlash = [PFObject objectWithClassName:@"Person"]; + XCTAssertTrue([theFlash isKindOfClass:[TheFlash class]], @"In the future, everyone is the Flash."); + XCTAssertEqualObjects(@"The Flash", [(TheFlash*)theFlash flashName], @"The Flash's name should be The Flash, duh."); +} + +- (void)testSubclassesMustHaveTheirParentsParseClassName { + [TheFlash registerSubclass]; + XCTAssertThrows([BarryAllen registerSubclass]); +} + +- (void)testDirtyPointerDetection { + [ClassWithDirtyingConstructor registerSubclass]; + XCTAssertThrows([ClassWithDirtyingConstructor objectWithoutDataWithObjectId:@"NotUsed"], + @"ClassWithDirtyingConstructor has an invalid init method"); + [PFObject unregisterSubclass:[ClassWithDirtyingConstructor class]]; +} + +- (void)testSubclassesCanInheritUtilityClassesWithoutParseClassName { + // Even though this class subclasses a subclass of PFObject and defines + // its own parseClassName, this should succeed because the parent class + // did not define parseClassName + [DescendantOfUtility registerSubclass]; +} + +- (void)testSubclassRegistrationBeforeInitializingParse { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + + [TheFlash registerSubclass]; + + [Parse setApplicationId:@"a" clientKey:@"b"]; + + PFObject *theFlash = [PFObject objectWithClassName:@"Person"]; + PFAssertIsKindOfClass(theFlash, [TheFlash class]); +} + +@end diff --git a/Tests/Unit/ObjectSubclassingControllerTests.m b/Tests/Unit/ObjectSubclassingControllerTests.m new file mode 100644 index 000000000..233b6dc28 --- /dev/null +++ b/Tests/Unit/ObjectSubclassingControllerTests.m @@ -0,0 +1,422 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObject+Subclass.h" +#import "PFObjectPrivate.h" +#import "PFObjectSubclassingController.h" +#import "PFRelation.h" +#import "PFSubclassing.h" +#import "PFUnitTestCase.h" +#import "ParseUnitTests-Swift.h" + +@interface TestSubclass : PFObject +@end + +@interface NotSubclass : PFObject +@end + +@interface PropertySubclass : PFObject { +@public + id _ivarProperty; +} + +@property (nonatomic, assign) int primitiveProperty; +@property (nonatomic, strong) id objectProperty; +@property (nonatomic, strong, readonly) PFRelation *relationProperty; +@property (nonatomic, strong) PFRelation *badRelation; + +@property (nonatomic, strong) id ivarProperty; +@property (nonatomic, copy) id aCopyProperty; + +@property (nonatomic, assign) CGPoint badProperty; + +@end + +@interface BadSubclass : TestSubclass +@end + +@interface GoodSubclass : TestSubclass +@end + +@implementation TestSubclass + ++ (NSString *)parseClassName { + return @"TestSubclass"; +} + +@end + +@implementation NotSubclass + ++ (NSString *)parseClassName { + return @"TestSubclass"; +} + +@end + +@implementation PropertySubclass + +@dynamic primitiveProperty, objectProperty, relationProperty, ivarProperty, aCopyProperty, badProperty, badRelation; + ++ (NSString *)parseClassName { + return @"PropertySubclass"; +} + +- (void)badSelector { + +} + +@end + +@implementation BadSubclass + ++ (NSString *)parseClassName { + return @"Bad"; +} + +@end + +@implementation GoodSubclass +@end + +@interface ObjectSubclassingControllerTests : PFUnitTestCase + +@end + +@implementation ObjectSubclassingControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (void)badSelector { + // To shut the compiler up +} + +- (NSInvocation *)_forwardingInvocationForTarget:(PFObject *)target + selector:(SEL)aSelector + controller:(PFObjectSubclassingController *)controller { + NSMethodSignature *methodSignature = [controller forwardingMethodSignatureForSelector:aSelector + ofClass:[target class]]; + if (methodSignature == nil) { + methodSignature = [target methodSignatureForSelector:aSelector]; + } + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setTarget:target]; + [invocation setSelector:aSelector]; + + return invocation; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructor { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + XCTAssertNotNil(subclassingController); +} + +- (void)testRegister { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [subclassingController registerSubclass:[TestSubclass class]]; + + XCTAssertEqual([TestSubclass class], [subclassingController subclassForParseClassName:@"TestSubclass"]); +} + +- (void)testRegistrationAfterMethodResolution { + PropertySubclass *subclass = [[PropertySubclass alloc] initWithClassName:@"Yolo"]; + XCTAssertNoThrow(subclass.primitiveProperty = 1); + XCTAssertEqual(subclass.primitiveProperty, 1); + + [PropertySubclass registerSubclass]; + XCTAssertEqual(subclass.primitiveProperty, 1); + XCTAssertNoThrow(subclass.primitiveProperty = 2); + XCTAssertEqual(subclass.primitiveProperty, 2); +} + +- (void)testUnregister { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [subclassingController registerSubclass:[TestSubclass class]]; + + XCTAssertEqual([TestSubclass class], [subclassingController subclassForParseClassName:@"TestSubclass"]); + + [subclassingController unregisterSubclass:[TestSubclass class]]; + + XCTAssertNil([subclassingController subclassForParseClassName:@"TestSubclass"]); +} + +- (void)testSubclassingEdgeCases { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [subclassingController registerSubclass:[TestSubclass class]]; + + XCTAssertEqual([TestSubclass class], [subclassingController subclassForParseClassName:@"TestSubclass"]); + XCTAssertThrows([subclassingController registerSubclass:[BadSubclass class]]); + XCTAssertEqual([TestSubclass class], [subclassingController subclassForParseClassName:@"TestSubclass"]); + + XCTAssertNoThrow([subclassingController registerSubclass:[GoodSubclass class]]); + XCTAssertEqual([GoodSubclass class], [subclassingController subclassForParseClassName:@"TestSubclass"]); + + XCTAssertNoThrow([subclassingController registerSubclass:[TestSubclass class]]); + XCTAssertEqual([GoodSubclass class], [subclassingController subclassForParseClassName:@"TestSubclass"]); + + XCTAssertThrows([subclassingController registerSubclass:[NotSubclass class]]); + XCTAssertEqual([GoodSubclass class], [subclassingController subclassForParseClassName:@"TestSubclass"]); +} + +- (void)testForwardingMethodSignature { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [subclassingController registerSubclass:[PropertySubclass class]]; + + XCTAssertEqualObjects([subclassingController forwardingMethodSignatureForSelector:@selector(primitiveProperty) + ofClass:[PropertySubclass class]], + [NSMethodSignature signatureWithObjCTypes:"i@:"]); + + XCTAssertEqualObjects([subclassingController forwardingMethodSignatureForSelector:@selector(setPrimitiveProperty:) + ofClass:[PropertySubclass class]], + [NSMethodSignature signatureWithObjCTypes:"v@:i"]); + + + XCTAssertNil([subclassingController forwardingMethodSignatureForSelector:@selector(badSelector) + ofClass:[PropertySubclass class]]); +} + +- (void)testBadForwarding { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + // Don't register subclass with controller. + [PropertySubclass registerSubclass]; + + PropertySubclass *object = [[PropertySubclass alloc] init]; + + [subclassingController registerSubclass:[PropertySubclass class]]; + NSInvocation *invocation = [self _forwardingInvocationForTarget:object + selector:@selector(badSelector) + controller:subclassingController]; + XCTAssertFalse([subclassingController forwardObjectInvocation:invocation withObject:object]); + + // This will print the warning message to the console, which gives us 100% test coverage! + invocation = [self _forwardingInvocationForTarget:object + selector:@selector(badRelation) + controller:subclassingController]; +} + +- (void)testForwardingGetter { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [PropertySubclass registerSubclass]; + [subclassingController registerSubclass:[PropertySubclass class]]; + +#define AssertInvocationAssertValueEquals(invocation, type, value) ({ \ + type _expected = (value); \ + type _actual; [invocation getReturnValue:&_actual]; \ + XCTAssertEqual(_expected, _actual); \ +}) + + PropertySubclass *target = [[PropertySubclass alloc] init]; + target[@"primitiveProperty"] = @1337; + target[@"objectProperty"] = @"Hello, World!"; + target[@"aCopyProperty"] = [[NSMutableString alloc] initWithString:@"Hello, World!"]; + target[@"badProperty"] = @"Some Value"; + target->_ivarProperty = @8675309; + + NSInvocation *invocation = [self _forwardingInvocationForTarget:target + selector:@selector(primitiveProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + AssertInvocationAssertValueEquals(invocation, int, 1337); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(objectProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + AssertInvocationAssertValueEquals(invocation, __unsafe_unretained id, @"Hello, World!"); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(ivarProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + AssertInvocationAssertValueEquals(invocation, __unsafe_unretained id, target->_ivarProperty); + + target[@"objectProperty"] = [NSNull null]; + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(objectProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + AssertInvocationAssertValueEquals(invocation, __unsafe_unretained id, nil); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(relationProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + __unsafe_unretained PFRelation *returnValue = nil; + [invocation getReturnValue:&returnValue]; + XCTAssertTrue([returnValue isKindOfClass:[PFRelation class]]); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(aCopyProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + __unsafe_unretained NSString *copyPropertyValue = nil; + [invocation getReturnValue:©PropertyValue]; + + // Ensure our mutable string is now immutable. + XCTAssertThrows([(NSMutableString *)copyPropertyValue appendString:@"foo"]); + + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(badProperty) + controller:subclassingController]; + XCTAssertThrows([subclassingController forwardObjectInvocation:invocation withObject:target]); +} + +- (void)testForwardingSetter { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [PropertySubclass registerSubclass]; + [subclassingController registerSubclass:[PropertySubclass class]]; + + PropertySubclass *target = [[PropertySubclass alloc] init]; + + + id objectAgument = nil; + NSInvocation *invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setObjectProperty:) + controller:subclassingController]; + + objectAgument = @"Hello, World!"; + [invocation setArgument:&objectAgument atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertEqualObjects(target[@"objectProperty"], @"Hello, World!"); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setPrimitiveProperty:) + controller:subclassingController]; + [invocation setArgument:&(int) { 1337 } atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertEqualObjects(target[@"primitiveProperty"], @1337); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setIvarProperty:) + controller:subclassingController]; + objectAgument = @8675309; + [invocation setArgument:&objectAgument atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertEqualObjects(target->_ivarProperty, @8675309); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setObjectProperty:) + controller:subclassingController]; + objectAgument = nil; + [invocation setArgument:&objectAgument atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertEqualObjects(target[@"objectProperty"], nil); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setACopyProperty:) + controller:subclassingController]; + objectAgument = [[NSMutableString alloc] initWithString:@"Hello, World!"]; + [invocation setArgument:&objectAgument atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertThrows([target[@"aCopyProperty"] appendString:@"foo"]); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setBadProperty:) + controller:subclassingController]; + [invocation setArgument:&(CGPoint) { 1, 1 } atIndex:2]; + XCTAssertThrows([subclassingController forwardObjectInvocation:invocation withObject:target]); +} + +- (void)testSwiftGetters { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [SwiftSubclass registerSubclass]; + [subclassingController registerSubclass:[SwiftSubclass class]]; + +#define AssertInvocationAssertValueEquals(invocation, type, value) ({ \ + type _expected = (value); \ + type _actual; [invocation getReturnValue:&_actual]; \ + XCTAssertEqual(_expected, _actual); \ +}) + + SwiftSubclass *target = [[SwiftSubclass alloc] init]; + target[@"primitiveProperty"] = @1337; + target[@"objectProperty"] = @"Hello, World!"; + target[@"aCopyProperty"] = [[NSMutableString alloc] initWithString:@"Hello, World!"]; + target[@"badProperty"] = @"Some Value"; + + NSInvocation *invocation = [self _forwardingInvocationForTarget:target + selector:@selector(primitiveProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + AssertInvocationAssertValueEquals(invocation, NSInteger, 1337); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(objectProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + AssertInvocationAssertValueEquals(invocation, __unsafe_unretained id, @"Hello, World!"); + + target[@"objectProperty"] = [NSNull null]; + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(objectProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + AssertInvocationAssertValueEquals(invocation, __unsafe_unretained id, nil); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(relationProperty) + controller:subclassingController]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + __unsafe_unretained PFRelation *returnValue = nil; + [invocation getReturnValue:&returnValue]; + XCTAssertTrue([returnValue isKindOfClass:[PFRelation class]]); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(badProperty) + controller:subclassingController]; + XCTAssertThrows([subclassingController forwardObjectInvocation:invocation withObject:target]); +} + +- (void)testSwiftSetters { + PFObjectSubclassingController *subclassingController = [[PFObjectSubclassingController alloc] init]; + [SwiftSubclass registerSubclass]; + [subclassingController registerSubclass:[SwiftSubclass class]]; + + SwiftSubclass *target = [[SwiftSubclass alloc] init]; + + id objectAgument = nil; + NSInvocation *invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setObjectProperty:) + controller:subclassingController]; + + objectAgument = @"Hello, World!"; + [invocation setArgument:&objectAgument atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertEqualObjects(target[@"objectProperty"], @"Hello, World!"); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setPrimitiveProperty:) + controller:subclassingController]; + [invocation setArgument:&(NSInteger) { 1337 } atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertEqualObjects(target[@"primitiveProperty"], @1337); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setObjectProperty:) + controller:subclassingController]; + objectAgument = nil; + [invocation setArgument:&objectAgument atIndex:2]; + [subclassingController forwardObjectInvocation:invocation withObject:target]; + XCTAssertEqualObjects(target[@"objectProperty"], nil); + + invocation = [self _forwardingInvocationForTarget:target + selector:@selector(setBadProperty:) + controller:subclassingController]; + [invocation setArgument:&(CGPoint) { 1, 1 } atIndex:2]; + XCTAssertThrows([subclassingController forwardObjectInvocation:invocation withObject:target]); +} + +@end diff --git a/Tests/Unit/ObjectUnitTests.m b/Tests/Unit/ObjectUnitTests.m new file mode 100644 index 000000000..2a9e163c3 --- /dev/null +++ b/Tests/Unit/ObjectUnitTests.m @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFObject.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface ObjectUnitTests : PFUnitTestCase + +@end + +@implementation ObjectUnitTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +#pragma mark Constructors + +- (void)testBasicConstructors { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertNotNil([[PFObject alloc] initWithClassName:@"Test"]); + PFAssertThrowsInvalidArgumentException([[PFObject alloc] initWithClassName:nil]); + + XCTAssertNotNil([PFObject objectWithClassName:@"Test"]); + PFAssertThrowsInvalidArgumentException([PFObject objectWithClassName:nil]); + + XCTAssertNotNil([PFObject objectWithoutDataWithClassName:@"Test" objectId:nil]); + XCTAssertNotNil([PFObject objectWithoutDataWithClassName:@"Test" objectId:@"1"]); + PFAssertThrowsInvalidArgumentException([PFObject objectWithoutDataWithClassName:nil objectId:nil]); +#pragma clang diagnostic pop +} + +- (void)testConstructorsWithReservedClassNames { + PFAssertThrowsInvalidArgumentException([[PFObject alloc] initWithClassName:@"_test"]); + PFAssertThrowsInvalidArgumentException([PFObject objectWithClassName:@"_test"]); + PFAssertThrowsInvalidArgumentException([PFObject objectWithoutDataWithClassName:@"_test" objectId:nil]); +} + +- (void)testConstructorFromDictionary { + XCTAssertNotNil([PFObject objectWithClassName:@"Test" dictionary:nil]); + XCTAssertNotNil([PFObject objectWithClassName:@"Test" dictionary:@{}]); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + PFAssertThrowsInvalidArgumentException([PFObject objectWithClassName:nil dictionary:nil]); +#pragma clang diagnostic pop + + PFObject *object = [PFObject objectWithClassName:@"Test" dictionary:@{ @"a" : [NSDate date] }]; + XCTAssertNotNil(object); + + NSString *string = @"foo"; + NSNumber *number = @0.75; + NSDate *date = [NSDate date]; + NSData *data = [@"foo" dataUsingEncoding:NSUTF8StringEncoding]; + NSNull *null = [NSNull null]; + NSDictionary *validDictionary = @{ @"string" : string, + @"number" : number, + @"date" : date, + @"data" : data, + @"null" : null, + @"object" : object }; + PFObject *object2 = [PFObject objectWithClassName:@"Test" dictionary:validDictionary]; + XCTAssertNotNil(object2); + XCTAssertEqualObjects(string, object2[@"string"], @"'string' should be set via constructor"); + XCTAssertEqualObjects(number, object2[@"number"], @"'number' should be set via constructor"); + XCTAssertEqualObjects(date, object2[@"date"], @"'date' should be set via constructor"); + XCTAssertEqualObjects(object, object2[@"object"], @"'object' should be set via constructor"); + XCTAssertEqualObjects(null, object2[@"null"], @"'null' should be set via constructor"); + XCTAssertEqualObjects(data, object2[@"data"], @"'data' should be set via constructor"); + + validDictionary = @{ @"array" : @[ object, object2 ], + @"dictionary" : @{@"bar" : date, @"score" : number} }; + PFObject *object3 = [PFObject objectWithClassName:@"Stuff" dictionary:validDictionary]; + XCTAssertNotNil(object3); + XCTAssertEqualObjects(validDictionary[@"array"], object3[@"array"], @"'array' should be set via constructor"); + XCTAssertEqualObjects(validDictionary[@"dictionary"], object3[@"dictionary"], + @"'dictionary' should be set via constructor"); + + // Dictionary constructor relise on constraints enforced by PFObject -setObject:forKey: + NSDictionary *invalidDictionary = @{ @"1" : @"2", + @YES : @"foo" }; + PFAssertThrowsInvalidArgumentException([PFObject objectWithClassName:@"Test" dictionary:invalidDictionary]); +} + +#pragma mark Accessors + +- (void)testObjectForKey { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + object[@"yarr"] = @"yolo"; + XCTAssertEqualObjects([object objectForKey:@"yarr"], @"yolo"); + XCTAssertEqualObjects(object[@"yarr"], @"yolo"); +} + +- (void)testObjectForUnavailableKey { + PFObject *object = [PFObject objectWithoutDataWithClassName:@"Yarr" objectId:nil]; + PFAssertThrowsInconsistencyException(object[@"yarr"]); +} + +- (void)testSettersWithNilArguments { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + id empty = nil; + + PFAssertThrowsInvalidArgumentException([object setObject:@"foo" forKey:empty]); + PFAssertThrowsInvalidArgumentException([object setObject:@"foo" forKeyedSubscript:empty]); + PFAssertThrowsInvalidArgumentException(object[empty] = @"foo"); + + PFAssertThrowsInvalidArgumentException([object setObject:empty forKey:@"foo"]); + PFAssertThrowsInvalidArgumentException([object setObject:empty forKeyedSubscript:@"foo"]); + PFAssertThrowsInvalidArgumentException(object[@"foo"] = empty); +} + +- (void)testSettersWithInvalidValueTypes { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + + NSSet *set = [NSSet set]; + PFAssertThrowsInvalidArgumentException([object setObject:set forKey:@"foo"]); + PFAssertThrowsInvalidArgumentException([object setObject:set forKeyedSubscript:@"foo"]); + PFAssertThrowsInvalidArgumentException(object[@"foo"] = set); +} + +- (void)testArraySetters { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + + [object addObject:@"yolo" forKey:@"yarr"]; + XCTAssertEqualObjects(object[@"yarr"], @[ @"yolo" ]); + + [object addObjectsFromArray:@[ @"yolo" ] forKey:@"yarr"]; + XCTAssertEqualObjects(object[@"yarr"], (@[ @"yolo", @"yolo" ])); + + [object addUniqueObject:@"yolo" forKey:@"yarrUnique"]; + [object addUniqueObject:@"yolo" forKey:@"yarrUnique"]; + XCTAssertEqualObjects(object[@"yarrUnique"], @[ @"yolo" ]); + + [object addUniqueObjectsFromArray:@[ @"yolo1" ] forKey:@"yarrUnique"]; + [object addUniqueObjectsFromArray:@[ @"yolo", @"yolo1" ] forKey:@"yarrUnique"]; + XCTAssertEqualObjects(object[@"yarrUnique"], (@[ @"yolo", @"yolo1" ])); + + object[@"removableYarr"] = @[ @"yolo" ]; + XCTAssertEqualObjects(object[@"removableYarr"], @[ @"yolo" ]); + + [object removeObject:@"yolo" forKey:@"removableYarr"]; + XCTAssertEqualObjects(object[@"removableYarr"], @[]); + + object[@"removableYarr"] = @[ @"yolo" ]; + [object removeObjectsInArray:@[ @"yolo", @"yolo1" ] forKey:@"removableYarr"]; + XCTAssertEqualObjects(object[@"removableYarr"], @[]); +} + +- (void)testIncrement { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + + [object incrementKey:@"yarr"]; + XCTAssertEqualObjects(object[@"yarr"], @1); + + [object incrementKey:@"yarr" byAmount:@2]; + XCTAssertEqualObjects(object[@"yarr"], @3); + + [object incrementKey:@"yarr" byAmount:@-2]; + XCTAssertEqualObjects(object[@"yarr"], @1); +} + +- (void)testRemoveObjectForKey { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + object[@"yarr"] = @1; + XCTAssertEqualObjects(object[@"yarr"], @1); + + [object removeObjectForKey:@"yarr"]; + XCTAssertNil(object[@"yarr"]); +} + +- (void)testKeyValueCoding { + PFObject *object = [PFObject objectWithClassName:@"Test"]; + [object setValue:@"yolo" forKey:@"yarr"]; + XCTAssertEqualObjects(object[@"yarr"], @"yolo"); + XCTAssertEqualObjects([object valueForKey:@"yarr"], @"yolo"); +} + +@end diff --git a/Tests/Unit/ObjectUtilitiesTests.m b/Tests/Unit/ObjectUtilitiesTests.m new file mode 100644 index 000000000..78ba815a5 --- /dev/null +++ b/Tests/Unit/ObjectUtilitiesTests.m @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFieldOperation.h" +#import "PFObjectUtilities.h" +#import "PFOperationSet.h" +#import "PFTestCase.h" + +@interface ObjectUtilitiesTests : PFTestCase + +@end + +@implementation ObjectUtilitiesTests + +- (void)testApplyFieldOperation { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + id value = [PFObjectUtilities newValueByApplyingFieldOperation:[PFSetOperation setWithValue:@"b"] + toDictionary:dictionary + forKey:@"a"]; + XCTAssertEqualObjects(value, @"b"); + XCTAssertEqualObjects(dictionary, @{ @"a" : @"b" }); +} + +- (void)testApplyOperationSet { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + operationSet[@"a"] = [PFSetOperation setWithValue:@"b"]; + [PFObjectUtilities applyOperationSet:operationSet toDictionary:dictionary]; + XCTAssertEqualObjects(dictionary, @{ @"a" : @"b" }); +} + +- (void)testEquality { + XCTAssertFalse([PFObjectUtilities isObject:nil equalToObject:@"a"]); + XCTAssertFalse([PFObjectUtilities isObject:@"a" equalToObject:nil]); + XCTAssertFalse([PFObjectUtilities isObject:@"a" equalToObject:@"b"]); + XCTAssertFalse([PFObjectUtilities isObject:@"a" equalToObject:[NSDate date]]); + XCTAssertTrue([PFObjectUtilities isObject:nil equalToObject:nil]); + XCTAssertTrue([PFObjectUtilities isObject:@"a" equalToObject:@"a"]); +} + +@end diff --git a/Tests/Unit/OfflineQueryControllerTests.m b/Tests/Unit/OfflineQueryControllerTests.m new file mode 100644 index 000000000..209d34016 --- /dev/null +++ b/Tests/Unit/OfflineQueryControllerTests.m @@ -0,0 +1,540 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import "OCMock+Parse.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFCoreManager.h" +#import "PFMutableQueryState.h" +#import "PFObjectPrivate.h" +#import "PFOfflineQueryController.h" +#import "PFOfflineStore.h" +#import "PFPin.h" +#import "PFPinningObjectStore.h" +#import "PFRelationPrivate.h" +#import "PFUnitTestCase.h" +#import "PFUser.h" + +@interface OfflineQueryControllerTests : PFUnitTestCase + +@end + +@implementation OfflineQueryControllerTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + OCMStub(mockedProvider.offlineStore).andReturn(nil); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + PFOfflineQueryController *offlineQueryController = [[PFOfflineQueryController alloc] initWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + XCTAssertNotNil(offlineQueryController); + XCTAssertEqual((id)offlineQueryController.commonDataSource, mockedProvider); + XCTAssertEqual((id)offlineQueryController.coreDataSource, objectStoreProvider); + + offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + XCTAssertNotNil(offlineQueryController); + XCTAssertEqual((id)offlineQueryController.commonDataSource, mockedProvider); + XCTAssertEqual((id)offlineQueryController.coreDataSource, objectStoreProvider); +} + +- (void)testBadConstructor { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + XCTAssertThrows([(id)[PFOfflineQueryController alloc] initWithCommonDataSource:mockedProvider]); +} + +- (void)testFindObjectsLDS { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + PFOfflineStore *mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + PFPinningObjectStore *pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + PFUser *mockedUser = PFStrictClassMock([PFUser class]); + PFPin *mockedPin = PFStrictClassMock([PFPin class]); + + BFTask *pinTask = [BFTask taskWithResult:mockedPin]; + NSArray *mockedPinnedObjects = @[ @1, @2, @3 ]; + BFTask *resultsTask = [BFTask taskWithResult:mockedPinnedObjects]; + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + queryState.queriesLocalDatastore = YES; + queryState.localDatastorePinName = @"aPinName"; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + + OCMStub([pinningObjectStore fetchPinAsyncWithName:@"aPinName"]).andReturn(pinTask); + OCMStub([mockedOfflineStore findAsyncForQueryState:queryState + user:mockedUser + pin:mockedPin]).andReturn(resultsTask); + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[offlineQueryController findObjectsAsyncForQueryState:queryState + withCancellationToken:nil + user:mockedUser] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(mockedPinnedObjects, task.result); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testFindObjectsLDSCancel { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + PFOfflineStore *mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + PFPinningObjectStore *pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + queryState.queriesLocalDatastore = YES; + queryState.localDatastorePinName = @"aPinName"; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationTokenSource cancel]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[offlineQueryController findObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationTokenSource.token + user:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testFindObjectsRelation { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedObject = PFStrictClassMock([PFObject class]); + id mockedUser = PFStrictClassMock([PFUser class]); + id mockedRelation = PFStrictClassMock([PFRelation class]); + id mockedRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + [queryState setRelationConditionWithObject:mockedObject forKey:@"relationKey"]; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + OCMStub(mockedProvider.commandRunner).andReturn(mockedRunner); + + OCMStub([mockedObject objectId]).andReturn(@"objectId"); + OCMStub([mockedObject parseClassName]).andReturn(@"MyClass"); + + OCMStub([mockedObject isDataAvailableForKey:@"relationKey"]).andReturn(YES); + OCMStub(mockedObject[@"relationKey"]).andReturn(mockedRelation); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + OCMExpect([mockedRelation _addKnownObject:[OCMArg isKindOfClass:[PFObject class]]]); + OCMExpect([mockedOfflineStore updateDataForObjectAsync:[OCMArg isKindOfClass:[PFObject class]]]) + .andDo(^(NSInvocation *invocation) { + // Grab the argument passed in + __unsafe_unretained id arg = nil; + [invocation getArgument:&arg atIndex:2]; + + // Create a task from it. + __autoreleasing BFTask *resultTask = [BFTask taskWithResult:arg]; + [invocation setReturnValue:&resultTask]; + }); + + NSDictionary *result = @{ @"results" : @[ @{@"className" : @"Yolo", + @"name" : @"yarr", + @"objectId" : @"abc", + @"job" : @"pirate"} ], + @"count" : @5 }; + [mockedRunner mockCommandResult:result forCommandsPassingTest:^BOOL(id obj) { + return YES; + }]; + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[offlineQueryController findObjectsAsyncForQueryState:queryState + withCancellationToken:nil + user:mockedUser] continueWithBlock:^id(BFTask *task) { + NSArray *results = task.result; + + XCTAssertNotNil(results); + XCTAssertEqual(1, results.count); + + PFObject *object = [results firstObject]; + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"abc"); + XCTAssertEqualObjects(object[@"name"], @"yarr"); + XCTAssertEqualObjects(object[@"job"], @"pirate"); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + OCMVerifyAll(mockedRelation); + OCMVerifyAll(mockedOfflineStore); +} + +- (void)testFindObjectsRelationCancel { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedObject = PFStrictClassMock([PFObject class]); + id mockedUser = PFStrictClassMock([PFUser class]); + id mockedRelation = PFStrictClassMock([PFRelation class]); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + [queryState setRelationConditionWithObject:mockedObject forKey:@"relationKey"]; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + + OCMStub([mockedObject objectId]).andReturn(@"objectId"); + OCMStub([mockedObject parseClassName]).andReturn(@"MyClass"); + + OCMStub([mockedObject isDataAvailableForKey:@"relationKey"]).andReturn(YES); + OCMStub(mockedObject[@"relationKey"]).andReturn(mockedRelation); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationTokenSource cancel]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[offlineQueryController findObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationTokenSource.token + user:mockedUser] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testFindObjectsNormal { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedUser = PFStrictClassMock([PFUser class]); + id mockedRelation = PFStrictClassMock([PFRelation class]); + id mockedRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + OCMStub(mockedProvider.commandRunner).andReturn(mockedRunner); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + OCMExpect([mockedRelation _addKnownObject:[OCMArg isKindOfClass:[PFObject class]]]); + OCMExpect([mockedOfflineStore updateDataForObjectAsync:[OCMArg isKindOfClass:[PFObject class]]]) + .andDo(^(NSInvocation *invocation) { + // Grab the argument passed in + __unsafe_unretained id arg = nil; + [invocation getArgument:&arg atIndex:2]; + + // Create a task from it. + __autoreleasing BFTask *resultTask = [BFTask taskWithResult:arg]; + [invocation setReturnValue:&resultTask]; + }); + + NSDictionary *result = @{ @"results" : @[ @{@"className" : @"Yolo", + @"name" : @"yarr", + @"objectId" : @"abc", + @"job" : @"pirate"} ], + @"count" : @5 }; + [mockedRunner mockCommandResult:result forCommandsPassingTest:^BOOL(id obj) { + return YES; + }]; + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[offlineQueryController findObjectsAsyncForQueryState:queryState + withCancellationToken:nil + user:mockedUser] continueWithBlock:^id(BFTask *task) { + NSArray *results = task.result; + + XCTAssertNotNil(results); + XCTAssertEqual(1, results.count); + + PFObject *object = [results firstObject]; + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"abc"); + XCTAssertEqualObjects(object[@"name"], @"yarr"); + XCTAssertEqualObjects(object[@"job"], @"pirate"); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testFindObjectsNormalCancel { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedUser = PFStrictClassMock([PFUser class]); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationTokenSource cancel]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[offlineQueryController findObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationTokenSource.token + user:mockedUser] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testCountObjectsLDS { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedObject = PFStrictClassMock([PFObject class]); + id mockedUser = PFStrictClassMock([PFUser class]); + id mockedPin = PFStrictClassMock([PFPin class]); + + BFTask *mockedCountPinTask = [BFTask taskWithResult:@1337]; + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + queryState.queriesLocalDatastore = YES; + queryState.localDatastorePinName = @"aPinName"; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + + OCMStub([mockedObject objectId]).andReturn(@"objectId"); + OCMStub([mockedObject parseClassName]).andReturn(@"MyClass"); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + OCMStub([pinningObjectStore fetchPinAsyncWithName:@"aPinName"]).andReturn(mockedPin); + + OCMStub([mockedOfflineStore countAsyncForQueryState:queryState + user:mockedUser + pin:mockedPin]).andReturn(mockedCountPinTask); + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[offlineQueryController countObjectsAsyncForQueryState:queryState + withCancellationToken:nil + user:mockedUser] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @1337); + + [expectation fulfill]; + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testCountObjectsLDSCancel { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedObject = PFStrictClassMock([PFObject class]); + id mockedUser = PFStrictClassMock([PFUser class]); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + queryState.queriesLocalDatastore = YES; + queryState.localDatastorePinName = @"aPinName"; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + + OCMStub([mockedObject objectId]).andReturn(@"objectId"); + OCMStub([mockedObject parseClassName]).andReturn(@"MyClass"); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationTokenSource cancel]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[offlineQueryController countObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationTokenSource.token + user:mockedUser] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + + [expectation fulfill]; + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testCountObjectsNormal { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedUser = PFStrictClassMock([PFUser class]); + id mockedRelation = PFStrictClassMock([PFRelation class]); + id mockedRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + OCMStub(mockedProvider.commandRunner).andReturn(mockedRunner); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + OCMExpect([mockedRelation _addKnownObject:[OCMArg isKindOfClass:[PFObject class]]]); + OCMExpect([mockedOfflineStore updateDataForObjectAsync:[OCMArg isKindOfClass:[PFObject class]]]) + .andDo(^(NSInvocation *invocation) { + // Grab the argument passed in + __unsafe_unretained id arg = nil; + [invocation getArgument:&arg atIndex:2]; + + // Create a task from it. + __autoreleasing BFTask *resultTask = [BFTask taskWithResult:arg]; + [invocation setReturnValue:&resultTask]; + }); + + NSDictionary *result = @{ @"results" : @[ @{@"className" : @"Yolo", + @"name" : @"yarr", + @"objectId" : @"abc", + @"job" : @"pirate"} ], + @"count" : @5 }; + [mockedRunner mockCommandResult:result forCommandsPassingTest:^BOOL(id obj) { + return YES; + }]; + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[offlineQueryController countObjectsAsyncForQueryState:queryState + withCancellationToken:nil + user:mockedUser] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(@5, task.result); + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testCountObjectsNormalCancel { + id mockedProvider = PFStrictProtocolMock(@protocol(PFCoreManagerDataSource)); + id objectStoreProvider = PFStrictProtocolMock(@protocol(PFPinningObjectStoreProvider)); + + id mockedOfflineStore = PFStrictClassMock([PFOfflineStore class]); + id pinningObjectStore = PFStrictClassMock([PFPinningObjectStore class]); + id mockedUser = PFStrictClassMock([PFUser class]); + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"ClassName"]; + + OCMStub(objectStoreProvider.pinningObjectStore).andReturn(pinningObjectStore); + OCMStub(mockedProvider.offlineStore).andReturn(mockedOfflineStore); + + OCMStub([mockedUser sessionToken]).andReturn(@"sessionToken"); + + PFOfflineQueryController *offlineQueryController = [PFOfflineQueryController controllerWithCommonDataSource:mockedProvider + coreDataSource:objectStoreProvider]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationTokenSource cancel]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[offlineQueryController countObjectsAsyncForQueryState:queryState + withCancellationToken:cancellationTokenSource.token + user:mockedUser] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/OfflineQueryLogicUnitTests.m b/Tests/Unit/OfflineQueryLogicUnitTests.m new file mode 100644 index 000000000..f97e88595 --- /dev/null +++ b/Tests/Unit/OfflineQueryLogicUnitTests.m @@ -0,0 +1,1570 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "BFTask+Private.h" +#import "PFObjectPrivate.h" +#import "PFOfflineQueryLogic.h" +#import "PFQueryPrivate.h" +#import "PFSQLiteDatabase.h" +#import "PFUnitTestCase.h" + +@interface OfflineQueryLogicUnitTests : PFUnitTestCase { + PFUser *_user; +} + +@end + +@implementation OfflineQueryLogicUnitTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [PFUser registerSubclass]; + _user = [PFUser user]; +} + +- (void)tearDown { + [PFObject unregisterSubclass:[PFUser class]]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testQueryEqual { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + BFTask *task = [BFTask taskWithResult:nil]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" equalTo:@"bar"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" equalTo:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" equalTo:@"bar"]; + [query whereKey:@"sum" equalTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + // Check double + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" equalTo:@"bar"]; + [query whereKey:@"sum" equalTo:@1337.0]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + // Check float + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" equalTo:@"bar"]; + [query whereKey:@"sum" equalTo:@1337.0f]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" equalTo:@"bar"]; + [query whereKey:@"sum" equalTo:@101]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryNotEqual { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + BFTask *task = [BFTask taskWithResult:nil]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" notEqualTo:@"bar"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" notEqualTo:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" notEqualTo:@"bar"]; + [query whereKey:@"sum" notEqualTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" notEqualTo:@"bar"]; + [query whereKey:@"sum" notEqualTo:@101]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" notEqualTo:@"gundam"]; + [query whereKey:@"sum" notEqualTo:@101]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryLessThan { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"today"] = [NSDate dateWithTimeIntervalSince1970:1337]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"foo" lessThan:@"bar"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThan:@"barz"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThan:@"appa yip yip"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" lessThan:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" lessThan:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" lessThan:@2331]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" lessThan:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + // while Date in PFObject is vanila NSDate. Is this problem also exists in Android? + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" lessThan:[NSDate dateWithTimeIntervalSince1970:1337]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" lessThan:[NSDate dateWithTimeIntervalSince1970:2133]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThan:@"appa yip yip"]; + [query whereKey:@"sum" lessThan:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThan:@"gokil"]; + [query whereKey:@"sum" lessThan:@3333]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryLessThanEqual { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"today"] = [NSDate dateWithTimeIntervalSince1970:1337]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"foo" lessThanOrEqualTo:@"bar"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThanOrEqualTo:@"barz"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThanOrEqualTo:@"appa yip yip"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" lessThanOrEqualTo:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" lessThanOrEqualTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" lessThanOrEqualTo:@2331]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" lessThanOrEqualTo:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" lessThanOrEqualTo:[NSDate dateWithTimeIntervalSince1970:1337]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" lessThanOrEqualTo:[NSDate dateWithTimeIntervalSince1970:2133]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThanOrEqualTo:@"appa yip yip"]; + [query whereKey:@"sum" lessThanOrEqualTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" lessThanOrEqualTo:@"gokil"]; + [query whereKey:@"sum" lessThanOrEqualTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryGreaterThan { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"today"] = [NSDate dateWithTimeIntervalSince1970:1337]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"foo" greaterThan:@"bar"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThan:@"barz"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThan:@"appa yip yip"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" greaterThan:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" greaterThan:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" greaterThan:@1331]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" greaterThan:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" greaterThan:[NSDate dateWithTimeIntervalSince1970:1337]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" greaterThan:[NSDate dateWithTimeIntervalSince1970:133]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThan:@"appa yip yip"]; + [query whereKey:@"sum" greaterThan:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThan:@"appa yip yip"]; + [query whereKey:@"sum" greaterThan:@1331]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryGreaterThanEqual { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"today"] = [NSDate dateWithTimeIntervalSince1970:1337]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"foo" greaterThanOrEqualTo:@"bar"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThanOrEqualTo:@"barz"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThanOrEqualTo:@"appa yip yip"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" greaterThanOrEqualTo:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" greaterThanOrEqualTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" greaterThanOrEqualTo:@1331]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" greaterThanOrEqualTo:@"1337"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" greaterThanOrEqualTo:[NSDate dateWithTimeIntervalSince1970:1337]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"today" greaterThanOrEqualTo:[NSDate dateWithTimeIntervalSince1970:133]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThanOrEqualTo:@"appa yip yip"]; + [query whereKey:@"sum" greaterThanOrEqualTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" greaterThanOrEqualTo:@"gokil"]; + [query whereKey:@"sum" greaterThanOrEqualTo:@1337]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryIn { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"ArrezTheGodOfWar"] = @[@"bar", @1337]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"foo" containedIn:@[@"bar", @"bir", @"barz"]]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" containedIn:@[@"ber", @YES, @"barz"]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" containedIn:@[@"1337", @123, @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" containedIn:@[@1337, @123, @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" containedIn:@[@1337, @"bar", @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" containedIn:@[@1337, @"barz", @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" containedIn:@[@"1337", @"barz", @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryNotIn { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"ArrezTheGodOfWar"] = @[@"bar", @1337]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"foo" notContainedIn:@[@"bar", @"bir", @"barz"]]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" notContainedIn:@[@"ber", @YES, @"barz"]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" notContainedIn:@[@"1337", @123, @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" notContainedIn:@[@1337, @123, @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" notContainedIn:@[@1337, @"bar", @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" notContainedIn:@[@1337, @"barz", @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" notContainedIn:@[@"1337", @"barz", @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryAll { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"ArrezTheGodOfWar"] = @[@"bar", @1337, @"awesome"]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"foo" containsAllObjectsInArray:@[@"bar"]]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"foo" containsAllObjectsInArray:@[@"bar", @YES, @"barz"]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"sum" containsAllObjectsInArray:@[@1337]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" containsAllObjectsInArray:@[@1337, @"bar", @456]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" containsAllObjectsInArray:@[@1337, @"bar", @"awesome", @"more awesome"]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"ArrezTheGodOfWar" containsAllObjectsInArray:@[@1337, @"bar"]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryRegex { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"barney"] = @"barney stinson"; + object[@"stinson"] = @"stinson"; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"barney" matchesRegex:@"stinson"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"stinson" matchesRegex:@"stinson"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"stinson" matchesRegex:@"barney"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryRegexWithModifier { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"barney"] = @"barney stinson"; + object[@"stinson"] = @"stinson"; + object[@"GreatMaster"] = @"Stinson"; + object[@"SomethingWithNewline"] = @"Something\nwith\nnewline"; + object[@"dika"] = @"Gandira Putra Prahandika"; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"stinson" matchesRegex:@"stinson" modifiers:@"i"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"GreatMaster" matchesRegex:@"stinson" modifiers:@"i"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"GreatMaster" matchesRegex:@"stinsonz" modifiers:@"i"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"^newline$" modifiers:nil]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"^newline$" modifiers:@"m"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"^Newline$" modifiers:@"im"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"^Newline$" modifiers:@"m"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"^Newline$" modifiers:@"i"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"with.*newline" modifiers:nil]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"with.*newline" modifiers:@"s"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"SomethingWithNewline" matchesRegex:@"with.*Newline" modifiers:@"is"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"dika" matchesRegex:@"Pu tra .*dika" modifiers:nil]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"dika" matchesRegex:@"Pu tra.*dika" modifiers:@"x"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryExists { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = [NSNull null]; + object[@"ArrezTheGodOfWar"] = @[ ]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKeyExists:@"foo"]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKeyExists:@"sum"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKeyExists:@"ArrezTheGodOfWar"]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryNearSphere { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"point1"] = [PFGeoPoint geoPointWithLatitude:10 longitude:10]; + object[@"point2"] = [PFGeoPoint geoPointWithLatitude:70 longitude:70]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"point1" nearGeoPoint:[PFGeoPoint geoPointWithLatitude:15 longitude:15] withinRadians:50]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"point2" nearGeoPoint:[PFGeoPoint geoPointWithLatitude:-15 longitude:-15] withinRadians:1]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryWithin { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"point1"] = [PFGeoPoint geoPointWithLatitude:10 longitude:10]; + object[@"point2"] = [PFGeoPoint geoPointWithLatitude:70 longitude:70]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + BFTask *task = [BFTask taskWithResult:nil]; + + [query whereKey:@"point1" withinGeoBoxFromSouthwest:[PFGeoPoint geoPointWithLatitude:5 longitude:5] + toNortheast:[PFGeoPoint geoPointWithLatitude:15 longitude:15]]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"point2" withinGeoBoxFromSouthwest:[PFGeoPoint geoPointWithLatitude:5 longitude:5] + toNortheast:[PFGeoPoint geoPointWithLatitude:15 longitude:15]]; + matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testQueryOr { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + PFSQLiteDatabase *database = [[PFSQLiteDatabase alloc] init]; + + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"foo"] = @"bar"; + object[@"sum"] = @1337; + object[@"ArrezTheGodOfWar"] = @[@"bar", @1337]; + PFQuery *query = nil; + BFTask *task = [BFTask taskWithResult:nil]; + + PFQuery *query1 = [PFQuery queryWithClassName:@"Object"]; + [query1 whereKey:@"foo" containedIn:@[@"bar", @"bir", @"barz"]]; + PFQuery *query2 = [PFQuery queryWithClassName:@"Object"]; + [query2 whereKey:@"foo" containedIn:@[@123, @456, @"barz"]]; + query = [PFQuery orQueryWithSubqueries:@[query1, query2]]; + PFConstraintMatcherBlock matcherBlock = [logic createMatcherForQueryState:query.state user:_user]; + + // Check matcher + task = [[task continueWithBlock:^id(BFTask *task) { + return matcherBlock(object, database); + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return nil; + }]; + + [task waitUntilFinished]; +} + +- (void)testSortDate { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + + NSMutableArray *objects = [NSMutableArray array]; + for (int i = 0; i < 10; ++i) { + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"num"] = @(10 - i); + [objects addObject:object]; + } + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + [query orderByAscending:@"createdAt"]; + + NSArray *sorted = [logic resultsByApplyingOptions:PFOfflineQueryOptionOrder + ofQueryState:query.state + toResults:objects]; + for (int i = 0; i < 10; ++i) { + XCTAssertEqual(10 - i, [sorted[i][@"num"] intValue]); + } +} + +- (void)testSortNumber { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + + NSMutableArray *objects = [NSMutableArray array]; + for (int i = 0; i < 10; ++i) { + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"num"] = @(10 - i); + [objects addObject:object]; + } + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + [query orderByAscending:@"num"]; + + NSArray *sorted = [logic resultsByApplyingOptions:PFOfflineQueryOptionOrder + ofQueryState:query.state + toResults:objects]; + for (int i = 0; i < 10; ++i) { + XCTAssertEqual(i + 1, [sorted[i][@"num"] intValue]); + } +} + +- (void)testSortNumberDescending { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + + NSMutableArray *objects = [NSMutableArray array]; + for (int i = 0; i < 10; ++i) { + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"num"] = @(i); + [objects addObject:object]; + } + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + [query orderByAscending:@"-num"]; + + NSArray *sorted = [logic resultsByApplyingOptions:PFOfflineQueryOptionOrder + ofQueryState:query.state + toResults:objects]; + for (int i = 0; i < 10; ++i) { + XCTAssertEqual(9 - i, [sorted[i][@"num"] intValue]); + } +} + +- (void)testSortGeoPoint { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + + NSMutableArray *objects = [NSMutableArray array]; + for (int i = 1; i <= 10; ++i) { + PFObject *object = [PFObject objectWithClassName:@"Object"]; + object[@"point"] = [PFGeoPoint geoPointWithLatitude:10 longitude:i]; + object[@"order"] = @(10 - i); + [objects addObject:object]; + } + PFGeoPoint *origin = [PFGeoPoint geoPointWithLatitude:10 longitude:0]; + PFQuery *query = [PFQuery queryWithClassName:@"Object"]; + [query whereKey:@"point" nearGeoPoint:origin]; + [query orderByAscending:@"order"]; + + NSArray *sorted = [logic resultsByApplyingOptions:PFOfflineQueryOptionOrder + ofQueryState:query.state + toResults:objects]; + // It should not care about order. Instead it should sort based on how near it is to origin + for (int i = 0; i < 10; ++i) { + XCTAssertEqual(9 - i, [sorted[i][@"order"] intValue]); + } +} + +- (void)testUnderLimit { + PFOfflineQueryLogic *logic = [[PFOfflineQueryLogic alloc] init]; + + NSArray *results = @[ [PFObject objectWithClassName:@"Test"] ]; + + PFQuery *query = [PFQuery queryWithClassName:@"Test"]; + query.limit = 25; + + NSArray *strippedArray = [logic resultsByApplyingOptions:PFOfflineQueryOptionLimit + ofQueryState:query.state + toResults:results]; + XCTAssertEqual(results.count, strippedArray.count); +} + +@end diff --git a/Tests/Unit/OperationSetUnitTests.m b/Tests/Unit/OperationSetUnitTests.m new file mode 100644 index 000000000..18f0df6b2 --- /dev/null +++ b/Tests/Unit/OperationSetUnitTests.m @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDecoder.h" +#import "PFFieldOperation.h" +#import "PFOperationSet.h" +#import "PFTestCase.h" + +@interface OperationSetUnitTests : PFTestCase + +@end + +@implementation OperationSetUnitTests + +- (PFOperationSet *)makeTestOperationSet { + PFOperationSet *result = [[PFOperationSet alloc] init]; + result[@"witch"] = [PFSetOperation setWithValue:@"doctor"]; + result[@"something"] = [[PFDeleteOperation alloc] init]; + result.saveEventually = YES; + return result; +} + +- (void)testMergeOperationSet { + PFOperationSet *operation1 = [[PFOperationSet alloc] init]; + PFOperationSet *operation2 = [self makeTestOperationSet]; + + // Check if we can override existing object + operation1[@"witch"] = [[PFDeleteOperation alloc] init]; + // Check if previous objects still exist when merged + operation1[@"bar"] = [PFSetOperation setWithValue:@"foo"]; + + // MERGE!! *insert awesome BGM here* + [operation1 mergeOperationSet:operation2]; + + // Should be merged with operation2 + PFAssertIsKindOfClass(operation1[@"witch"], PFDeleteOperation); + + // Should exist from operation1 + PFAssertIsKindOfClass(operation1[@"bar"], PFSetOperation); + + // Should exist from operation2 + PFAssertIsKindOfClass(operation1[@"something"], PFDeleteOperation); +} + +- (void)testOperationSetWithREST { + PFOperationSet *operation1 = [self makeTestOperationSet]; + // Use default PFEncoding + NSArray *operationSetUUIDs = nil; + NSDictionary *restified = [operation1 RESTDictionaryUsingObjectEncoder:[PFPointerObjectEncoder objectEncoder] + operationSetUUIDs:&operationSetUUIDs]; + // Check returned operationSetUUIDs + XCTAssertNotNil(operationSetUUIDs); + PFAssertEqualInts(1, operationSetUUIDs.count); + XCTAssertEqualObjects(operation1.uuid, operationSetUUIDs[0]); + // Use default PFDecoder + PFOperationSet *operation2 = [PFOperationSet operationSetFromRESTDictionary:restified + usingDecoder:[[PFDecoder alloc] init]]; + + // Make sure they're equal + NSEnumerator *keyEnumerator1 = [operation1 keyEnumerator]; + for (id key in keyEnumerator1) { + PFAssertIsKindOfClass(operation1[key], operation2[key]); + } + NSEnumerator *keyEnumerator2 = [operation1 keyEnumerator]; + for (id key in keyEnumerator2) { + PFAssertIsKindOfClass(operation1[key], operation2[key]); + } + XCTAssertEqual(operation1.uuid, operation2.uuid); + XCTAssertEqual(operation1.saveEventually, operation2.saveEventually); +} + +- (void)testGetterAndSetter { + PFOperationSet *testOp = [[PFOperationSet alloc] init]; + id setOp = [PFSetOperation setWithValue:@"doctor"]; + id deleteOp = [[PFDeleteOperation alloc] init]; + [testOp setObject:setOp forKey:@"witch"]; + testOp[@"something"] = deleteOp; + + XCTAssertEqual(setOp, [testOp objectForKey:@"witch"]); + XCTAssertEqual(setOp, testOp[@"witch"]); + + XCTAssertEqual(deleteOp, [testOp objectForKey:@"something"]); + XCTAssertEqual(deleteOp, testOp[@"something"]); +} + +- (void)testRemoveObjectForKey { + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + + PFFieldOperation *operation = [PFSetOperation setWithValue:@"yolo"]; + operationSet[@"yarr"] = operation; + + XCTAssertEqual(operationSet[@"yarr"], operation); + + NSDate *date = operationSet.updatedAt; + [operationSet removeObjectForKey:@"yarr"]; + XCTAssertNil(operationSet[@"yarr"]); + XCTAssertNotEqualObjects(date, operationSet.updatedAt); +} + +- (void)testCopying { + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + operationSet[@"yarr"] = [PFSetOperation setWithValue:@"yolo"]; + + PFOperationSet *operationSetCopy = [operationSet copy]; + XCTAssertEqualObjects(operationSet.uuid, operationSetCopy.uuid); + XCTAssertEqualObjects(operationSet.updatedAt, operationSetCopy.updatedAt); + XCTAssertEqual(operationSet.saveEventually, operationSetCopy.saveEventually); + XCTAssertEqualObjects(operationSet[@"yarr"], operationSetCopy[@"yarr"]); +} + +- (void)testFastEnumeration { + PFOperationSet *operationSet = [[PFOperationSet alloc] init]; + operationSet[@"yarr1"] = [PFSetOperation setWithValue:@"yolo"]; + operationSet[@"yarr2"] = [PFSetOperation setWithValue:@"yolo"]; + + NSMutableArray *keys = [NSMutableArray array]; + for (NSString *key in operationSet) { + [keys addObject:key]; + } + XCTAssertEqualObjects(keys, (@[ @"yarr1", @"yarr2" ])); +} + +@end diff --git a/Tests/Unit/ParseModuleUnitTests.m b/Tests/Unit/ParseModuleUnitTests.m new file mode 100644 index 000000000..de7e2c85b --- /dev/null +++ b/Tests/Unit/ParseModuleUnitTests.m @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestCase.h" +#import "ParseInternal.h" + +@interface ParseTestModule : NSObject + +@property (nonatomic, assign) BOOL didInitializeCalled; + +@end + +@implementation ParseTestModule + +- (void)parseDidInitializeWithApplicationId:(NSString *)applicationId clientKey:(NSString *)clientKey { + self.didInitializeCalled = YES; +} + +@end + +@interface ParseModuleUnitTests : PFTestCase + +@end + +@implementation ParseModuleUnitTests + +- (void)testModuleSelectors { + ParseModuleCollection *collection = [[ParseModuleCollection alloc] init]; + + ParseTestModule *module = [[ParseTestModule alloc] init]; + [collection addParseModule:module]; + + [collection parseDidInitializeWithApplicationId:nil clientKey:nil]; + + // Spin the run loop, as the delegate messages are being called on the main thread + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + + XCTAssertTrue(module.didInitializeCalled, @"Did initialize method should be called on a module."); +} + +- (void)testWeakModuleReference { + ParseModuleCollection *collection = [[ParseModuleCollection alloc] init]; + + @autoreleasepool { + ParseTestModule *module = [[ParseTestModule alloc] init]; + [collection addParseModule:module]; + } + + [collection parseDidInitializeWithApplicationId:nil clientKey:nil]; + XCTAssertEqual([collection modulesCount], 0, @"Module should be removed from the collection."); +} + +- (void)testModuleRemove { + ParseModuleCollection *collection = [[ParseModuleCollection alloc] init]; + + ParseTestModule *moduleA = [[ParseTestModule alloc] init]; + ParseTestModule *moduleB = [[ParseTestModule alloc] init]; + + [collection addParseModule:moduleA]; + [collection addParseModule:moduleB]; + + [collection removeParseModule:moduleA]; + + XCTAssertTrue([collection containsModule:moduleB]); + XCTAssertFalse([collection containsModule:moduleA]); + XCTAssertEqual([collection modulesCount], 1, @"Module should be removed from the collection"); +} + +- (void)testNilModule { + ParseModuleCollection *collection = [[ParseModuleCollection alloc] init]; + + XCTAssertNoThrow([collection addParseModule:nil]); + XCTAssertEqual([collection modulesCount], 0); + XCTAssertNoThrow([collection removeParseModule:nil]); + XCTAssertEqual([collection modulesCount], 0); +} + +@end diff --git a/Tests/Unit/ParseSetupUnitTests.m b/Tests/Unit/ParseSetupUnitTests.m new file mode 100644 index 000000000..db10ae946 --- /dev/null +++ b/Tests/Unit/ParseSetupUnitTests.m @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFTestCase.h" +#import "Parse_Private.h" + +@interface ParseSetupUnitTests : PFTestCase + +@end + +@implementation ParseSetupUnitTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)tearDown { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testLDSEnabledWithoutInitializer { + XCTAssertFalse([Parse isLocalDatastoreEnabled]); + [Parse enableLocalDatastore]; + + XCTAssertTrue([Parse isLocalDatastoreEnabled]); + [Parse setApplicationId:@"a" clientKey:@"b"]; + + XCTAssertTrue([Parse isLocalDatastoreEnabled]); +} + +- (void)testInitializeWithLDSAfterInitializeShouldThrowException { + [Parse setApplicationId:@"a" clientKey:@"b"]; + PFAssertThrowsInconsistencyException([Parse enableLocalDatastore]); +} +@end diff --git a/Tests/Unit/PinUnitTests.m b/Tests/Unit/PinUnitTests.m new file mode 100644 index 000000000..1f2ba0679 --- /dev/null +++ b/Tests/Unit/PinUnitTests.m @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPin.h" +#import "PFTestCase.h" +#import "Parse_Private.h" + +@interface PinUnitTests : PFTestCase + +@end + +@implementation PinUnitTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [Parse enableLocalDatastore]; + [Parse setApplicationId:@"a" clientKey:@"b"]; +} + +- (void)tearDown { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testAccessors { + PFPin *pin = [[PFPin alloc] init]; + + pin.name = @"Matcha"; + pin.objects = [@[ @"Green Tea" ] mutableCopy]; + + XCTAssertEqualObjects(@"Matcha", pin.name); + XCTAssertEqualObjects(@"Green Tea", pin.objects[0]); +} + +@end diff --git a/Tests/Unit/PinningObjectStoreTests.m b/Tests/Unit/PinningObjectStoreTests.m new file mode 100644 index 000000000..9f6267c5a --- /dev/null +++ b/Tests/Unit/PinningObjectStoreTests.m @@ -0,0 +1,311 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "BFTask+Private.h" +#import "PFOfflineStore.h" +#import "PFPin.h" +#import "PFPinningObjectStore.h" +#import "PFUnitTestCase.h" + +@interface PinningObjectStoreTests : PFUnitTestCase + +@end + +@implementation PinningObjectStoreTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFOfflineStoreProvider)); + PFOfflineStore *store = PFStrictClassMock([PFOfflineStore class]); + OCMStub(dataSource.offlineStore).andReturn(store); + return dataSource; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id dataSource = [self mockedDataSource]; + + PFPinningObjectStore *store = [[PFPinningObjectStore alloc] initWithDataSource:dataSource]; + XCTAssertNotNil(dataSource); + XCTAssertEqual((id)store.dataSource, dataSource); + + store = [PFPinningObjectStore storeWithDataSource:dataSource]; + XCTAssertNotNil(dataSource); + XCTAssertEqual((id)store.dataSource, dataSource); +} + +- (void)testFetchPin { + id dataSource = [self mockedDataSource]; + + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + PFOfflineStore *offlineStore = dataSource.offlineStore; + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[pin]]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store fetchPinAsyncWithName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, pin); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFetchPinCaching { + id dataSource = [self mockedDataSource]; + + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + PFOfflineStore *offlineStore = dataSource.offlineStore; + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[pin]]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[[store fetchPinAsyncWithName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, pin); + return [store fetchPinAsyncWithName:@"Yolo"]; + }] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, pin); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFetchNewPin { + id dataSource = [self mockedDataSource]; + + PFOfflineStore *offlineStore = dataSource.offlineStore; + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[]]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store fetchPinAsyncWithName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + PFPin *pin = task.result; + XCTAssertEqualObjects(pin.name, @"Yolo"); + XCTAssertNil(pin.objects); + [expectation fulfill]; + return [store fetchPinAsyncWithName:@"Yolo"]; + }]; + [self waitForTestExpectations]; +} + +- (void)testPinObjects { + id dataSource = [self mockedDataSource]; + id offlineStore = dataSource.offlineStore; + + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[ pin ]]]; + [OCMExpect([offlineStore saveObjectLocallyAsync:pin includeChildren:YES]) andReturn:[BFTask taskWithResult:nil]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store pinObjectsAsync:@[ object ] withPinName:@"Yolo" includeChildren:YES] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + XCTAssertEqualObjects(pin.objects, @[ object ]); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(offlineStore); +} + +- (void)testPinObjectsExistingPin { + id dataSource = [self mockedDataSource]; + id offlineStore = dataSource.offlineStore; + + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + + PFObject *existingObject = [PFObject objectWithClassName:@"Yarr"]; + pin.objects = [@[ existingObject ] mutableCopy]; + + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[ pin ]]]; + [OCMExpect([offlineStore saveObjectLocallyAsync:pin includeChildren:YES]) andReturn:[BFTask taskWithResult:nil]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store pinObjectsAsync:@[ object ] withPinName:@"Yolo" includeChildren:YES] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + XCTAssertEqualObjects(pin.objects, (@[ existingObject, object ])); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(offlineStore); +} + +- (void)testPinZeroObjects { + id dataSource = [self mockedDataSource]; + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store pinObjectsAsync:nil withPinName:@"Yolo" includeChildren:YES] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testPinObjectsWithoutChildren { + id dataSource = [self mockedDataSource]; + id offlineStore = dataSource.offlineStore; + + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[ pin ]]]; + [OCMExpect([offlineStore saveObjectLocallyAsync:pin withChildren:[OCMArg checkWithBlock:^BOOL(id obj) { + return ([obj count] == 1); + }]]) andReturn:[BFTask taskWithResult:nil]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store pinObjectsAsync:@[ object ] withPinName:@"Yolo" includeChildren:NO] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + XCTAssertEqualObjects(pin.objects, (@[ object ])); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(offlineStore); +} + +- (void)testUnpinObjects { + id dataSource = [self mockedDataSource]; + id offlineStore = dataSource.offlineStore; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + pin.objects = [@[ object, [PFObject objectWithClassName:@"Yarr"] ] mutableCopy]; + + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[ pin ]]]; + [OCMExpect([offlineStore saveObjectLocallyAsync:pin includeChildren:YES]) andReturn:[BFTask taskWithResult:nil]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store unpinObjectsAsync:@[ object ] withPinName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(offlineStore); +} + +- (void)testUnpinObjectsEmptyPin { + id dataSource = [self mockedDataSource]; + id offlineStore = dataSource.offlineStore; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + pin.objects = [@[ object ] mutableCopy]; + + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[ pin ]]]; + [OCMExpect([offlineStore unpinObjectAsync:pin]) andReturn:[BFTask taskWithResult:nil]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store unpinObjectsAsync:@[ object ] withPinName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(offlineStore); +} + +- (void)testUnpinZeroObjects { + id dataSource = [self mockedDataSource]; + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store unpinObjectsAsync:nil withPinName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testUnpinObjectsNoPin { + id dataSource = [self mockedDataSource]; + id offlineStore = dataSource.offlineStore; + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:nil]]; + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + PFObject *object = [PFObject objectWithClassName:@"Yarr"]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store unpinObjectsAsync:@[ object ] withPinName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(offlineStore); +} + +- (void)testUnpinAllObjects { + id dataSource = [self mockedDataSource]; + + PFPin *pin = [PFPin pinWithName:@"Yolo"]; + id offlineStore = dataSource.offlineStore; + [OCMStub([offlineStore findAsyncForQueryState:[OCMArg isNotNil] + user:nil + pin:nil]) andReturn:[BFTask taskWithResult:@[pin]]]; + [OCMExpect([offlineStore unpinObjectAsync:pin]) andReturn:[BFTask taskWithResult:nil]]; + + PFPinningObjectStore *store = [PFPinningObjectStore storeWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[store unpinAllObjectsAsyncWithPinName:@"Yolo"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @YES); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + OCMVerifyAll(offlineStore); +} + +@end diff --git a/Tests/Unit/ProductTests.m b/Tests/Unit/ProductTests.m new file mode 100644 index 000000000..221b12537 --- /dev/null +++ b/Tests/Unit/ProductTests.m @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFProduct.h" +#import "PFUnitTestCase.h" + +@interface ProductTests : PFUnitTestCase + +@end + +@implementation ProductTests + +- (void)testSubclass { + XCTAssertNotNil([PFProduct parseClassName]); + + XCTAssertNoThrow([PFProduct object]); + PFAssertThrowsInvalidArgumentException([[PFProduct alloc] initWithClassName:@"Yarr"]); +} + +@end diff --git a/Tests/Unit/PropertyInfoTests.m b/Tests/Unit/PropertyInfoTests.m new file mode 100644 index 000000000..7515004aa --- /dev/null +++ b/Tests/Unit/PropertyInfoTests.m @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFPropertyInfo.h" +#import "PFPropertyInfo_Runtime.h" +#import "PFTestCase.h" + +@interface TestObject : NSObject + +@property (atomic, copy) NSString *foo; +@property (atomic, assign) int bar; + +@property (atomic, assign) id noIvar; + +@end + +@implementation TestObject + +@dynamic noIvar; + +static void *noIvarKey = &noIvarKey; +- (void)setNoIvar:(id)noIvar { + objc_setAssociatedObject(self, noIvarKey, noIvar, OBJC_ASSOCIATION_RETAIN); +} + +- (id)noIvar { + return objc_getAssociatedObject(self, noIvarKey); +} + +@end + +@interface PropertyInfoTests : PFTestCase + +@end + +@implementation PropertyInfoTests + +- (void)testInit { + PFPropertyInfo *info = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"foo"]; + + XCTAssertEqual(info.name, @"foo"); + XCTAssertEqual(info.associationType, PFPropertyInfoAssociationTypeCopy); + + info = [PFPropertyInfo propertyInfoWithClass:[TestObject class] + name:@"foo" + associationType:PFPropertyInfoAssociationTypeWeak]; + + XCTAssertEqual(info.name, @"foo"); + XCTAssertEqual(info.associationType, PFPropertyInfoAssociationTypeWeak); +} + +- (void)testGetWrappedValue { + TestObject *obj = [[TestObject alloc] init]; + obj.foo = @"Bar"; + obj.bar = 25; + + PFPropertyInfo *fooInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"foo"]; + PFPropertyInfo *barInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"bar"]; + + XCTAssertEqualObjects(@"Bar", [fooInfo getWrappedValueFrom:obj]); + XCTAssertEqualObjects(@25, [barInfo getWrappedValueFrom:obj]); +} + +- (void)testSetWrappedValue { + TestObject *obj = [[TestObject alloc] init]; + + PFPropertyInfo *fooInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"foo"]; + PFPropertyInfo *barInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"bar"]; + + [fooInfo setWrappedValue:@"Bar" forObject:obj]; + [barInfo setWrappedValue:@25 forObject:obj]; + + XCTAssertEqualObjects(@"Bar", obj.foo); + XCTAssertEqual(25, obj.bar); +} + +- (void)testTakeValue { + TestObject *a = [[TestObject alloc] init]; + TestObject *b = [[TestObject alloc] init]; + + a.foo = @"Bar"; + a.bar = 15; + a.noIvar = @"Foo"; + + PFPropertyInfo *fooInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"foo"]; + PFPropertyInfo *barInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"bar"]; + PFPropertyInfo *noIvarInvo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"noIvar"]; + + [fooInfo takeValueFrom:a toObject:b]; + [barInfo takeValueFrom:a toObject:b]; + [noIvarInvo takeValueFrom:a toObject:b]; + + XCTAssertEqualObjects(a.foo, b.foo); + XCTAssertEqual(a.bar, b.bar); + XCTAssertEqualObjects(a.noIvar, b.noIvar); +} + +- (void)testEquality { + PFPropertyInfo *fooInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"foo"]; + PFPropertyInfo *barInfo = [PFPropertyInfo propertyInfoWithClass:[TestObject class] name:@"bar"]; + + XCTAssertTrue([fooInfo isEqual:fooInfo]); + XCTAssertFalse([fooInfo isEqual:barInfo]); + XCTAssertFalse([fooInfo isEqual:nil]); +} + +@end diff --git a/Tests/Unit/PurchaseControllerTests.m b/Tests/Unit/PurchaseControllerTests.m new file mode 100644 index 000000000..27467d1be --- /dev/null +++ b/Tests/Unit/PurchaseControllerTests.m @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import +#import + +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFEncoder.h" +#import "PFFileManager.h" +#import "PFFile_Private.h" +#import "PFMacros.h" +#import "PFPaymentTransactionObserver.h" +#import "PFProductsRequestHandler.h" +#import "PFPurchaseController.h" +#import "PFRESTCommand.h" +#import "PFTestSKPaymentTransaction.h" +#import "PFTestSKProduct.h" +#import "PFTestSKProductsRequest.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface PurchaseControllerTests : PFUnitTestCase +@end + +@implementation PurchaseControllerTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + PFTestSKProductsRequest.validProducts = PF_SET([self sampleProduct]); +} + +- (void)tearDown { + PFTestSKProductsRequest.validProducts = nil; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (SKProduct *)sampleProduct { + return [PFTestSKProduct productWithProductIdentifier:@"product" + price:[NSDecimalNumber decimalNumberWithString:@"13.37"] + title:@"Fizz" + description:@"FizzBuzz"]; +} + +- (NSData *)sampleData { + uint8_t sampleData[16] = { + 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, + }; + + return [NSData dataWithBytes:sampleData length:sizeof(sampleData)]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructor { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id fileManager = PFClassMock([PFFileManager class]); + + PFPurchaseController *controller = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner + fileManager:fileManager]; + + XCTAssertNotNil(controller); + XCTAssertEqual(controller.commandRunner, commandRunner); + XCTAssertEqual(controller.fileManager, fileManager); + + // This makes the test less sad. + controller.paymentQueue = PFClassMock([SKPaymentQueue class]); + + XCTAssertNotNil(controller.paymentQueue); + XCTAssertNotNil(controller.transactionObserver); +} + +- (void)testFindProductsAsync { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id fileManager = PFStrictClassMock([PFFileManager class]); + + PFPurchaseController *purchaseController = [PFPurchaseController controllerWithCommandRunner:commandRunner + fileManager:fileManager]; + + purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [[purchaseController findProductsAsyncWithIdentifiers:PF_SET(@"product")] continueWithSuccessBlock:^id(BFTask *task) { + NSSet *products = [(PFProductsRequestResult *)task.result validProducts]; + XCTAssertEqual(products.count, 1); + id product = [products anyObject]; + + XCTAssertEqualObjects([product productIdentifier], @"product"); + + [expectation fulfill]; + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testBuyProductsAsync { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id fileManager = PFStrictClassMock([PFFileManager class]); + + PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner + fileManager:fileManager]; + + purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; + purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); + + __block SKPaymentTransaction *transaction = nil; + SKPaymentQueue *paymentQueue = purchaseController.paymentQueue; + + // Use a block for this so that we don't accidentally force lazy loading of the transaction observer too early. + // Otherwise it will call addTransactionObserver right away, which will cause us to crash of course. + OCMStub([paymentQueue addTransactionObserver:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:purchaseController.transactionObserver]; + }]]); + + OCMStub([paymentQueue finishTransaction:[OCMArg checkWithBlock:^BOOL(id obj) { + return obj == transaction; + }]]); + + OCMStub([paymentQueue addPayment:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + // Do stuff + __unsafe_unretained SKPayment *payment = nil; + [invocation getArgument:&payment atIndex:2]; + + if ([payment.productIdentifier isEqualToString:@"product"]) { + transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStatePurchased]; + } else { + transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStateFailed]; + } + + [purchaseController.transactionObserver paymentQueue:paymentQueue updatedTransactions: @[ transaction ]]; + }); + + XCTestExpectation *successExpectation = [self expectationWithDescription:@"Success"]; + [[purchaseController buyProductAsyncWithIdentifier:@"product"] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [successExpectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; + + OCMStub([purchaseController canPurchase]).andReturn(YES); + XCTestExpectation *failInvalidProductExpectation = [self expectationWithDescription:@"Failed Invalid Product"]; + + [[purchaseController buyProductAsyncWithIdentifier:@"nonexistent"] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.faulted); + XCTAssertNotNil(task.error); + + [failInvalidProductExpectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testDownloadAssetAsync { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id fileManager = PFStrictClassMock([PFFileManager class]); + + PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner + fileManager:fileManager]; + + purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; + purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); + + SKPayment *payment = [SKPayment paymentWithProduct:[self sampleProduct]]; + PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStatePurchased]; + transaction.transactionReceipt = [self sampleData]; + + PFFile *mockedFile = PFPartialMock([PFFile fileWithName:@"testData" data:[self sampleData]]); + + // lol. Probably should just stick this in the PFFile_Private header. + NSString *stagedPath = [mockedFile valueForKey:@"stagedFilePath"]; + OCMStub([mockedFile _cachedFilePath]).andReturn(stagedPath); + + PFCommandResult *mockedCommandResult = [PFCommandResult commandResultWithResult:(NSDictionary *)mockedFile + resultString:nil + httpResponse:nil]; + BFTask *mockedTask = [BFTask taskWithResult:mockedCommandResult]; + + NSString *tempDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; + + OCMStub([[commandRunner ignoringNonObjectArgs] runCommandAsync:OCMOCK_ANY withOptions:0]).andReturn(mockedTask); + OCMStub([fileManager parseDataItemPathForPathComponent:@"product"]).andReturn(tempDirectory); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + __block int lastProgress = -1; + [[purchaseController downloadAssetAsyncForTransaction:transaction + withProgressBlock:^(int percentDone) { + XCTAssertGreaterThan(percentDone, lastProgress); + + lastProgress = percentDone; + } + sessionToken:@"token"] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertEqual(lastProgress, 100); + + NSData *contentsOfFile = [NSData dataWithContentsOfFile:task.result]; + XCTAssertEqualObjects(contentsOfFile, [self sampleData]); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testDownloadInvalidReceipt { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id fileManager = PFStrictClassMock([PFFileManager class]); + + PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner + fileManager:fileManager]; + purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; + purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); + + SKPayment *payment = [SKPayment paymentWithProduct:[self sampleProduct]]; + PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStatePurchased]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[purchaseController downloadAssetAsyncForTransaction:transaction + withProgressBlock:nil + sessionToken:@"token"] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.faulted); + XCTAssertNotNil(task.error); + XCTAssertEqual(task.error.code, kPFErrorReceiptMissing); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testDownloadInvalidFile { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id fileManager = PFStrictClassMock([PFFileManager class]); + + PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner + fileManager:fileManager]; + purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; + purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); + + SKPayment *payment = [SKPayment paymentWithProduct:[self sampleProduct]]; + PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStatePurchased]; + transaction.transactionReceipt = [self sampleData]; + + PFCommandResult *mockedResult = [PFCommandResult commandResultWithResult:@{ @"a" : @"Hello" } + resultString:nil + httpResponse:nil]; + BFTask *mockedTask = [BFTask taskWithResult:mockedResult]; + OCMStub([[commandRunner ignoringNonObjectArgs] runCommandAsync:OCMOCK_ANY withOptions:0]).andReturn(mockedTask); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[purchaseController downloadAssetAsyncForTransaction:transaction + withProgressBlock:nil + sessionToken:@"token"] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.faulted); + XCTAssertNotNil(task.error); + XCTAssertEqual(task.error.code, kPFErrorInvalidPurchaseReceipt); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/PurchaseUnitTests.m b/Tests/Unit/PurchaseUnitTests.m new file mode 100644 index 000000000..e84ee4d29 --- /dev/null +++ b/Tests/Unit/PurchaseUnitTests.m @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCommandRunning.h" +#import "PFFileManager.h" +#import "PFPaymentTransactionObserver_Private.h" +#import "PFPurchase.h" +#import "PFPurchaseController.h" +#import "PFTestSKPaymentTransaction.h" +#import "PFTestSKProduct.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface PurchaseUnitTests : PFUnitTestCase + +@end + +@implementation PurchaseUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFPurchaseController *)mockedPurchaseController { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + PFFileManager *fileManager = PFStrictClassMock([PFFileManager class]); + + PFPurchaseController *purchaseController = PFPartialMock([[PFPurchaseController alloc] initWithCommandRunner:commandRunner + fileManager:fileManager]); + + SKPaymentQueue *paymentQueue = PFClassMock([SKPaymentQueue class]); + purchaseController.paymentQueue = paymentQueue; + + return purchaseController; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testAddObserver { + PFPurchaseController *mockedPurchaseController = [self mockedPurchaseController]; + SKPaymentQueue *queue = mockedPurchaseController.paymentQueue; + [Parse _currentManager].purchaseController = mockedPurchaseController; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [PFPurchase addObserverForProduct:@"someProduct" block:^(SKPaymentTransaction *transaction) { + XCTAssertEqualObjects(transaction.payment.productIdentifier, @"someProduct"); + [expectation fulfill]; + }]; + + PFPaymentTransactionObserver *transactionObserver = mockedPurchaseController.transactionObserver; + XCTAssertEqual(transactionObserver.blocks.count, 1); + + PFTestSKProduct *product = [PFTestSKProduct productWithProductIdentifier:@"someProduct" + price:nil + title:@"The Title" + description:@"The description"]; + + SKPayment *payment = [SKPayment paymentWithProduct:product]; + OCMStub([queue addPayment:payment]).andDo(^(NSInvocation *invocation) { + PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStatePurchased]; + [transactionObserver paymentQueue:queue updatedTransactions:@[ transaction ]]; + }); + [mockedPurchaseController.paymentQueue addPayment:payment]; + + [self waitForTestExpectations]; +} + +- (void)testBuyProduct { + PFPurchaseController *mockedPurchaseController = [self mockedPurchaseController]; + [Parse _currentManager].purchaseController = mockedPurchaseController; + + BFTask *mockedTask = [BFTask taskWithResult:nil]; + OCMStub([mockedPurchaseController buyProductAsyncWithIdentifier:@"someProduct"]).andReturn(mockedTask); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFPurchase buyProduct:@"someProduct" block:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; +} + +- (void)testRestore { + PFPurchaseController *mockedPurchaseController = [self mockedPurchaseController]; + [Parse _currentManager].purchaseController = mockedPurchaseController; + + SKPaymentQueue *queue = mockedPurchaseController.paymentQueue; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + OCMStub([queue restoreCompletedTransactions]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + [PFPurchase restore]; + + [self waitForTestExpectations]; +} + +- (void)testDownloadAsset { + PFPurchaseController *mockedPurchaseController = [self mockedPurchaseController]; + [Parse _currentManager].purchaseController = mockedPurchaseController; + + BFTask *mockedTask = [BFTask taskWithResult:@"SomePath"]; + + PFTestSKProduct *testProduct = [PFTestSKProduct productWithProductIdentifier:@"Yarr" + price:nil + title:@"El Capitan" + description:@"Ye Loot"]; + + SKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:[SKPayment paymentWithProduct:testProduct] + withError:nil + inState:SKPaymentTransactionStatePurchased]; + + OCMStub([mockedPurchaseController downloadAssetAsyncForTransaction:[OCMArg isEqual:transaction] + withProgressBlock:[OCMArg isNil] + sessionToken:[OCMArg isNil]]).andReturn(mockedTask); + + OCMStub([mockedPurchaseController downloadAssetAsyncForTransaction:OCMOCK_ANY + withProgressBlock:OCMOCK_ANY + sessionToken:OCMOCK_ANY] + ).andThrow([NSException exceptionWithName:NSInternalInconsistencyException + reason:@"Failed Validation" + userInfo:nil]); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFPurchase downloadAssetForTransaction:transaction completion:^(NSString *filePath, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(filePath, @"SomePath"); + + [expectation fulfill]; + }]; + + [self waitForTestExpectations]; +} + +- (void)testAssetContentPath { + PFPurchaseController *mockedPurchaseController = [self mockedPurchaseController]; + [Parse _currentManager].purchaseController = mockedPurchaseController; + + NSString *somePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; + + OCMStub([mockedPurchaseController assetContentPathForProductWithIdentifier:OCMOCK_ANY + fileName:OCMOCK_ANY]).andReturn(somePath); + + + XCTAssertNil([PFPurchase assetContentPathForProduct:nil]); + + NSError *error; + [@"" writeToFile:somePath atomically:YES + encoding:NSUTF8StringEncoding + error:&error]; + + XCTAssertNil(error); + XCTAssertNotNil([PFPurchase assetContentPathForProduct:nil]); +} + +@end diff --git a/Tests/Unit/PushChannelsControllerTests.m b/Tests/Unit/PushChannelsControllerTests.m new file mode 100644 index 000000000..d62d9ea75 --- /dev/null +++ b/Tests/Unit/PushChannelsControllerTests.m @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCurrentInstallationController.h" +#import "PFInstallation.h" +#import "PFMacros.h" +#import "PFPushChannelsController.h" +#import "PFUnitTestCase.h" + +@interface PushChannelsControllerTests : PFUnitTestCase + +@end + +@implementation PushChannelsControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFCurrentInstallationControllerProvider)); + + PFCurrentInstallationController *controller = PFStrictClassMock([PFCurrentInstallationController class]); + OCMStub(dataSource.currentInstallationController).andReturn(controller); + + return dataSource; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id dataSource = [self mockedDataSource]; + + PFPushChannelsController *controller = [[PFPushChannelsController alloc] initWithDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, dataSource); + + controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, dataSource); +} + +- (void)testGetSubscribedChannels { + id dataSource = [self mockedDataSource]; + PFCurrentInstallationController *installationController = dataSource.currentInstallationController; + + BFTask *emptyTask = [BFTask taskWithResult:nil]; + + PFInstallation *installation = PFStrictClassMock([PFInstallation class]); + OCMStub(installation.objectId).andReturn(@"yarr"); + OCMStub(installation.deviceToken).andReturn(@"yolo"); + [OCMStub(installation.channels) andReturn:@[ @"a", @"a", @"b" ]]; + OCMStub([installation fetchInBackground]).andReturn(emptyTask); + + BFTask *task = [BFTask taskWithResult:installation]; + OCMStub([installationController getCurrentObjectAsync]).andReturn(task); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller getSubscribedChannelsAsync] continueWithSuccessBlock:^id(BFTask *task) { + NSSet *result = task.result; + XCTAssertNotNil(result); + XCTAssertEqualObjects(result, (PF_SET(@"a", @"b"))); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetSubscribedChannelsWithUnsavedInstallation { + id dataSource = [self mockedDataSource]; + PFCurrentInstallationController *installationController = dataSource.currentInstallationController; + + BFTask *emptyTask = [BFTask taskWithResult:nil]; + + PFInstallation *installation = PFStrictClassMock([PFInstallation class]); + OCMStub(installation.objectId).andReturn(nil); + OCMStub(installation.deviceToken).andReturn(@"yolo"); + [OCMStub(installation.channels) andReturn:@[ @"a", @"a", @"b" ]]; + OCMStub([installation saveInBackground]).andReturn(emptyTask); + + BFTask *task = [BFTask taskWithResult:installation]; + OCMStub([installationController getCurrentObjectAsync]).andReturn(task); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller getSubscribedChannelsAsync] continueWithSuccessBlock:^id(BFTask *task) { + NSSet *result = task.result; + XCTAssertNotNil(result); + XCTAssertEqualObjects(result, (PF_SET(@"a", @"b"))); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetSubscribedChannelsNoDeviceToken { + id dataSource = [self mockedDataSource]; + PFCurrentInstallationController *installationController = dataSource.currentInstallationController; + + PFInstallation *installation = PFStrictClassMock([PFInstallation class]); + OCMStub(installation.objectId).andReturn(@"yarr"); + OCMStub(installation.deviceToken).andReturn(nil); + + BFTask *task = [BFTask taskWithResult:installation]; + OCMStub([installationController getCurrentObjectAsync]).andReturn(task); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller getSubscribedChannelsAsync] continueWithBlock:^id(BFTask *task) { + NSError *error = task.error; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, kPFErrorPushMisconfigured); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testSubscribeToChannel { + id dataSource = [self mockedDataSource]; + PFCurrentInstallationController *installationController = dataSource.currentInstallationController; + + BFTask *emptyTask = [BFTask taskWithResult:@YES]; + + PFInstallation *installation = PFStrictClassMock([PFInstallation class]); + OCMStub(installation.objectId).andReturn(@"yarr"); + OCMStub(installation.deviceToken).andReturn(@"yolo"); + [OCMStub(installation.channels) andReturn:@[ @"a", @"a", @"b" ]]; + OCMStub([installation isDirtyForKey:@"channels"]).andReturn(NO); + OCMExpect([installation addUniqueObject:@"c" forKey:@"channels"]); + OCMStub([installation saveInBackground]).andReturn(emptyTask); + + BFTask *task = [BFTask taskWithResult:installation]; + OCMStub([installationController getCurrentObjectAsync]).andReturn(task); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller subscribeToChannelAsyncWithName:@"c"] continueWithSuccessBlock:^id(BFTask *task) { + NSNumber *result = task.result; + XCTAssertNotNil(result); + XCTAssertEqualObjects(result, @YES); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)installation); +} + +- (void)testSubscribeToExistingChannel { + id dataSource = [self mockedDataSource]; + PFCurrentInstallationController *installationController = dataSource.currentInstallationController; + + PFInstallation *installation = PFStrictClassMock([PFInstallation class]); + OCMStub(installation.objectId).andReturn(@"yarr"); + OCMStub(installation.deviceToken).andReturn(@"yolo"); + [OCMStub(installation.channels) andReturn:@[ @"a", @"a", @"b" ]]; + OCMStub([installation isDirtyForKey:@"channels"]).andReturn(NO); + + BFTask *task = [BFTask taskWithResult:installation]; + OCMStub([installationController getCurrentObjectAsync]).andReturn(task); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller subscribeToChannelAsyncWithName:@"a"] continueWithSuccessBlock:^id(BFTask *task) { + NSNumber *result = task.result; + XCTAssertNotNil(result); + XCTAssertEqualObjects(result, @YES); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testUnsubscribeFromChannel { + id dataSource = [self mockedDataSource]; + PFCurrentInstallationController *installationController = dataSource.currentInstallationController; + + BFTask *emptyTask = [BFTask taskWithResult:@YES]; + + PFInstallation *installation = PFStrictClassMock([PFInstallation class]); + OCMStub(installation.deviceToken).andReturn(@"yolo"); + [OCMStub(installation.channels) andReturn:@[ @"a", @"a", @"b" ]]; + OCMExpect([installation removeObject:@"a" forKey:@"channels"]); + OCMStub([installation saveInBackground]).andReturn(emptyTask); + + BFTask *task = [BFTask taskWithResult:installation]; + OCMStub([installationController getCurrentObjectAsync]).andReturn(task); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller unsubscribeFromChannelAsyncWithName:@"a"] continueWithSuccessBlock:^id(BFTask *task) { + NSNumber *result = task.result; + XCTAssertNotNil(result); + XCTAssertEqualObjects(result, @YES); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)installation); +} + +- (void)testUnsubscribeFromNotSubscribedChannel { + id dataSource = [self mockedDataSource]; + PFCurrentInstallationController *installationController = dataSource.currentInstallationController; + + BFTask *emptyTask = [BFTask taskWithResult:@YES]; + + PFInstallation *installation = PFStrictClassMock([PFInstallation class]); + OCMStub(installation.deviceToken).andReturn(@"yolo"); + [OCMStub(installation.channels) andReturn:@[ @"a", @"a", @"b" ]]; + OCMStub([installation isDirtyForKey:@"channels"]).andReturn(NO); + OCMStub([installation saveInBackground]).andReturn(emptyTask); + + BFTask *task = [BFTask taskWithResult:installation]; + OCMStub([installationController getCurrentObjectAsync]).andReturn(task); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:dataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller unsubscribeFromChannelAsyncWithName:@"c"] continueWithSuccessBlock:^id(BFTask *task) { + NSNumber *result = task.result; + XCTAssertNotNil(result); + XCTAssertEqualObjects(result, @YES); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/PushCommandTests.m b/Tests/Unit/PushCommandTests.m new file mode 100644 index 000000000..debd4c8c8 --- /dev/null +++ b/Tests/Unit/PushCommandTests.m @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFMutablePushState.h" +#import "PFMutableQueryState.h" +#import "PFRESTPushCommand.h" +#import "PFTestCase.h" + +@interface PushCommandTests : PFTestCase + +@end + +NS_ASSUME_NONNULL_BEGIN + +@implementation PushCommandTests + +- (void)testEmptyPushCommand { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + + PFRESTPushCommand *command = [PFRESTPushCommand sendPushCommandWithPushState:state sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"push"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertEqualObjects(command.parameters[@"where"], @{}); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +- (void)testPushCommandChannels { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + state.channels = [NSSet setWithObject:@"El Capitan!"]; + + PFRESTPushCommand *command = [PFRESTPushCommand sendPushCommandWithPushState:state sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"push"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertEqualObjects(command.parameters[@"channels"], @[ @"El Capitan!" ]); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +- (void)testPushCommandQuery { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"_Installation"]; + [queryState setEqualityConditionWithObject:@"value" forKey:@"key"]; + state.queryState = queryState; + + PFRESTPushCommand *command = [PFRESTPushCommand sendPushCommandWithPushState:state sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"push"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertEqualObjects(command.parameters[@"where"], @{ @"key" : @"value" }); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +- (void)testPushCommandExpirationDate { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + state.expirationDate = [NSDate date]; + + PFRESTPushCommand *command = [PFRESTPushCommand sendPushCommandWithPushState:state sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"push"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters[@"expiration_time"]); + XCTAssertNil(command.parameters[@"expiration_interval"]); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +- (void)testPushCommandExpirationTimeInterval { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + state.expirationTimeInterval = @100500; + + PFRESTPushCommand *command = [PFRESTPushCommand sendPushCommandWithPushState:state sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"push"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNil(command.parameters[@"expiration_time"]); + XCTAssertNotNil(command.parameters[@"expiration_interval"]); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +- (void)testPushCommandPayload { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + state.payload = @{ @"alert" : @"yolo" }; + + PFRESTPushCommand *command = [PFRESTPushCommand sendPushCommandWithPushState:state sessionToken:@"yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"push"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertEqualObjects(command.parameters[@"data"], state.payload); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/Unit/PushControllerTests.m b/Tests/Unit/PushControllerTests.m new file mode 100644 index 000000000..696ee929c --- /dev/null +++ b/Tests/Unit/PushControllerTests.m @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFMacros.h" +#import "PFMutablePushState.h" +#import "PFPushController.h" +#import "PFRESTPushCommand.h" +#import "PFTestCase.h" + +@interface PushControllerTests : PFTestCase + +@end + +@implementation PushControllerTests + +- (void)testConstructor { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFPushController *pushController = [[PFPushController alloc] initWithCommandRunner:commandRunner]; + XCTAssertNotNil(pushController); + XCTAssertEqual(pushController.commandRunner, commandRunner); + + pushController = [PFPushController controllerWithCommandRunner:commandRunner]; + XCTAssertNotNil(pushController); + XCTAssertEqual(pushController.commandRunner, commandRunner); +} + +- (void)testSendPushNotificationAsync { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + PFPushController *pushController = [[PFPushController alloc] initWithCommandRunner:commandRunner]; + + PFMutablePushState *pushState = [[PFMutablePushState alloc] init]; + pushState.payload = @{ @"theKey": @"theValue" }; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + PFCommandResult *result = [[PFCommandResult alloc] initWithResult:@{ } + resultString:nil + httpResponse:nil]; + BFTask *mockedTask = [BFTask taskWithResult:result]; + + + OCMStub([[commandRunner ignoringNonObjectArgs] runCommandAsync:[OCMArg checkWithBlock:^BOOL(id obj) { + PFAssertIsKindOfClass(obj, PFRESTPushCommand); + + PFRESTPushCommand *command = obj; + XCTAssertEqualObjects(command.httpPath, @"push"); + XCTAssertEqualObjects(command.httpMethod, @"POST"); + XCTAssertEqualObjects(command.parameters[@"data"], pushState.payload); + + return YES; + }] withOptions:0]).andReturn(mockedTask); + + [[pushController sendPushNotificationAsyncWithState:pushState + sessionToken:@"token"] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/PushManagerTests.m b/Tests/Unit/PushManagerTests.m new file mode 100644 index 000000000..cfaaa8599 --- /dev/null +++ b/Tests/Unit/PushManagerTests.m @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCommandRunning.h" +#import "PFCurrentInstallationController.h" +#import "PFPushChannelsController.h" +#import "PFPushController.h" +#import "PFPushManager.h" +#import "PFTestCase.h" + +@interface PushManagerTests : PFTestCase + +@end + +@implementation PushManagerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedCommonDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFCommandRunnerProvider)); + id commandRunnerMock = PFStrictProtocolMock(@protocol(PFCommandRunning)); + OCMStub(dataSource.commandRunner).andReturn(commandRunnerMock); + return dataSource; +} + +- (id)mockedCoreDataSource { + id dataSource = PFProtocolMock(@protocol(PFCurrentInstallationControllerProvider)); + id installationControllerMock = PFClassMock([PFCurrentInstallationController class]); + OCMStub(dataSource.currentInstallationController).andReturn(installationControllerMock); + return dataSource; +} + +- (PFPushManager *)samplePushManager { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + return [PFPushManager managerWithCommonDataSource:commonDataSource coreDataSource:coreDataSource]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + + PFPushManager *manager = [[PFPushManager alloc] initWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + XCTAssertNotNil(manager); + XCTAssertEqual((id)manager.commonDataSource, commonDataSource); + XCTAssertEqual((id)manager.coreDataSource, coreDataSource); + + manager = [PFPushManager managerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + XCTAssertNotNil(manager); + XCTAssertEqual((id)manager.commonDataSource, commonDataSource); + XCTAssertEqual((id)manager.coreDataSource, coreDataSource); +} + +- (void)testPushController { + PFPushManager *manager = [self samplePushManager]; + XCTAssertNotNil(manager.pushController); + + PFPushController *controller = [PFPushController controllerWithCommandRunner:manager.commonDataSource.commandRunner]; + manager.pushController = controller; + XCTAssertEqual(manager.pushController, controller); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + manager.pushController = nil; + XCTAssertNotNil(manager.pushController); // The method reloads push controller if not available +#pragma clang diagnostic pop +} + +- (void)testChannelsController { + PFPushManager *manager = [self samplePushManager]; + XCTAssertNotNil(manager.pushController); + + PFPushChannelsController *controller = [PFPushChannelsController controllerWithDataSource:manager.coreDataSource]; + manager.channelsController = controller; + XCTAssertEqual(manager.channelsController, controller); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + manager.channelsController = nil; + XCTAssertNotNil(manager.channelsController); // The method reloads channels controller if not available +#pragma clang diagnostic pop +} + +@end diff --git a/Tests/Unit/PushMobileTests.m b/Tests/Unit/PushMobileTests.m new file mode 100644 index 000000000..e050c1460 --- /dev/null +++ b/Tests/Unit/PushMobileTests.m @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFPush.h" +#import "PFPushUtilities.h" +#import "PFUnitTestCase.h" + +@interface PushMobileTests : PFUnitTestCase + +@end + +@implementation PushMobileTests + +- (void)testHandlePushStringAlert { + id mockedUtils = PFStrictProtocolMock(@protocol(PFPushInternalUtils)); + OCMExpect([mockedUtils showAlertViewWithTitle:[OCMArg isNil] message:@"hello"]); + OCMExpect([mockedUtils playVibrate]); + + // NOTE: Async parse preload step may call this selector. + // Don't epxect it because it doesn't ALWAYs get to this point before returning from the method. + OCMStub([mockedUtils getDeviceTokenFromKeychain]).andReturn(nil); + + [PFPush setPushInternalUtilClass:mockedUtils]; + [PFPush handlePush:@{ @"aps" : @{@"alert" : @"hello"} }]; + + OCMVerifyAll(mockedUtils); + + [PFPush setPushInternalUtilClass:nil]; +} + +- (void)testHandlePushDictionaryAlert { + id mockedUtils = PFStrictProtocolMock(@protocol(PFPushInternalUtils)); + OCMExpect([mockedUtils showAlertViewWithTitle:[OCMArg isNil] message:@"hello bob 1"]); + OCMExpect([mockedUtils playVibrate]); + + // NOTE: Async parse preload step may call this selector. + // Don't epxect it because it doesn't ALWAYs get to this point before returning from the method. + OCMStub([mockedUtils getDeviceTokenFromKeychain]).andReturn(nil); + + [PFPush setPushInternalUtilClass:mockedUtils]; + + [PFPush handlePush:@{ @"aps" : @{@"alert" : @{@"loc-key" : @"hello %@ %@", @"loc-args" : @[ @"bob", @"1" ]}} }]; + + [PFPush setPushInternalUtilClass:nil]; + + OCMVerifyAll(mockedUtils); +} + +@end diff --git a/Tests/Unit/PushStateTests.m b/Tests/Unit/PushStateTests.m new file mode 100644 index 000000000..d9c229e16 --- /dev/null +++ b/Tests/Unit/PushStateTests.m @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutablePushState.h" +#import "PFQueryState.h" +#import "PFTestCase.h" + +@interface PushStateTests : PFTestCase + +@end + +@implementation PushStateTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFPushState *)samplePushState { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + state.channels = [NSSet setWithObject:@"yolo"]; + state.queryState = [[PFQueryState alloc] init]; + state.expirationDate = [NSDate date]; + state.expirationTimeInterval = @1.0; + state.payload = @{ @"alert" : @"yarr" }; + return [state copy]; +} + +- (void)assertPushState:(PFPushState *)state equalToState:(PFPushState *)differentState { + XCTAssertEqualObjects(state.channels, differentState.channels); + XCTAssertEqualObjects(state.queryState, differentState.queryState); + XCTAssertEqualObjects(state.expirationDate, differentState.expirationDate); + XCTAssertEqualObjects(state.expirationTimeInterval, differentState.expirationTimeInterval); + XCTAssertEqualObjects(state.payload, differentState.payload); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testInit { + PFPushState *state = [[PFPushState alloc] init]; + XCTAssertNotNil(state); + XCTAssertNil(state.channels); + XCTAssertNil(state.queryState); + XCTAssertNil(state.expirationDate); + XCTAssertNil(state.expirationTimeInterval); + XCTAssertNil(state.expirationTimeInterval); + + state = [[PFMutablePushState alloc] init]; + XCTAssertNotNil(state); + XCTAssertNil(state.channels); + XCTAssertNil(state.queryState); + XCTAssertNil(state.expirationDate); + XCTAssertNil(state.expirationTimeInterval); + XCTAssertNil(state.expirationTimeInterval); +} + +- (void)testInitWithState { + PFPushState *sampleState = [self samplePushState]; + + PFPushState *state = [[PFPushState alloc] initWithState:sampleState]; + [self assertPushState:state equalToState:sampleState]; + + state = [PFPushState stateWithState:sampleState]; + [self assertPushState:state equalToState:sampleState]; + + state = [[PFMutablePushState alloc] initWithState:sampleState]; + [self assertPushState:state equalToState:sampleState]; + + state = [PFMutablePushState stateWithState:sampleState]; + [self assertPushState:state equalToState:sampleState]; +} + +- (void)testCopying { + PFPushState *sampleState = [self samplePushState]; + [self assertPushState:[sampleState copy] equalToState:sampleState]; + + sampleState = [[PFMutablePushState alloc] initWithState:sampleState]; + [self assertPushState:[sampleState copy] equalToState:sampleState]; +} + +- (void)testMutableCopying { + PFMutablePushState *state = [[self samplePushState] mutableCopy]; + state.payload = @{ @"abc" : @"def" }; + XCTAssertEqualObjects(state.payload, @{ @"abc" : @"def" }); +} + +- (void)testMutableAccessors { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + + NSSet *channels = [NSMutableSet setWithObject:@"yarr"]; + state.channels = channels; + XCTAssertNotEqual(state.channels, channels); + XCTAssertEqualObjects(state.channels, channels); + + PFQueryState *queryState = [[PFQueryState alloc] init]; + state.queryState = queryState; + XCTAssertNotEqual(state.queryState, queryState); + XCTAssertEqualObjects(state.queryState, queryState); + + state.expirationDate = [NSDate dateWithTimeIntervalSince1970:100500]; + XCTAssertEqualObjects(state.expirationDate, [NSDate dateWithTimeIntervalSince1970:100500]); + + state.expirationTimeInterval = @100500.0; + XCTAssertEqualObjects(state.expirationTimeInterval, @100500.0); + + NSDictionary *payload = [@{ @"a" : @"b" } mutableCopy]; + state.payload = payload; + XCTAssertNotEqual(state.payload, payload); + XCTAssertEqualObjects(state.payload, payload); +} + +- (void)testSetPayloadWithMessage { + PFMutablePushState *state = [[PFMutablePushState alloc] init]; + [state setPayloadWithMessage:@"yolo"]; + XCTAssertEqualObjects(state.payload, @{ @"alert" : @"yolo" }); + + [state setPayloadWithMessage:nil]; + XCTAssertNil(state.payload); +} + +@end diff --git a/Tests/Unit/PushUnitTests.m b/Tests/Unit/PushUnitTests.m new file mode 100644 index 000000000..97ff95963 --- /dev/null +++ b/Tests/Unit/PushUnitTests.m @@ -0,0 +1,584 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCoreManager.h" +#import "PFCurrentInstallationController.h" +#import "PFMacros.h" +#import "PFMutablePushState.h" +#import "PFMutableQueryState.h" +#import "PFPush.h" +#import "PFPushChannelsController.h" +#import "PFPushController.h" +#import "PFPushManager.h" +#import "PFPushPrivate.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface PushUnitTests : PFUnitTestCase + +@property (nonatomic, strong) XCTestExpectation *expectationToFulfuill; +@property (nonatomic, copy) void (^validationBlock)(id); + +@end + +@implementation PushUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFPushManager *)mockedPushManager { + PFPushManager *mockedManager = PFStrictClassMock([PFPushManager class]); + PFPushController *mockedController = PFStrictClassMock([PFPushController class]); + PFPushChannelsController *mockedChannelsController = PFStrictClassMock([PFPushChannelsController class]); + + OCMStub(mockedManager.channelsController).andReturn(mockedChannelsController); + OCMStub(mockedManager.pushController).andReturn(mockedController); + + return mockedManager; +} + +- (void)validateObjectResults:(id)results error:(NSError *)error { + XCTAssertNil(error); + + self.validationBlock(results); + [self.expectationToFulfuill fulfill]; +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [Parse _currentManager].pushManager = [self mockedPushManager]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + PFPush *push = [PFPush push]; + XCTAssertNotNil(push); + + push = [[PFPush alloc] init]; + XCTAssertNotNil(push); +} + +- (void)testCopy { + PFPush *push = [PFPush push]; + PFPush *copied = [push copy]; + XCTAssertNotEqual(push, copied); +} + +#pragma mark NSObject + +- (void)testEqualityAndHash { + PFPush *pushA = [[PFPush alloc] init]; + PFPush *pushB = [[PFPush alloc] init]; + XCTAssertTrue([pushA isEqual:pushB]); + XCTAssertEqual([pushA hash], [pushB hash]); + + PFQuery *query = [PFQuery queryWithClassName:@"aClass"]; + [pushA setQuery:query]; + XCTAssertFalse([pushA isEqual:pushB]); + + [pushB setQuery:query]; + XCTAssertTrue([pushA isEqual:pushB]); + XCTAssertEqual([pushA hash], [pushB hash]); + + pushA = [[PFPush alloc] init]; + pushB = [[PFPush alloc] init]; + + NSString *channelName = @"channel"; + [pushA setChannel:channelName]; + XCTAssertFalse([pushA isEqual:pushB]); + + [pushB setChannel:channelName]; + XCTAssertTrue([pushA isEqual:pushB]); + XCTAssertEqual([pushA hash], [pushB hash]); + + NSString *message = @"Hello, World!"; + [pushA setMessage:message]; + XCTAssertFalse([pushA isEqual:pushB]); + + [pushB setMessage:message]; + XCTAssertTrue([pushA isEqual:pushB]); + XCTAssertEqual([pushA hash], [pushB hash]); + + NSDate *date = [NSDate date]; + [pushA expireAtDate:date]; + XCTAssertFalse([pushA isEqual:pushB]); + + [pushB expireAtDate:date]; + XCTAssertTrue([pushA isEqual:pushB]); + XCTAssertEqual([pushA hash], [pushB hash]); + + NSTimeInterval interval = 60; + [pushA expireAfterTimeInterval:interval]; + XCTAssertFalse([pushA isEqual:pushB]); + + [pushB expireAfterTimeInterval:interval]; + XCTAssertTrue([pushA isEqual:pushB]); + XCTAssertEqual([pushA hash], [pushB hash]); + + NSDictionary *payload = @{ @"foo": @"bar" }; + [pushA setData:payload]; + XCTAssertFalse([pushA isEqual:pushB]); + + [pushB setData:payload]; + XCTAssertTrue([pushA isEqual:pushB]); + XCTAssertEqual([pushA hash], [pushB hash]); +} + +- (void)testHash { + PFPush *pushA = [[PFPush alloc] init]; + PFPush *pushB = [[PFPush alloc] init]; + XCTAssertEqual([pushA hash], [pushB hash]); + + PFQuery *query = [PFQuery queryWithClassName:@"aClass"]; + [pushA setQuery:query]; + [pushB setQuery:query]; + XCTAssertEqual([pushA hash], [pushB hash]); + + pushA = [[PFPush alloc] init]; + pushB = [[PFPush alloc] init]; + + NSString *channelName = @"channel"; + [pushA setChannel:channelName]; + [pushB setChannel:channelName]; + XCTAssertEqual([pushA hash], [pushB hash]); + + NSString *message = @"Hello, World!"; + [pushA setMessage:message]; + [pushB setMessage:message]; + XCTAssertEqual([pushA hash], [pushB hash]); + + NSDate *date = [NSDate date]; + [pushA expireAtDate:date]; + [pushB expireAtDate:date]; + XCTAssertEqual([pushA hash], [pushB hash]); + + NSTimeInterval interval = 60; + [pushA expireAfterTimeInterval:interval]; + [pushB expireAfterTimeInterval:interval]; + XCTAssertEqual([pushA hash], [pushB hash]); + + NSDictionary *payload = @{ @"foo": @"bar" }; + [pushA setData:payload]; + [pushB setData:payload]; + XCTAssertEqual([pushA hash], [pushB hash]); +} + +- (void)testSendPush { + NSString *channelName = @"channel"; + NSString *message = @"Hello, World!"; + + PFPushController *mockedPushController = [Parse _currentManager].pushManager.pushController; + + PFPush *thePush = [PFPush push]; + PFMutablePushState *expectedPushState = [[PFMutablePushState alloc] init]; + + [thePush setMessage:message]; + [expectedPushState setPayloadWithMessage:message]; + + BFTask *mockedResult = [BFTask taskWithResult:@YES]; + OCMStub([mockedPushController sendPushNotificationAsyncWithState:[OCMArg isEqual:expectedPushState] + sessionToken:nil]).andReturn(mockedResult); + + XCTAssertTrue([thePush sendPush:NULL]); + + [thePush setChannel:channelName]; + [expectedPushState setChannels:PF_SET(channelName)]; + + XCTAssertTrue([thePush sendPush:NULL]); + + [thePush setChannels:@[ channelName ]]; + + XCTAssertTrue([thePush sendPush:NULL]); + + PFQuery *query = [PFQuery queryWithClassName:@"aClass"]; + PFQueryState *queryState = [[PFMutableQueryState alloc] initWithParseClassName:@"aClass"]; + + thePush = [PFPush push]; + [thePush setMessage:message]; + [thePush setQuery:query]; + + [expectedPushState setChannels:nil]; + [expectedPushState setQueryState:queryState]; + + XCTAssertTrue([thePush sendPush:NULL]); + + NSDate *expiryDate = [NSDate dateWithTimeIntervalSinceNow:60 * 10]; + [thePush expireAtDate:expiryDate]; + [expectedPushState setExpirationDate:expiryDate]; + + XCTAssertTrue([thePush sendPush:NULL]); + + [thePush expireAfterTimeInterval:60 * 10]; + [expectedPushState setExpirationDate:nil]; + [expectedPushState setExpirationTimeInterval:@(60 * 10)]; + + XCTAssertTrue([thePush sendPush:NULL]); + + [thePush clearExpiration]; + [expectedPushState setExpirationTimeInterval:nil]; + + XCTAssertTrue([thePush sendPush:NULL]); + + XCTestExpectation *backgroundBlockExpectation = [self expectationWithDescription:@"backgroundBlock"]; + XCTestExpectation *backgroundTargetSelectorExpectation = [self expectationWithDescription:@"backgroundTargetSel"]; + + [[[thePush sendPushInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertTrue([task.result boolValue]); + + return task; + }] waitUntilFinished]; + [thePush sendPushInBackgroundWithBlock:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + + [backgroundBlockExpectation fulfill]; + }]; + + @weakify(self); + self.validationBlock = ^(id success) { + @strongify(self); + XCTAssertTrue([success boolValue]); + }; + + self.expectationToFulfuill = backgroundTargetSelectorExpectation; + [thePush sendPushInBackgroundWithTarget:self selector:@selector(validateObjectResults:error:)]; + + [self waitForTestExpectations]; +} + +- (void)testStaticChannelPush { + NSString *channelName = @"channel"; + NSString *message = @"Hello, World!"; + + PFPushController *mockedPushController = [Parse _currentManager].pushManager.pushController; + + PFMutablePushState *expectedPushState = [[PFMutablePushState alloc] init]; + [expectedPushState setPayloadWithMessage:message]; + [expectedPushState setChannels:PF_SET(channelName)]; + + BFTask *mockedResult = [BFTask taskWithResult:@YES]; + OCMStub([mockedPushController sendPushNotificationAsyncWithState:expectedPushState + sessionToken:nil]).andReturn(mockedResult); + + XCTestExpectation *toChannelBlockExpectation = [self expectationWithDescription:@"toChannelBlock"]; + XCTestExpectation *toChannelTargetSelectorExpectation = [self expectationWithDescription:@"toChannelTargetSel"]; + + [PFPush sendPushMessageToChannel:channelName withMessage:message error:NULL]; + [[[PFPush sendPushMessageToChannelInBackground:channelName + withMessage:message] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertTrue([task.result boolValue]); + + return task; + }] waitUntilFinished]; + [PFPush sendPushMessageToChannelInBackground:channelName + withMessage:message + block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + + [toChannelBlockExpectation fulfill]; + }]; + + @weakify(self); + self.validationBlock = ^(id success) { + @strongify(self); + XCTAssertTrue([success boolValue]); + }; + + self.expectationToFulfuill = toChannelTargetSelectorExpectation; + [PFPush sendPushMessageToChannelInBackground:channelName + withMessage:message + target:self + selector:@selector(validateObjectResults:error:)]; + + [self waitForTestExpectations]; +} + +- (void)testStaticChannelPushData { + NSString *channelName = @"channel"; + NSDictionary *payload = @{ @"alert" : @"MyMessage", + @"customKey" : @"customValue" }; + + PFPushController *mockedPushController = [Parse _currentManager].pushManager.pushController; + + PFMutablePushState *expectedPushState = [[PFMutablePushState alloc] init]; + [expectedPushState setChannels:PF_SET(channelName)]; + [expectedPushState setPayload:payload]; + + BFTask *mockedResult = [BFTask taskWithResult:@YES]; + OCMStub([mockedPushController sendPushNotificationAsyncWithState:expectedPushState + sessionToken:nil]).andReturn(mockedResult); + + XCTestExpectation *toChannelBlockExpectation = [self expectationWithDescription:@"toChannelBlock"]; + XCTestExpectation *toChannelTargetSelectorExpectation = [self expectationWithDescription:@"toChannelTargetSel"]; + + [PFPush sendPushDataToChannel:channelName withData:payload error:NULL]; + [PFPush sendPushDataToChannelInBackground:channelName withData:payload block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + + [toChannelBlockExpectation fulfill]; + }]; + + @weakify(self); + self.validationBlock = ^(id success) { + @strongify(self); + XCTAssertTrue([success boolValue]); + }; + + self.expectationToFulfuill = toChannelTargetSelectorExpectation; + [PFPush sendPushDataToChannelInBackground:channelName + withData:payload + target:self + selector:@selector(validateObjectResults:error:)]; + + [self waitForTestExpectations]; +} + +- (void)testStaticQueryPush { + PFQuery *query = [PFQuery queryWithClassName:@"SomeClass"]; + NSString *message = @"Hello, World!"; + + PFPushController *mockedPushController = [Parse _currentManager].pushManager.pushController; + + PFMutablePushState *expectedPushState = [[PFMutablePushState alloc] init]; + [expectedPushState setPayloadWithMessage:message]; + [expectedPushState setQueryState:[[PFMutableQueryState alloc] initWithParseClassName:@"SomeClass"]]; + + BFTask *mockedResult = [BFTask taskWithResult:@YES]; + OCMStub([mockedPushController sendPushNotificationAsyncWithState:expectedPushState + sessionToken:nil]).andReturn(mockedResult); + + XCTestExpectation *toQueryBlockExpectation = [self expectationWithDescription:@"toQueryBlock"]; + + [PFPush sendPushMessageToQuery:query withMessage:message error:NULL]; + [[[PFPush sendPushMessageToQueryInBackground:query + withMessage:message] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertTrue([task.result boolValue]); + + return task; + }] waitUntilFinished]; + [PFPush sendPushMessageToQueryInBackground:query + withMessage:message + block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + + [toQueryBlockExpectation fulfill]; + }]; + + [self waitForTestExpectations]; +} + +- (void)testStaticQueryPushData { + PFQuery *query = [PFQuery queryWithClassName:@"SomeClass"]; + NSDictionary *payload = @{ @"alert" : @"MyMessage", + @"customKey" : @"customValue" }; + + PFPushController *mockedPushController = [Parse _currentManager].pushManager.pushController; + + PFMutablePushState *expectedPushState = [[PFMutablePushState alloc] init]; + [expectedPushState setPayload:payload]; + [expectedPushState setQueryState:[[PFMutableQueryState alloc] initWithParseClassName:@"SomeClass"]]; + + BFTask *mockedResult = [BFTask taskWithResult:@YES]; + OCMStub([mockedPushController sendPushNotificationAsyncWithState:expectedPushState + sessionToken:nil]).andReturn(mockedResult); + + XCTestExpectation *toQueryBlockExpectation = [self expectationWithDescription:@"toQueryBlock"]; + + [PFPush sendPushDataToQuery:query withData:payload error:NULL]; + [[[PFPush sendPushDataToQueryInBackground:query withData:payload] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertTrue([task.result boolValue]); + + return task; + }] waitUntilFinished]; + [PFPush sendPushDataToQueryInBackground:query + withData:payload + block:^(BOOL succeeded, NSError *error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + + [toQueryBlockExpectation fulfill]; + }]; + + [self waitForTestExpectations]; +} + +- (void)testGetSubscribedChannels { + NSString *channel = @"channel"; + NSSet *channelsSet = PF_SET(channel); + + PFPushChannelsController *mockedChannelsController = [Parse _currentManager].pushManager.channelsController; + + BFTask *mockedResult = [BFTask taskWithResult:channelsSet]; + OCMStub([mockedChannelsController getSubscribedChannelsAsync]).andReturn(mockedResult); + + XCTestExpectation *subscribeBlockExpectation = [self expectationWithDescription:@"subscribeBlock"]; + XCTestExpectation *subscribeTargetSelectorExpectation = [self expectationWithDescription:@"subscribeTargetSel"]; + + XCTAssertEqualObjects(channelsSet, [PFPush getSubscribedChannels:NULL]); + [[[PFPush getSubscribedChannelsInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(channelsSet, task.result); + + return task; + }] waitUntilFinished]; + [PFPush getSubscribedChannelsInBackgroundWithBlock:^(NSSet *channels, NSError *error) { + XCTAssertEqualObjects(channelsSet, channels); + + [subscribeBlockExpectation fulfill]; + }]; + + @weakify(self); + self.validationBlock = ^(id result) { + @strongify(self); + XCTAssertEqualObjects(channelsSet, result); + }; + + self.expectationToFulfuill = subscribeTargetSelectorExpectation; + [PFPush getSubscribedChannelsInBackgroundWithTarget:self selector:@selector(validateObjectResults:error:)]; + + [self waitForTestExpectations]; +} + +- (void)testSubscribeToChannels { + NSString *channel = @"channel"; + + PFPushChannelsController *mockedChannelsController = [Parse _currentManager].pushManager.channelsController; + + BFTask *mockedResult = [BFTask taskWithResult:@YES]; + OCMStub([mockedChannelsController subscribeToChannelAsyncWithName:channel]).andReturn(mockedResult); + + XCTestExpectation *subscribeBlockExpectation = [self expectationWithDescription:@"subscribeBlock"]; + XCTestExpectation *subscribeTargetSelectorExpectation = [self expectationWithDescription:@"subscribeTargetSel"]; + + [PFPush subscribeToChannel:channel error:NULL]; + [[[PFPush subscribeToChannelInBackground:channel] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertTrue([task.result boolValue]); + + return task; + }] waitUntilFinished]; + [PFPush subscribeToChannelInBackground:channel block:^(BOOL succeeded, NSError *__nullable error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + + [subscribeBlockExpectation fulfill]; + }]; + + @weakify(self); + self.validationBlock = ^(id success) { + @strongify(self); + XCTAssertTrue([success boolValue]); + }; + + self.expectationToFulfuill = subscribeTargetSelectorExpectation; + [PFPush subscribeToChannelInBackground:channel + target:self + selector:@selector(validateObjectResults:error:)]; + + [self waitForTestExpectations]; +} + +- (void)testUnsubscribeFromChannels { + NSString *channel = @"channel"; + + PFPushChannelsController *mockedChannelsController = [Parse _currentManager].pushManager.channelsController; + + BFTask *mockedResult = [BFTask taskWithResult:@YES]; + OCMStub([mockedChannelsController unsubscribeFromChannelAsyncWithName:channel]).andReturn(mockedResult); + + XCTestExpectation *unsubscribeBlockExpectation = [self expectationWithDescription:@"unsubscribeBlock"]; + XCTestExpectation *unsubscribeTargetSelectorExpectation = [self expectationWithDescription:@"unsubscribeTargetSel"]; + + [PFPush unsubscribeFromChannel:channel error:NULL]; + [[[PFPush unsubscribeFromChannelInBackground:channel] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertTrue([task.result boolValue]); + + return task; + }] waitUntilFinished]; + [PFPush unsubscribeFromChannelInBackground:channel block:^(BOOL succeeded, NSError *__nullable error) { + XCTAssertTrue(succeeded); + XCTAssertNil(error); + + [unsubscribeBlockExpectation fulfill]; + }]; + + @weakify(self); + self.validationBlock = ^(id success) { + @strongify(self); + XCTAssertTrue([success boolValue]); + }; + + self.expectationToFulfuill = unsubscribeTargetSelectorExpectation; + [PFPush unsubscribeFromChannelInBackground:channel + target:self + selector:@selector(validateObjectResults:error:)]; + + [self waitForTestExpectations]; +} + +- (void)testDeviceToken { + PFInstallation *installation = [[PFInstallation alloc] init]; + BFTask *installationTask = [BFTask taskWithResult:installation]; + + PFCurrentInstallationController *mockedInstallationController = PFStrictClassMock([PFCurrentInstallationController class]); + OCMStub([mockedInstallationController getCurrentObjectAsync]).andReturn(installationTask); + + [Parse _currentManager].coreManager.currentInstallationController = mockedInstallationController; + + XCTAssertNil(installation.deviceToken); + + [PFPush storeDeviceToken:@"token"]; + XCTAssertEqualObjects(installation.deviceToken, @"token"); + + [[PFPush pushInternalUtilClass] clearDeviceToken]; + XCTAssertNil(installation.deviceToken); + + NSData *dataToken = [NSData dataWithBytes:(const char[]) { 0xFF, 0x7F, 0x00 } length:3]; + NSString *expectedString = @"ff7f00"; + + [PFPush storeDeviceToken:dataToken]; + XCTAssertEqualObjects(installation.deviceToken, expectedString); + + [[PFPush pushInternalUtilClass] clearDeviceToken]; + XCTAssertNil(installation.deviceToken); +} + +- (void)testDeprecatedMethods { + PFPush *push = [PFPush push]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + [push setPushToIOS:YES]; + [push setPushToAndroid:YES]; + +#pragma clang diagnostic pop +} + +@end diff --git a/Tests/Unit/QueryCachedControllerTests.m b/Tests/Unit/QueryCachedControllerTests.m new file mode 100644 index 000000000..e87fec5cd --- /dev/null +++ b/Tests/Unit/QueryCachedControllerTests.m @@ -0,0 +1,424 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCachedQueryController.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFJSONSerialization.h" +#import "PFKeyValueCache.h" +#import "PFMutableQueryState.h" +#import "PFObject.h" +#import "PFRESTQueryCommand.h" +#import "PFTestCase.h" + +@protocol CachedQueryControllerDataSource + +@end + +@interface QueryCachedControllerTests : PFTestCase + +@end + +@implementation QueryCachedControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedDataSource { + id dataSource = PFStrictProtocolMock(@protocol(CachedQueryControllerDataSource)); + + id runner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + OCMStub(dataSource.commandRunner).andReturn(runner); + + PFKeyValueCache *keyValueCache = PFStrictClassMock([PFKeyValueCache class]); + OCMStub(dataSource.keyValueCache).andReturn(keyValueCache); + + return dataSource; +} + +- (PFQueryState *)sampleQueryStateWithCachePolicy:(PFCachePolicy)cachePolicy { + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"Yolo"]; + [queryState setEqualityConditionWithObject:@"yarr" forKey:@"name"]; + [queryState selectKeys:@[ @"name" ]]; + queryState.cachePolicy = cachePolicy; + return [queryState copy]; +} + +- (PFCommandResult *)sampleCommandResult { + PFCommandResult *result = [PFCommandResult commandResultWithResult:@{ @"results" : @[ @{@"className" : @"Yolo", + @"name" : @"yarr", + @"objectId" : @"abc", + @"job" : @"pirate"} ], + @"count" : @5 } + resultString:nil + httpResponse:nil]; + return result; +} + +- (void)assertFindObjectsResult:(NSArray *)result { + XCTAssertNotNil(result); + XCTAssertEqual(result.count, 1); + + PFObject *object = [result lastObject]; + XCTAssertNotNil(object); + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"abc"); + XCTAssertEqualObjects(object[@"name"], @"yarr"); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id dataSource = [self mockedDataSource]; + + PFCachedQueryController *controller = [[PFCachedQueryController alloc] initWithCommonDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.commonDataSource, dataSource); + + controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.commonDataSource, dataSource); +} + +- (void)testFindObjectsIgnoreCache { + id dataSource = [self mockedDataSource]; + id runner = dataSource.commandRunner; + BFTask *commandTask = [BFTask taskWithResult:[self sampleCommandResult]]; + OCMStub([[runner ignoringNonObjectArgs] runCommandAsync:[OCMArg isNotNil] + withOptions:0 + cancellationToken:[OCMArg isNil]]).andReturn(commandTask); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyIgnoreCache]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + [self assertFindObjectsResult:task.result]; + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsCacheOnly { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + + NSString *jsonString = [PFJSONSerialization stringFromJSONObject:[self sampleCommandResult].result]; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg isNotNil] maxAge:0]).andReturn(jsonString); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyCacheOnly]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + [self assertFindObjectsResult:task.result]; + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsCacheOnlyCorruptJSON { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg isNotNil] maxAge:0]).andReturn(@"blah blah"); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyCacheOnly]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + NSError *error = task.error; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, kPFErrorCacheMiss); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsNetworkOnly { + id dataSource = [self mockedDataSource]; + + id keyValueCache = dataSource.keyValueCache; + OCMStub([keyValueCache setObject:[OCMArg isEqual:@"yolo"] forKey:[OCMArg isNotNil]]); + + id runner = dataSource.commandRunner; + BFTask *commandTask = [BFTask taskWithResult:[self sampleCommandResult]]; + OCMStub([[runner ignoringNonObjectArgs] runCommandAsync:[OCMArg isNotNil] + withOptions:0 + cancellationToken:[OCMArg isNil]]).andReturn(commandTask); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyNetworkOnly]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + [self assertFindObjectsResult:task.result]; + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsCacheElseNetwork { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg isNotNil] maxAge:0]).andReturn(nil); + OCMStub([[cache ignoringNonObjectArgs] setObject:[OCMArg isEqual:@"yolo"] forKey:[OCMArg isNotNil]]); + + id runner = dataSource.commandRunner; + BFTask *commandTask = [BFTask taskWithResult:[self sampleCommandResult]]; + OCMStub([[runner ignoringNonObjectArgs] runCommandAsync:[OCMArg isNotNil] + withOptions:0 + cancellationToken:[OCMArg isNil]]).andReturn(commandTask); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyCacheElseNetwork]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + [self assertFindObjectsResult:task.result]; + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsCacheElseNetworkCacheResult { + id dataSource = [self mockedDataSource]; + + id cache = dataSource.keyValueCache; + NSString *jsonString = [PFJSONSerialization stringFromJSONObject:[self sampleCommandResult].result]; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg isNotNil] maxAge:0]).andReturn(jsonString); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyCacheElseNetwork]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + [self assertFindObjectsResult:task.result]; + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsNetworkElseCache { + id dataSource = [self mockedDataSource]; + + id cache = dataSource.keyValueCache; + NSString *jsonString = [PFJSONSerialization stringFromJSONObject:[self sampleCommandResult].result]; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg isNotNil] maxAge:0]).andReturn(jsonString); + + id runner = dataSource.commandRunner; + BFTask *commandTask = [BFTask taskWithError:[NSError errorWithDomain:@"TestErrorDomain" code:100500 userInfo:nil]]; + OCMStub([[runner ignoringNonObjectArgs] runCommandAsync:[OCMArg isNotNil] + withOptions:0 + cancellationToken:[OCMArg isNil]]).andReturn(commandTask); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyNetworkElseCache]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + [self assertFindObjectsResult:task.result]; + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsNetworkElseCacheNetworkResult { + id dataSource = [self mockedDataSource]; + + id keyValueCache = dataSource.keyValueCache; + OCMStub([keyValueCache setObject:[OCMArg isEqual:@"yolo"] forKey:[OCMArg isNotNil]]); + + id runner = dataSource.commandRunner; + BFTask *commandTask = [BFTask taskWithResult:[self sampleCommandResult]]; + OCMStub([[runner ignoringNonObjectArgs] runCommandAsync:[OCMArg isNotNil] + withOptions:0 + cancellationToken:[OCMArg isNil]]).andReturn(commandTask); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyNetworkElseCache]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + [self assertFindObjectsResult:task.result]; + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsCacheThenNetwork { + id dataSource = [self mockedDataSource]; + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyCacheThenNetwork]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.exception); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsUnknownPolicy { + id dataSource = [self mockedDataSource]; + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:100]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.exception); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCountObjectsIgnoreCache { + id dataSource = [self mockedDataSource]; + id runner = dataSource.commandRunner; + BFTask *commandTask = [BFTask taskWithResult:[self sampleCommandResult]]; + OCMStub([[runner ignoringNonObjectArgs] runCommandAsync:[OCMArg isNotNil] + withOptions:0 + cancellationToken:[OCMArg isNil]]).andReturn(commandTask); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyIgnoreCache]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller countObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @5); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCountObjectsCacheOnly { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + NSString *jsonString = [PFJSONSerialization stringFromJSONObject:[self sampleCommandResult].result]; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg isNotNil] maxAge:0]).andReturn(jsonString); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:kPFCachePolicyCacheOnly]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller countObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @5); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCacheKey { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + + NSString *jsonString = [PFJSONSerialization stringFromJSONObject:[self sampleCommandResult].result]; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg isNotNil] maxAge:0]).andReturn(jsonString); + + PFQueryState *state = [self sampleQueryStateWithCachePolicy:0]; + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + + NSString *cacheKey = [PFRESTQueryCommand findCommandForQueryState:state withSessionToken:@"a"].cacheKey; + XCTAssertEqualObjects([controller cacheKeyForQueryState:state sessionToken:@"a"], cacheKey); +} + +- (void)testHasCachedResult { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + + PFQueryState *state = [self sampleQueryStateWithCachePolicy:0]; + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + + NSString *cacheKey = [PFRESTQueryCommand findCommandForQueryState:state withSessionToken:@"a"].cacheKey; + + NSString *jsonString = [PFJSONSerialization stringFromJSONObject:[self sampleCommandResult].result]; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:cacheKey]; + }] maxAge:0]).andReturn(jsonString); + + XCTAssertTrue([controller hasCachedResultForQueryState:state sessionToken:@"a"]); + + cacheKey = [PFRESTQueryCommand findCommandForQueryState:state withSessionToken:nil].cacheKey; + OCMStub([[cache ignoringNonObjectArgs] objectForKey:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:cacheKey]; + }] maxAge:0]).andReturn(nil); + + controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + XCTAssertFalse([controller hasCachedResultForQueryState:state sessionToken:nil]); +} + +- (void)testClearCachedResult { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + OCMExpect([cache removeObjectForKey:[OCMArg isNotNil]]); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + PFQueryState *state = [self sampleQueryStateWithCachePolicy:0]; + [controller clearCachedResultForQueryState:state sessionToken:@"a"]; + + OCMVerifyAll(cache); +} + +- (void)testClearAllCachedResults { + id dataSource = [self mockedDataSource]; + id cache = dataSource.keyValueCache; + OCMExpect([cache removeAllObjects]); + + PFCachedQueryController *controller = [PFCachedQueryController controllerWithCommonDataSource:dataSource]; + [controller clearAllCachedResults]; + + OCMVerifyAll(cache); +} + +@end diff --git a/Tests/Unit/QueryControllerUnitTests.m b/Tests/Unit/QueryControllerUnitTests.m new file mode 100644 index 000000000..8d514217f --- /dev/null +++ b/Tests/Unit/QueryControllerUnitTests.m @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "BFTask+Private.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFMutableQueryState.h" +#import "PFObject.h" +#import "PFQueryController.h" +#import "PFTestCase.h" + +@interface QueryControllerUnitTests : PFTestCase + +@end + +@implementation QueryControllerUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedCommonDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFCommandRunnerProvider)); + + id runner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + + PFCommandResult *result = [PFCommandResult commandResultWithResult:@{ @"results" : @[ @{@"className" : @"Yolo", + @"name" : @"yarr", + @"objectId" : @"abc", + @"job" : @"pirate"} ], + @"count" : @5 } + resultString:nil + httpResponse:nil]; + BFTask *task = [BFTask taskWithResult:result]; + OCMStub([[runner ignoringNonObjectArgs] runCommandAsync:OCMOCK_ANY + withOptions:0 + cancellationToken:OCMOCK_ANY]).andReturn(task); + + OCMStub(dataSource.commandRunner).andReturn(runner); + + return dataSource; +} + +- (PFQueryState *)sampleQueryState { + PFMutableQueryState *queryState = [PFMutableQueryState stateWithParseClassName:@"Yolo"]; + [queryState setEqualityConditionWithObject:@"yarr" forKey:@"name"]; + [queryState selectKeys:@[ @"name" ]]; + return [queryState copy]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id dataSource = [self mockedCommonDataSource]; + + PFQueryController *controller = [[PFQueryController alloc] initWithCommonDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.commonDataSource, dataSource); + + controller = [PFQueryController controllerWithCommonDataSource:dataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.commonDataSource, dataSource); +} + +- (void)testFindObjectsResult { + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + PFQueryState *state = [self sampleQueryState]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + NSArray *objects = task.result; + XCTAssertNotNil(objects); + XCTAssertEqual(objects.count, 1); + + PFObject *object = [objects lastObject]; + XCTAssertNotNil(object); + XCTAssertEqualObjects(object.parseClassName, @"Yolo"); + XCTAssertEqualObjects(object.objectId, @"abc"); + XCTAssertEqualObjects(object[@"name"], @"yarr"); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsCancellation { + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + PFQueryState *state = [self sampleQueryState]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationTokenSource cancel]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller findObjectsAsyncForQueryState:state + withCancellationToken:cancellationTokenSource.token + user:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCountObjectsResult { + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + PFQueryState *state = [self sampleQueryState]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller countObjectsAsyncForQueryState:state + withCancellationToken:nil + user:nil] continueWithBlock:^id(BFTask *task) { + NSNumber *count = task.result; + XCTAssertNotNil(count); + XCTAssertEqual([count intValue], 5); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCountObjectsCancellation { + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + PFQueryState *state = [self sampleQueryState]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationTokenSource cancel]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller countObjectsAsyncForQueryState:state + withCancellationToken:cancellationTokenSource.token + user:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCacheKey { + PFQueryState *state = [self sampleQueryState]; + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + + XCTAssertNil([controller cacheKeyForQueryState:state sessionToken:@"a"]); + XCTAssertNil([controller cacheKeyForQueryState:state sessionToken:nil]); +} + +- (void)testHasCachedResult { + PFQueryState *state = [self sampleQueryState]; + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + + XCTAssertFalse([controller hasCachedResultForQueryState:state sessionToken:@"a"]); + XCTAssertFalse([controller hasCachedResultForQueryState:state sessionToken:nil]); +} + +- (void)testClearCachedResult { + PFQueryState *state = [self sampleQueryState]; + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + + // It should do nothing - so just test it doesn't crash. + [controller clearCachedResultForQueryState:state sessionToken:@"a"]; + [controller clearCachedResultForQueryState:state sessionToken:nil]; +} + +- (void)testClearAllCachedResults { + PFQueryController *controller = [PFQueryController controllerWithCommonDataSource:[self mockedCommonDataSource]]; + + // It should do nothing - so just test it doesn't crash. + [controller clearAllCachedResults]; +} + +@end diff --git a/Tests/Unit/QueryPredicateUnitTests.m b/Tests/Unit/QueryPredicateUnitTests.m new file mode 100644 index 000000000..984ec1313 --- /dev/null +++ b/Tests/Unit/QueryPredicateUnitTests.m @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFQuery.h" +#import "PFQueryUtilities.h" +#import "PFTestCase.h" + +@interface QueryPredicateUnitTests : PFTestCase + +@end + +@implementation QueryPredicateUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (void)assertPredicate:(NSPredicate *)predicate hasNormalForm:(NSPredicate *)expectedDNF { + NSPredicate *actualDNF = [PFQueryUtilities predicateByNormalizingPredicate:predicate]; + XCTAssertEqualObjects([expectedDNF predicateFormat], [actualDNF predicateFormat]); +} + +- (void)assertUnsupportedPredicate:(NSPredicate *)predicate { + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"TestObject" predicate:predicate]); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testDisjunctiveNormalForm { + [self assertPredicate:[NSPredicate predicateWithFormat:@"A = B"] + hasNormalForm:[NSPredicate predicateWithFormat:@"A = B"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"!(3 <= X)"] + hasNormalForm:[NSPredicate predicateWithFormat:@"X < 3"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"A BETWEEN {3, 5}"] + hasNormalForm:[NSPredicate predicateWithFormat:@"A >= 3 AND A <= 5"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"A BETWEEN %@", @[@3, @5]] + hasNormalForm:[NSPredicate predicateWithFormat:@"A >= 3 AND A <= 5"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"A = B AND C = D"] + hasNormalForm:[NSPredicate predicateWithFormat:@"A = B AND C = D"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"A = B OR C = D"] + hasNormalForm:[NSPredicate predicateWithFormat:@"A = B OR C = D"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"(A = B AND C = D) OR (E = F AND G = H)"] + hasNormalForm:[NSPredicate predicateWithFormat:@"(A = B AND C = D) OR (E = F AND G = H)"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"(A = B OR C = D) AND (E = F OR G = H)"] + hasNormalForm:[NSPredicate predicateWithFormat:@"(A = B AND E = F) OR (A = B AND G = H) OR " + "(C = D AND E = F) OR (C = D AND G = H)"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"NOT ((A = B AND C = D) OR (E = F AND G = H))"] + hasNormalForm:[NSPredicate predicateWithFormat:@"(A != B AND E != F) OR (A != B AND G != H) OR " + "(C != D AND E != F) OR (C != D AND G != H)"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"NOT (A <= B)"] + hasNormalForm:[NSPredicate predicateWithFormat:@"A > B"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"NOT (NOT (A = B))"] + hasNormalForm:[NSPredicate predicateWithFormat:@"A = B"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"NOT (A BETWEEN %@ OR A BETWEEN %@)", + @[@3, @5], @[@7, @9]] + hasNormalForm:[NSPredicate predicateWithFormat:@"(A < 3 AND A < 7) OR (A < 3 AND A > 9) OR " + "(A > 5 and A < 7) OR (A > 5 AND A > 9)"]]; +} + +- (void)testUnsupportedPredicates { + [self assertUnsupportedPredicate:[NSPredicate predicateWithFormat:@"x = y"]]; + [self assertUnsupportedPredicate:[NSPredicate predicateWithFormat:@"NOT (text CONTAINS 'word')"]]; + [self assertUnsupportedPredicate:[NSPredicate predicateWithFormat:@"ANY x = 3"]]; + [self assertUnsupportedPredicate:[NSPredicate predicateWithFormat:@"text LIKE 'foo'"]]; + [self assertUnsupportedPredicate:[NSPredicate predicateWithFormat:@"A=1 OR B=2 OR C=3 OR D=4 OR E=5"]]; + [self assertUnsupportedPredicate:[NSPredicate predicateWithFormat:@"$foo = 'bar'"]]; +} + +- (void)testHoistedCommonPredicates { + // These queries don't end up in DNF, because we can make them more efficient. + + [self assertPredicate:[NSPredicate predicateWithFormat:@"(A = B OR C = D) AND E = F"] + hasNormalForm:[NSPredicate predicateWithFormat:@"(A = B OR C = D) AND E = F"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"(A = B AND E = F) OR (C = D AND E = F)"] + hasNormalForm:[NSPredicate predicateWithFormat:@"(A = B OR C = D) AND E = F"]]; +} + +- (void)testNormalizeYodaConditions { + [self assertPredicate:[NSPredicate predicateWithFormat:@"3 <= X"] + hasNormalForm:[NSPredicate predicateWithFormat:@"X >= 3"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"%@ != number", @[@3, @5, @7, @9, @11]] + hasNormalForm:[NSPredicate predicateWithFormat:@"number != %@", @[@3, @5, @7, @9, @11]]]; +} + +- (void)testNormalizeContainedIn { + [self assertPredicate:[NSPredicate predicateWithFormat:@"number IN %@", @[@3, @5, @7, @9, @11]] + hasNormalForm:[NSPredicate predicateWithFormat:@"number IN %@", @[@3, @5, @7, @9, @11]]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"NOT (number IN %@)", @[@3, @5, @7, @9, @11]] + hasNormalForm:[NSPredicate predicateWithFormat:@"number notContainedIn: %@", @[@3, @5, @7, @9, @11]]]; + + // These rely on Mongo's conflation of containment with equality. + [self assertPredicate:[NSPredicate predicateWithFormat:@"3 IN Y"] + hasNormalForm:[NSPredicate predicateWithFormat:@"Y = 3"]]; + + [self assertPredicate:[NSPredicate predicateWithFormat:@"NOT (3 IN Y)"] + hasNormalForm:[NSPredicate predicateWithFormat:@"Y != 3"]]; +} + +@end diff --git a/Tests/Unit/QueryStateUnitTests.m b/Tests/Unit/QueryStateUnitTests.m new file mode 100644 index 000000000..641cb9b0c --- /dev/null +++ b/Tests/Unit/QueryStateUnitTests.m @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMacros.h" +#import "PFMutableQueryState.h" +#import "PFTestCase.h" + +@interface QueryStateUnitTests : PFTestCase + +@end + +@implementation QueryStateUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFQueryState *)sampleQueryState { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + state.limit = 100; + state.skip = 200; + state.cachePolicy = kPFCachePolicyCacheOnly; + state.maxCacheAge = 100500.0; + state.trace = YES; + state.shouldIgnoreACLs = YES; + state.shouldIncludeDeletingEventually = YES; + state.queriesLocalDatastore = YES; + state.localDatastorePinName = @"Yolo!"; + + [state setEqualityConditionWithObject:@"a" forKey:@"b"]; + [state setRelationConditionWithObject:@"c" forKey:@"d"]; + + [state sortByKey:@"a" ascending:NO]; + + [state includeKey:@"yolo"]; + [state selectKeys:@[ @"yolo" ]]; + [state redirectClassNameForKey:@"ABC"]; + return state; +} + +- (void)assertQueryState:(PFQueryState *)state equalToState:(PFQueryState *)differentState { + XCTAssertEqualObjects(state, differentState); + XCTAssertEqualObjects(state.parseClassName, differentState.parseClassName); + + XCTAssertEqual(state.limit, differentState.limit); + XCTAssertEqual(state.skip, differentState.skip); + XCTAssertEqual(state.cachePolicy, differentState.trace); + XCTAssertEqual(state.trace, differentState.trace); + XCTAssertEqual(state.shouldIgnoreACLs, differentState.shouldIgnoreACLs); + XCTAssertEqual(state.shouldIncludeDeletingEventually, differentState.shouldIncludeDeletingEventually); + XCTAssertEqual(state.queriesLocalDatastore, differentState.queriesLocalDatastore); + + XCTAssertEqualObjects(state.conditions, differentState.conditions); + XCTAssertEqualObjects(state.sortKeys, differentState.sortKeys); + XCTAssertEqualObjects(state.sortOrderString, differentState.sortOrderString); + XCTAssertEqualObjects(state.includedKeys, differentState.includedKeys); + XCTAssertEqualObjects(state.selectedKeys, differentState.selectedKeys); + XCTAssertEqualObjects(state.extraOptions, differentState.extraOptions); + + XCTAssertEqualObjects(state.localDatastorePinName, differentState.localDatastorePinName); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testDefaultValues { + PFQueryState *state = [[PFQueryState alloc] init]; + XCTAssertEqual(state.cachePolicy, kPFCachePolicyIgnoreCache); + XCTAssertEqual(state.maxCacheAge, INFINITY); + XCTAssertEqual(state.limit, -1); + + state = [[PFMutableQueryState alloc] init]; + XCTAssertEqual(state.cachePolicy, kPFCachePolicyIgnoreCache); + XCTAssertEqual(state.maxCacheAge, INFINITY); + XCTAssertEqual(state.limit, -1); +} + +- (void)testInitWithState { + PFQueryState *sampleState = [self sampleQueryState]; + PFQueryState *state = [[PFQueryState alloc] initWithState:sampleState]; + [self assertQueryState:state equalToState:sampleState]; + + state = [[PFMutableQueryState alloc] initWithState:sampleState]; + [self assertQueryState:state equalToState:sampleState]; + + state = [PFQueryState stateWithState:sampleState]; + [self assertQueryState:state equalToState:sampleState]; + + state = [PFMutableQueryState stateWithState:sampleState]; + [self assertQueryState:state equalToState:sampleState]; +} + +- (void)testInitWithClassName { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); + + state = [PFMutableQueryState stateWithParseClassName:@"Yarr"]; + XCTAssertEqualObjects(state.parseClassName, @"Yarr"); +} + +- (void)testCopying { + PFQueryState *sampleState = [self sampleQueryState]; + PFQueryState *state = [sampleState copy]; + + XCTAssertFalse([state isKindOfClass:[PFMutableQueryState class]]); + [self assertQueryState:state equalToState:sampleState]; +} + +- (void)testMutableCopying { + PFQueryState *sampleState = [self sampleQueryState]; + PFMutableQueryState *state = [sampleState mutableCopy]; + + XCTAssert([state isKindOfClass:[PFMutableQueryState class]]); + [self assertQueryState:state equalToState:sampleState]; +} + +- (void)testGenericConditions { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + [state setConditionType:@"$yolo" withObject:@"a" forKey:@"yarr"]; + + XCTAssertEqualObjects(state.conditions[@"yarr"][@"$yolo"], @"a"); +} + +- (void)testEqualityConditions { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + [state setEqualityConditionWithObject:@"a" forKey:@"yarr"]; + + XCTAssertEqualObjects(state.conditions[@"yarr"], @"a"); +} + +- (void)testRelationConditions { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + [state setRelationConditionWithObject:@"a" forKey:@"yarr"]; + + XCTAssertEqualObjects(state.conditions[@"$relatedTo"][@"object"], @"a"); + XCTAssertEqualObjects(state.conditions[@"$relatedTo"][@"key"], @"yarr"); +} + +- (void)testRemoveConditions { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + [state setEqualityConditionWithObject:@"a" forKey:@"b"]; + XCTAssertEqual(state.conditions.count, 1); + + [state removeAllConditions]; + XCTAssertEqual(state.conditions.count, 0); +} + +- (void)testSortByKey { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + + [state sortByKey:@"a" ascending:YES]; + XCTAssertEqualObjects(state.sortKeys, @[ @"a" ]); + + [state sortByKey:@"b" ascending:NO]; + XCTAssertEqualObjects(state.sortKeys, @[ @"-b" ]); +} + +- (void)testAddSortKey { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + + [state addSortKey:@"a" ascending:YES]; + XCTAssertEqualObjects(state.sortKeys, @[ @"a" ]); + + [state addSortKey:@"b" ascending:NO]; + + NSArray *sortKeys = @[ @"a", @"-b" ]; + XCTAssertEqualObjects(state.sortKeys, sortKeys); + + [state addSortKey:nil ascending:YES]; + XCTAssertEqualObjects(state.sortKeys, sortKeys); + + [state addSortKey:nil ascending:NO]; + XCTAssertEqualObjects(state.sortKeys, sortKeys); +} + +- (void)testAddSortKeysFromDescriptors { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + + NSArray *sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"a" ascending:YES], + [NSSortDescriptor sortDescriptorWithKey:@"b" ascending:NO] ]; + [state addSortKeysFromSortDescriptors:sortDescriptors]; + + NSArray *sortKeys = @[ @"a", @"-b" ]; + XCTAssertEqualObjects(state.sortKeys, sortKeys); +} + +- (void)testIncludeKeys { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + [state includeKey:@"a"]; + [state includeKey:@"b"]; + + NSSet *includedKeys = PF_SET(@"a", @"b"); + XCTAssertEqualObjects(state.includedKeys, includedKeys); +} + +- (void)testSelectKeys { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + + NSArray *selectedKeys = @[ @"a", @"b" ]; + [state selectKeys:selectedKeys]; + XCTAssertEqualObjects(state.selectedKeys, [NSSet setWithArray:selectedKeys]); + + [state selectKeys:@[]]; + XCTAssertEqualObjects(state.selectedKeys, [NSSet setWithArray:selectedKeys]); + + [state selectKeys:nil]; + XCTAssertNil(state.selectedKeys); +} + +- (void)testRedirectClassName { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + + [state redirectClassNameForKey:@"yolo"]; + XCTAssertEqualObjects(state.extraOptions, @{ @"redirectClassNameForKey" : @"yolo" }); +} + +- (void)testDebugQuickLookObject { + PFMutableQueryState *state = [[PFMutableQueryState alloc] initWithParseClassName:@"Yarr"]; + id quickLookObject = [state debugQuickLookObject]; + + XCTAssertNotNil(quickLookObject); + XCTAssertEqualObjects(quickLookObject, [[state dictionaryRepresentation] description]); +} + +@end diff --git a/Tests/Unit/QueryUnitTests.m b/Tests/Unit/QueryUnitTests.m new file mode 100644 index 000000000..7d41ceb4d --- /dev/null +++ b/Tests/Unit/QueryUnitTests.m @@ -0,0 +1,1359 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFCoreManager.h" +#import "PFMacros.h" +#import "PFMutableQueryState.h" +#import "PFQueryController.h" +#import "PFQueryPrivate.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface QueryUnitTestsInvocationVerifier : NSObject + +@end + +@implementation QueryUnitTestsInvocationVerifier + +- (void)verifyNumber:(NSNumber *)number error:(NSError *)error { +} + +- (void)verifyArray:(NSArray *)array error:(NSError *)error { +} + +- (void)verifyObject:(PFObject *)object error:(NSError *)error { +} + +@end + +@interface QueryUnitTests : PFUnitTestCase + +@end + +@implementation QueryUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFQueryController *)mockQueryControllerFindObjectsForQueryState:(PFQueryState *)state + withResult:(id)result + error:(NSError *)error { + BFTask *task = (error ? [BFTask taskWithError:error] : [BFTask taskWithResult:result]); + + id controller = PFStrictClassMock([PFQueryController class]); + OCMStub([controller findObjectsAsyncForQueryState:[OCMArg checkWithBlock:^BOOL(id obj) { + return [state isEqual:obj]; + }] + withCancellationToken:[OCMArg isNotNil] + user:[OCMArg isNil]]).andReturn(task); + [Parse _currentManager].coreManager.queryController = controller; + return controller; +} + +- (PFQueryController *)mockQueryControllerCountObjectsForQueryState:(PFQueryState *)state + withResult:(id)result + error:(NSError *)error { + BFTask *task = (error ? [BFTask taskWithError:error] : [BFTask taskWithResult:result]); + + id controller = PFStrictClassMock([PFQueryController class]); + OCMStub([controller countObjectsAsyncForQueryState:[OCMArg checkWithBlock:^BOOL(id obj) { + return [state isEqual:obj]; + }] + withCancellationToken:[OCMArg isNotNil] + user:[OCMArg isNil]]).andReturn(task); + [Parse _currentManager].coreManager.queryController = controller; + return controller; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +#pragma mark Constructors + +- (void)testConstructors { + PFQuery *query = [[PFQuery alloc] init]; + XCTAssertNotNil(query); + + query = [[PFQuery alloc] initWithClassName:@"a"]; + XCTAssertNotNil(query); + XCTAssertEqualObjects(query.parseClassName, @"a"); + XCTAssertEqualObjects(query.state.parseClassName, @"a"); + + query = [PFQuery queryWithClassName:@"b"]; + XCTAssertNotNil(query); + XCTAssertEqualObjects(query.parseClassName, @"b"); + XCTAssertEqualObjects(query.state.parseClassName, @"b"); +} + +- (void)testPredicateConstructors { + PFQuery *query = [PFQuery queryWithClassName:@"a" predicate:nil]; + XCTAssertNotNil(query); + XCTAssertEqualObjects(query.parseClassName, @"a"); + XCTAssertEqualObjects(query.state.parseClassName, @"a"); +} + +- (void)testDefaultValues { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + XCTAssertNotNil(query); + XCTAssertEqual(query.cachePolicy, kPFCachePolicyIgnoreCache); + XCTAssertEqual(query.maxCacheAge, INFINITY); + XCTAssertEqual(query.limit, -1); +} + +- (void)testOrQuery { + PFQuery *query1 = [PFQuery queryWithClassName:@"Yolo"]; + PFQuery *query2 = [PFQuery queryWithClassName:@"Yolo"]; + + PFQuery *query = [PFQuery orQueryWithSubqueries:@[ query1, query2 ]]; + XCTAssertEqualObjects(query.state.conditions[@"$or"], (@[ query1, query2 ])); +} + +#pragma mark Pagination + +- (void)testLimit { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.limit = 100500; + XCTAssertEqual(query.limit, 100500); + XCTAssertEqual(query.state.limit, 100500); +} + +- (void)testSkip { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.skip = 500; + XCTAssertEqual(query.skip, 500); + XCTAssertEqual(query.state.skip, 500); +} + +#pragma mark Caching + +- (void)testCachePolicy { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.cachePolicy = kPFCachePolicyNetworkOnly; + XCTAssertEqual(query.cachePolicy, kPFCachePolicyNetworkOnly); + XCTAssertEqual(query.state.cachePolicy, kPFCachePolicyNetworkOnly); +} + +- (void)testCachePolicyWithLocalDatastore { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [Parse enableLocalDatastore]; + [Parse setApplicationId:@"a" clientKey:@"b"]; + + PFQuery *query = [[PFQuery queryWithClassName:@"a"] fromLocalDatastore]; + PFAssertThrowsInconsistencyException([query setCachePolicy:kPFCachePolicyNetworkOnly]); + + query = [[PFQuery queryWithClassName:@"a"] fromPin]; + PFAssertThrowsInconsistencyException([query setCachePolicy:kPFCachePolicyNetworkOnly]); +} + +- (void)testMaxCacheAge { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.maxCacheAge = 100500.0; + XCTAssertEqual(query.maxCacheAge, 100500.0); + XCTAssertEqual(query.state.maxCacheAge, 100500.0); +} + +- (void)testHasCachedResult { + id queryController = PFStrictClassMock([PFQueryController class]); + [Parse _currentManager].coreManager.queryController = queryController; + + PFQuery *query = [PFQuery queryWithClassName:@"A"]; + + OCMStub([queryController hasCachedResultForQueryState:query.state sessionToken:nil]).andReturn(YES); + XCTAssertTrue([query hasCachedResult]); +} + +- (void)testClearCachedResult { + id queryController = PFStrictClassMock([PFQueryController class]); + [Parse _currentManager].coreManager.queryController = queryController; + + PFQuery *query = [PFQuery queryWithClassName:@"A"]; + + OCMExpect([queryController clearCachedResultForQueryState:query.state sessionToken:nil]); + + [query clearCachedResult]; + OCMVerifyAll(queryController); +} + +- (void)testClearAllCachedResults { + id queryController = PFStrictClassMock([PFQueryController class]); + [Parse _currentManager].coreManager.queryController = queryController; + OCMExpect([queryController clearAllCachedResults]); + + [PFQuery clearAllCachedResults]; + OCMVerifyAll(queryController); +} + +#pragma mark Other Properties + +- (void)testParseClassName { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + XCTAssertEqualObjects(query.parseClassName, @"a"); + + query.parseClassName = @"b"; + XCTAssertEqualObjects(query.parseClassName, @"b"); +} + +- (void)testTrace { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.trace = YES; + XCTAssertTrue(query.trace); + XCTAssertTrue(query.state.trace); +} + +- (void)testIncludeKey { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query includeKey:@"yolo"]; + XCTAssertEqualObjects(query.state.includedKeys, (PF_SET(@"yolo"))); + + [query includeKey:@"yolo1"]; + XCTAssertEqualObjects(query.state.includedKeys, (PF_SET(@"yolo", @"yolo1"))); + XCTAssertTrue([query.state.includedKeys containsObject:@"yolo"]); + XCTAssertTrue([query.state.includedKeys containsObject:@"yolo1"]); +} + +- (void)testSelectKeys { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query selectKeys:@[ @"a", @"a" ]]; + XCTAssertEqualObjects(query.state.selectedKeys, (PF_SET(@"a"))); + + [query selectKeys:@[ @"a", @"b" ]]; + XCTAssertEqualObjects(query.state.selectedKeys, (PF_SET(@"a", @"b"))); +} + +#pragma mark Order + +- (void)testOrderByAscending { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query orderByAscending:@"yarr"]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"yarr" ]); + [query orderByAscending:@"yarr1"]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"yarr1" ]); +} + +- (void)testAddAscendingOrder { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query addAscendingOrder:@"yarr"]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"yarr" ]); + [query addAscendingOrder:@"yarr1"]; + XCTAssertEqualObjects(query.state.sortKeys, (@[ @"yarr", @"yarr1" ])); +} + +- (void)testOrderByDescending { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query orderByDescending:@"yarr"]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"-yarr" ]); + [query orderByDescending:@"yarr1"]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"-yarr1" ]); +} + +- (void)testAddDescendingOrder { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query addDescendingOrder:@"yarr"]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"-yarr" ]); + [query addDescendingOrder:@"yarr1"]; + XCTAssertEqualObjects(query.state.sortKeys, (@[ @"-yarr", @"-yarr1" ])); +} + +- (void)testSortBySortDescriptors { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query orderBySortDescriptor:[NSSortDescriptor sortDescriptorWithKey:@"yarr" ascending:YES]]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"yarr" ]); + + [query orderBySortDescriptor:[NSSortDescriptor sortDescriptorWithKey:@"yarr" ascending:NO]]; + XCTAssertEqualObjects(query.state.sortKeys, @[ @"-yarr" ]); + + [query orderBySortDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"yarr" ascending:YES], + [NSSortDescriptor sortDescriptorWithKey:@"yarr1" ascending:NO] ]]; + XCTAssertEqualObjects(query.state.sortKeys, (@[ @"yarr", @"-yarr1" ])); +} + +#pragma mark Conditions + +- (void)testWhereKeyExists { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKeyExists:@"yolo"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$exists" : @YES} }); +} + +- (void)testWhereKeyDoesNotExist { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKeyDoesNotExist:@"yolo"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$exists" : @NO} }); +} + +- (void)testWhereKeyEqualTo { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" equalTo:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @"yarr" }); +} + +- (void)testWhereKeyNotEqualTo { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" notEqualTo:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$ne" : @"yarr"} }); +} + +- (void)testWhereEqualityValidation { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + XCTAssertNoThrow([query whereKey:@"a" equalTo:@"b"]); + XCTAssertNoThrow([query whereKey:@"a" equalTo:@1]); + XCTAssertNoThrow([query whereKey:@"a" equalTo:[NSDate date]]); + XCTAssertNoThrow([query whereKey:@"a" equalTo:[NSNull null]]); + XCTAssertNoThrow([query whereKey:@"a" equalTo:[[PFGeoPoint alloc] init]]); + XCTAssertNoThrow([query whereKey:@"a" equalTo:[PFObject objectWithClassName:@"Yolo"]]); + + PFAssertThrowsInvalidArgumentException([query whereKey:@"a" equalTo:[NSValue valueWithNonretainedObject:@1]]); +} + +- (void)testWhereKeyLessThan { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" lessThan:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$lt" : @"yarr"} }); +} + +- (void)testWhereKeyLessThanOrEqualTo { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" lessThanOrEqualTo:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$lte" : @"yarr"} }); +} + +- (void)testWhereKeyGreaterThan { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" greaterThan:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$gt" : @"yarr"} }); +} + +- (void)testWhereKeyGreaterThanOrEqualTo { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" greaterThanOrEqualTo:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$gte" : @"yarr"} }); +} + +- (void)testWhereOrderingClauseValidation { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + XCTAssertNoThrow([query whereKey:@"a" lessThanOrEqualTo:@"b"]); + XCTAssertNoThrow([query whereKey:@"a" lessThan:@1]); + XCTAssertNoThrow([query whereKey:@"a" greaterThan:[NSDate date]]); + PFAssertThrowsInvalidArgumentException([query whereKey:@"a" lessThanOrEqualTo:[NSNull null]]); + PFAssertThrowsInvalidArgumentException([query whereKey:@"a" lessThan:[[PFGeoPoint alloc] init]]); + PFAssertThrowsInvalidArgumentException([query whereKey:@"a" greaterThan:[PFObject objectWithClassName:@"Yolo"]]); +} + +- (void)testWhereContainedIn { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" containedIn:@[ @"yarr" ]]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$in" : @[ @"yarr" ]} }); +} + +- (void)testWhereNotContainedIn { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" notContainedIn:@[ @"yarr" ]]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$nin" : @[ @"yarr" ]} }); +} + +- (void)testWhereContainsAllObjectsInArray { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" containsAllObjectsInArray:@[ @"yarr" ]]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$all" : @[ @"yarr" ]} }); +} + +- (void)testWhereKeyNearGeoPoint { + PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]; + + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" nearGeoPoint:geoPoint]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$nearSphere" : geoPoint} }); +} + +- (void)testWhereKeyNearGeoPointWithinMiles { + PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]; + + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" nearGeoPoint:geoPoint withinMiles:3958.8]; + + // $maxDistance is the max distance in radians relative to radius of earth? + XCTAssertEqualObjects(query.state.conditions, (@{ @"yolo" : @{@"$nearSphere" : geoPoint, @"$maxDistance" : @1} })); +} + +- (void)testWhereKeyNearGeoPointWithinKilometers { + PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]; + + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" nearGeoPoint:geoPoint withinKilometers:6371.0]; + + // $maxDistance is the max distance in radians relative to radius of earth? + XCTAssertEqualObjects(query.state.conditions, (@{ @"yolo" : @{@"$nearSphere" : geoPoint, @"$maxDistance" : @1} })); +} + +- (void)testWhereKeyNearGeoPointWithinRadians { + PFGeoPoint *geoPoint = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]; + + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" nearGeoPoint:geoPoint withinRadians:10.0]; + XCTAssertEqualObjects(query.state.conditions, (@{ @"yolo" : @{@"$nearSphere" : geoPoint, @"$maxDistance" : @10} })); +} + +- (void)testWhereKeyWithinGeobox { + PFGeoPoint *geoPoint1 = [PFGeoPoint geoPointWithLatitude:10.0 longitude:20.0]; + PFGeoPoint *geoPoint2 = [PFGeoPoint geoPointWithLatitude:20.0 longitude:30.0]; + + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" withinGeoBoxFromSouthwest:geoPoint1 toNortheast:geoPoint2]; + XCTAssertEqualObjects(query.state.conditions, (@{ @"yolo" : @{@"$within" : @{@"$box" : @[ geoPoint1, geoPoint2 ]}} })); +} + +- (void)testWhereKeyMatchesRegex { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" matchesRegex:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$regex" : @"yarr"} }); +} + +- (void)testWhereKeyMatchesRegexModifiers { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" matchesRegex:@"yarr" modifiers:@"i"]; + XCTAssertEqualObjects(query.state.conditions, (@{ @"yolo" : @{@"$regex" : @"yarr", @"$options" : @"i"} })); +} + +- (void)testWhereKeyContainsString { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" containsString:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$regex" : @"\\Qyarr\\E"} }); +} + +- (void)testWhereKeyHasPrefix { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" hasPrefix:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$regex" : @"^\\Qyarr\\E"} }); +} + +- (void)testWhereKeyHasSuffix { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" hasSuffix:@"yarr"]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$regex" : @"\\Qyarr\\E$"} }); +} + +- (void)testWhereKeyMatchesKeyInQuery { + PFQuery *inQuery = [PFQuery queryWithClassName:@"b"]; + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" matchesKey:@"yolo1" inQuery:inQuery]; + XCTAssertEqualObjects(query.state.conditions, (@{ @"yolo" : @{@"$select" : @{@"key" : @"yolo1", @"query" : inQuery}} })); +} + +- (void)testWhereKeyDoesNotMatchKeyInQuery { + PFQuery *inQuery = [PFQuery queryWithClassName:@"b"]; + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" doesNotMatchKey:@"yolo1" inQuery:inQuery]; + XCTAssertEqualObjects(query.state.conditions, (@{ @"yolo" : @{@"$dontSelect" : @{@"key" : @"yolo1", @"query" : inQuery}} })); +} + +- (void)testWhereKeyMatchesQuery { + PFQuery *inQuery = [PFQuery queryWithClassName:@"b"]; + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" matchesQuery:inQuery]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$inQuery" : inQuery} }); +} + +- (void)testWhereKeyDoesNotMatchQuery { + PFQuery *inQuery = [PFQuery queryWithClassName:@"b"]; + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"yolo" doesNotMatchQuery:inQuery]; + XCTAssertEqualObjects(query.state.conditions, @{ @"yolo" : @{@"$notInQuery" : inQuery} }); +} + +- (void)testWhereRelatedToObject { + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereRelatedToObject:object fromKey:@"yolo"]; + XCTAssertEqualObjects(query.state.conditions, (@{ @"$relatedTo" : @{@"key" : @"yolo", @"object" : object} })); +} + +- (void)testRedirectClassNameForKey { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query redirectClassNameForKey:@"yolo"]; + XCTAssertEqualObjects(query.state.extraOptions, @{ @"redirectClassNameForKey" : @"yolo" }); +} + +#pragma mark Get Objects by Id + +- (void)testGetObjectOfClassObjectId { + PFMutableQueryState *state = [PFMutableQueryState stateWithParseClassName:@"Yolo"]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + PFObject *result = [PFQuery getObjectOfClass:@"Yolo" objectId:@"yarr"]; + XCTAssertEqual(result, object); +} + +- (void)testGetObjectOfClassObjectIdError { + PFMutableQueryState *state = [PFMutableQueryState stateWithParseClassName:@"Yolo"]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + NSError *originalError = [NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:originalError]; + + NSError *error = nil; + PFObject *result = [PFQuery getObjectOfClass:@"Yolo" objectId:@"yarr" error:&error]; + XCTAssertNil(result); + XCTAssertEqualObjects(error, originalError); +} + +- (void)testGetObjectWithId { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state mutableCopy]; + [state removeAllConditions]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + PFObject *result = [query getObjectWithId:@"yarr"]; + XCTAssertEqual(result, object); +} + +- (void)testGetObjectWithIdError { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state mutableCopy]; + [state removeAllConditions]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + NSError *originalError = [NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ @"yolo" ] error:originalError]; + + NSError *error = nil; + PFObject *result = [query getObjectWithId:@"yarr" error:&error]; + XCTAssertNil(result); + XCTAssertEqualObjects(error, originalError); +} + +- (void)testGetObjectWithIdNotFoundError { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state mutableCopy]; + [state removeAllConditions]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + [self mockQueryControllerFindObjectsForQueryState:state + withResult:@[] + error:nil]; + + NSError *error = nil; + PFObject *result = [query getObjectWithId:@"yarr" error:&error]; + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, kPFErrorObjectNotFound); +} + +- (void)testGetObjectWithNilId { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [Parse _currentManager].coreManager.queryController = PFStrictClassMock([PFQueryController class]); + NSError *error = nil; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertNil([query getObjectWithId:nil error:&error]); +#pragma clang diagnostic pop + XCTAssertNil(error); +} + +- (void)testGetObjectWithIdViaTask { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state mutableCopy]; + [state removeAllConditions]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[query getObjectInBackgroundWithId:@"yarr"] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, object); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetObjectWithIdViaBlock { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state mutableCopy]; + [state removeAllConditions]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [query getObjectInBackgroundWithId:@"yarr" block:^(PFObject *result, NSError *error) { + XCTAssertEqual(object, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetObjectWithIdViaBlockCacheThenNetwork { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.cachePolicy = kPFCachePolicyCacheThenNetwork; + [query whereKey:@"a" equalTo:@"b"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + + id controller = PFStrictClassMock([PFQueryController class]); + [OCMStub([controller findObjectsAsyncForQueryState:[OCMArg checkWithBlock:^BOOL(id obj) { + PFMutableQueryState *state = [query.state mutableCopy]; + [state removeAllConditions]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + state.cachePolicy = kPFCachePolicyCacheOnly; + if ([state isEqual:obj]) { + return YES; + } + state.cachePolicy = kPFCachePolicyNetworkOnly; + if ([state isEqual:obj]) { + return YES; + } + return NO; + }] withCancellationToken:OCMOCK_ANY user:nil]) andReturn:[BFTask taskWithResult:@[ object ]]]; + [Parse _currentManager].coreManager.queryController = controller; + + XCTestExpectation *cacheExpectation = [self expectationWithDescription:@"cacheExpectation"]; + XCTestExpectation *networkExpectation = [self expectationWithDescription:@"networkExpectation"]; + __block NSUInteger counter = 0; + [query getObjectInBackgroundWithId:@"yarr" block:^(PFObject *result, NSError *error) { + if (counter == 0) { + XCTAssertEqual(result, object); + XCTAssertNil(error); + [cacheExpectation fulfill]; + } else if (counter == 1) { + XCTAssertEqual(result, object); + XCTAssertNil(error); + [networkExpectation fulfill]; + } else { + XCTFail(@"PFQuery.countObjectsInBackgroundWithBlock called more than twice."); + } + counter++; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetObjectWithIdViaInvocation { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state mutableCopy]; + [state removeAllConditions]; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + state.limit = 1; + state.skip = 0; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + id verifier = PFStrictClassMock([QueryUnitTestsInvocationVerifier class]); + OCMStub([verifier verifyObject:object error:nil]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + [query getObjectInBackgroundWithId:@"yarr" target:verifier selector:@selector(verifyObject:error:)]; + [self waitForTestExpectations]; +} + +#pragma mark Get User Objects + +- (void)testGetUserObjectWithId { + PFMutableQueryState *state = [PFMutableQueryState stateWithParseClassName:@"_User"]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + PFObject *result = [PFQuery getUserObjectWithId:@"yarr"]; + XCTAssertEqual(result, object); +} + +- (void)testGetUserObjectWithIdError { + PFMutableQueryState *state = [PFMutableQueryState stateWithParseClassName:@"_User"]; + state.limit = 1; + state.skip = 0; + [state setEqualityConditionWithObject:@"yarr" forKey:@"objectId"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + NSError *originalError = [NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:originalError]; + + NSError *error = nil; + PFObject *result = [PFQuery getUserObjectWithId:@"yarr" error:&error]; + XCTAssertNil(result); + XCTAssertEqualObjects(error, originalError); +} + +- (void)testQueryForUser { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + PFQuery *query = [PFQuery queryForUser]; +#pragma clang diagnostic pop + + PFMutableQueryState *state = [PFMutableQueryState stateWithParseClassName:@"_User"]; + XCTAssertEqualObjects(query.state, state); +} + +#pragma mark Find Objects + +- (void)testFindObjects { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [self mockQueryControllerFindObjectsForQueryState:query.state withResult:@[ @"yolo" ] error:nil]; + NSArray *result = [query findObjects]; + XCTAssertEqualObjects(result, @[ @"yolo" ]); +} + +- (void)testFindObjectsError { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + NSError *originalError = [NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]; + [self mockQueryControllerFindObjectsForQueryState:query.state withResult:@[ @"yolo" ] error:originalError]; + + NSError *error = nil; + NSArray *result = [query findObjects:&error]; + XCTAssertNil(result); + XCTAssertEqualObjects(error, originalError); +} + +- (void)testFindObjectsViaTask { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [self mockQueryControllerFindObjectsForQueryState:query.state withResult:@[ @"yolo" ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[query findObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @[ @"yolo" ]); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsViaBlock { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [self mockQueryControllerFindObjectsForQueryState:query.state withResult:@[ @"yolo" ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [query findObjectsInBackgroundWithBlock:^(NSArray *results, NSError *error) { + XCTAssertEqualObjects(results, @[ @"yolo" ]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsViaBlockCacheThenNetwork { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.cachePolicy = kPFCachePolicyCacheThenNetwork; + [query whereKey:@"a" equalTo:@"b"]; + + id controller = PFStrictClassMock([PFQueryController class]); + [OCMStub([controller findObjectsAsyncForQueryState:[OCMArg checkWithBlock:^BOOL(id obj) { + PFMutableQueryState *state = [query.state mutableCopy]; + state.cachePolicy = kPFCachePolicyCacheOnly; + if ([state isEqual:obj]) { + return YES; + } + state.cachePolicy = kPFCachePolicyNetworkOnly; + if ([state isEqual:obj]) { + return YES; + } + return NO; + }] withCancellationToken:OCMOCK_ANY user:nil]) andReturn:[BFTask taskWithResult:@[ @"yolo1" ]]]; + [Parse _currentManager].coreManager.queryController = controller; + + XCTestExpectation *cacheExpectation = [self expectationWithDescription:@"cacheExpectation"]; + XCTestExpectation *networkExpectation = [self expectationWithDescription:@"networkExpectation"]; + __block NSUInteger counter = 0; + [query findObjectsInBackgroundWithBlock:^(NSArray *results, NSError *error) { + if (counter == 0) { + XCTAssertEqualObjects(results, @[ @"yolo1" ]); + XCTAssertNil(error); + [cacheExpectation fulfill]; + } else if (counter == 1) { + XCTAssertEqualObjects(results, @[ @"yolo1" ]); + XCTAssertNil(error); + [networkExpectation fulfill]; + } else { + XCTFail(@"PFQuery.findObjectsInBackgroundWithBlock called more than twice."); + } + counter++; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsViaInvocation { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:query.state withResult:@[ object ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + id verifier = PFStrictClassMock([QueryUnitTestsInvocationVerifier class]); + OCMStub([verifier verifyArray:@[ object ] error:nil]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + [query findObjectsInBackgroundWithTarget:verifier selector:@selector(verifyArray:error:)]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsViaTaskCancellation { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + id controller = PFStrictClassMock([PFQueryController class]); + [[OCMStub([controller findObjectsAsyncForQueryState:query.state + withCancellationToken:[OCMArg isNotNil] + user:nil]) andDo:^(NSInvocation *invocation) { + [query cancel]; + + __unsafe_unretained BFCancellationToken *cancellationToken = nil; + [invocation getArgument:&cancellationToken atIndex:3]; + XCTAssertTrue(cancellationToken.cancellationRequested); + }] andReturn:[BFTask cancelledTask]]; + [Parse _currentManager].coreManager.queryController = controller; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[query findObjectsInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testFindObjectsConcurrently { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + id controller = PFStrictClassMock([PFQueryController class]); + [[OCMStub([controller findObjectsAsyncForQueryState:query.state + withCancellationToken:[OCMArg isNotNil] + user:nil]) andDo:^(NSInvocation *invocation) { + XCTAssertThrows([query findObjects]); + }] andReturn:[BFTask cancelledTask]]; + [Parse _currentManager].coreManager.queryController = controller; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[query findObjectsInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +#pragma mark Get First Object + +- (void)testGetFirstObject { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state copy]; + state.limit = 1; + state.skip = 0; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + PFObject *result = [query getFirstObject]; + XCTAssertEqual(result, object); +} + +- (void)testGetFirstObjectError { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state copy]; + state.limit = 1; + state.skip = 0; + + NSError *originalError = [NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ @"yolo" ] error:originalError]; + + NSError *error = nil; + PFObject *result = [query getFirstObject:&error]; + XCTAssertNil(result); + XCTAssertEqualObjects(error, originalError); +} + +- (void)testGetFirstObjectNotFoundError { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state copy]; + state.limit = 1; + state.skip = 0; + [self mockQueryControllerFindObjectsForQueryState:state + withResult:@[] + error:nil]; + + NSError *error = nil; + PFObject *result = [query getFirstObject:&error]; + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, kPFErrorObjectNotFound); +} + +- (void)testGetFirstObjectViaTask { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state copy]; + state.limit = 1; + state.skip = 0; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[query getFirstObjectInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, object); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetFirstObjectViaBlock { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state copy]; + state.limit = 1; + state.skip = 0; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [query getFirstObjectInBackgroundWithBlock:^(PFObject *result, NSError *error) { + XCTAssertEqual(object, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetFirstObjectViaBlockCacheThenNetwork { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.cachePolicy = kPFCachePolicyCacheThenNetwork; + [query whereKey:@"a" equalTo:@"b"]; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + + id controller = PFStrictClassMock([PFQueryController class]); + [OCMStub([controller findObjectsAsyncForQueryState:[OCMArg checkWithBlock:^BOOL(id obj) { + PFMutableQueryState *state = [query.state mutableCopy]; + state.limit = 1; + state.skip = 0; + state.cachePolicy = kPFCachePolicyCacheOnly; + if ([state isEqual:obj]) { + return YES; + } + state.cachePolicy = kPFCachePolicyNetworkOnly; + if ([state isEqual:obj]) { + return YES; + } + return NO; + }] withCancellationToken:OCMOCK_ANY user:nil]) andReturn:[BFTask taskWithResult:@[ object ]]]; + [Parse _currentManager].coreManager.queryController = controller; + + XCTestExpectation *cacheExpectation = [self expectationWithDescription:@"cacheExpectation"]; + XCTestExpectation *networkExpectation = [self expectationWithDescription:@"networkExpectation"]; + __block NSUInteger counter = 0; + [query getFirstObjectInBackgroundWithBlock:^(PFObject *result, NSError *error) { + if (counter == 0) { + XCTAssertEqual(result, object); + XCTAssertNil(error); + [cacheExpectation fulfill]; + } else if (counter == 1) { + XCTAssertEqual(result, object); + XCTAssertNil(error); + [networkExpectation fulfill]; + } else { + XCTFail(@"PFQuery.countObjectsInBackgroundWithBlock called more than twice."); + } + counter++; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetFirstObjectViaInvocation { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + PFMutableQueryState *state = [query.state copy]; + state.limit = 1; + state.skip = 0; + + PFObject *object = [PFObject objectWithClassName:@"Yolo"]; + [self mockQueryControllerFindObjectsForQueryState:state withResult:@[ object ] error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + id verifier = PFStrictClassMock([QueryUnitTestsInvocationVerifier class]); + OCMStub([verifier verifyObject:object error:nil]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + [query getFirstObjectInBackgroundWithTarget:verifier selector:@selector(verifyObject:error:)]; + [self waitForTestExpectations]; +} + +#pragma mark Count Objects + +- (void)testCountObjects { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [self mockQueryControllerCountObjectsForQueryState:query.state withResult:@100500 error:nil]; + NSInteger result = [query countObjects]; + XCTAssertEqual(result, 100500); +} + +- (void)testCountObjectsError { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + NSError *originalError = [NSError errorWithDomain:@"TestDomain" code:100500 userInfo:nil]; + [self mockQueryControllerCountObjectsForQueryState:query.state withResult:@[ @"yolo" ] error:originalError]; + + NSError *error = nil; + NSInteger result = [query countObjects:&error]; + XCTAssertEqual(result, -1); + XCTAssertEqualObjects(error, originalError); +} + +- (void)testCountObjectsViaTask { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [self mockQueryControllerCountObjectsForQueryState:query.state withResult:@100500 error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[query countObjectsInBackground] continueWithSuccessBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.result, @100500); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testCountObjectsViaBlock { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [self mockQueryControllerCountObjectsForQueryState:query.state withResult:@100500 error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [query countObjectsInBackgroundWithBlock:^(int result, NSError *error) { + XCTAssertEqual(result, 100500); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testCountObjectsViaBlockCacheThenNetwork { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + query.cachePolicy = kPFCachePolicyCacheThenNetwork; + [query whereKey:@"a" equalTo:@"b"]; + + id controller = PFStrictClassMock([PFQueryController class]); + [OCMStub([controller countObjectsAsyncForQueryState:[OCMArg checkWithBlock:^BOOL(id obj) { + PFMutableQueryState *state = [query.state mutableCopy]; + state.cachePolicy = kPFCachePolicyCacheOnly; + if ([state isEqual:obj]) { + return YES; + } + state.cachePolicy = kPFCachePolicyNetworkOnly; + if ([state isEqual:obj]) { + return YES; + } + return NO; + }] withCancellationToken:OCMOCK_ANY user:nil]) andReturn:[BFTask taskWithResult:@100500]]; + [Parse _currentManager].coreManager.queryController = controller; + + XCTestExpectation *cacheExpectation = [self expectationWithDescription:@"cacheExpectation"]; + XCTestExpectation *networkExpectation = [self expectationWithDescription:@"networkExpectation"]; + __block NSUInteger counter = 0; + [query countObjectsInBackgroundWithBlock:^(int result, NSError *error) { + if (counter == 0) { + XCTAssertEqual(result, 100500); + XCTAssertNil(error); + [cacheExpectation fulfill]; + } else if (counter == 1) { + XCTAssertEqual(result, 100500); + XCTAssertNil(error); + [networkExpectation fulfill]; + } else { + XCTFail(@"PFQuery.countObjectsInBackgroundWithBlock called more than twice."); + } + counter++; + }]; + [self waitForTestExpectations]; +} + +- (void)testCountObjectsViaInvocation { + PFQuery *query = [PFQuery queryWithClassName:@"a"]; + [query whereKey:@"a" equalTo:@"b"]; + + [self mockQueryControllerCountObjectsForQueryState:query.state withResult:@100500 error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + id verifier = PFStrictClassMock([QueryUnitTestsInvocationVerifier class]); + OCMStub([verifier verifyNumber:@100500 error:nil]).andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + [query countObjectsInBackgroundWithTarget:verifier selector:@selector(verifyNumber:error:)]; + [self waitForTestExpectations]; +} + +#pragma mark Local Datastore + +- (void)testFromLocalDatastore { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [Parse enableLocalDatastore]; + [Parse setApplicationId:@"a" clientKey:@"b"]; + + PFQuery *query = [PFQuery queryWithClassName:@"Yarr"]; + [query fromLocalDatastore]; + + XCTAssertTrue(query.state.queriesLocalDatastore); + XCTAssertNil(query.state.localDatastorePinName); +} + +- (void)testFromPin { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [Parse enableLocalDatastore]; + [Parse setApplicationId:@"a" clientKey:@"b"]; + + PFQuery *query = [PFQuery queryWithClassName:@"Yarr"]; + [query fromPin]; + + XCTAssertTrue(query.state.queriesLocalDatastore); + XCTAssertEqualObjects(query.state.localDatastorePinName, PFObjectDefaultPin); +} + +- (void)testFromPinWithName { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [Parse enableLocalDatastore]; + [Parse setApplicationId:@"a" clientKey:@"b"]; + + PFQuery *query = [PFQuery queryWithClassName:@"Yarr"]; + [query fromPinWithName:@"Yolo"]; + + XCTAssertTrue(query.state.queriesLocalDatastore); + XCTAssertEqualObjects(query.state.localDatastorePinName, @"Yolo"); +} + +- (void)testIgnoreACLs { + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; + [Parse enableLocalDatastore]; + [Parse setApplicationId:@"a" clientKey:@"b"]; + + PFQuery *query = [PFQuery queryWithClassName:@"Yarr"]; + [query ignoreACLs]; + + XCTAssertTrue(query.state.shouldIgnoreACLs); +} + +#pragma mark Copying + +- (void)testNSCopying { + PFQuery *query = [PFQuery queryWithClassName:@"Yarr"]; + + [query whereKey:@"a" equalTo:@"bar"]; + [query orderByAscending:@"b"]; + [query includeKey:@"c"]; + [query selectKeys:@[ @"d" ]]; + [query redirectClassNameForKey:@"e"]; + + query.limit = 10; + query.skip = 20; + + query.cachePolicy = kPFCachePolicyIgnoreCache; + query.maxCacheAge = 30.0; + + query.trace = YES; + + PFQuery *queryCopy = [query copy]; + + XCTAssertEqualObjects(queryCopy.parseClassName, query.parseClassName); + + XCTAssertEqualObjects(queryCopy.state.conditions[@"a"], query.state.conditions[@"a"]); + XCTAssertEqualObjects(queryCopy.state.sortOrderString, query.state.sortOrderString); + XCTAssertEqualObjects([queryCopy.state.includedKeys anyObject], [query.state.includedKeys anyObject]); + XCTAssertEqualObjects([queryCopy.state.selectedKeys anyObject], [query.state.selectedKeys anyObject]); + XCTAssertEqualObjects([[queryCopy.state.extraOptions allValues] lastObject], + [[query.state.extraOptions allValues] lastObject]); + + XCTAssertEqual(queryCopy.limit, query.limit); + XCTAssertEqual(queryCopy.skip, query.skip); + + XCTAssertEqual(queryCopy.cachePolicy, query.cachePolicy); + XCTAssertEqual(queryCopy.maxCacheAge, query.maxCacheAge); + + XCTAssertEqual(queryCopy.trace, query.trace); +} + +#pragma mark Predicates + +- (void)testQueryFromValidComparisonPredicate { + PFQuery *query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a == \"b\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @"b"); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a != \"b\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$ne" : @"b" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a < \"b\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$lt" : @"b" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a <= \"b\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$lte" : @"b" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a > \"b\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$gt" : @"b" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a >= \"b\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$gte" : @"b" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a BEGINSWITH \"b\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$regex" : @"^\\Qb\\E" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"%@ IN a", @1]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @1); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a IN %@", @[ @1, @2, @3 ]]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], (@{ @"$in" : @[ @1, @2, @3 ] })); + + PFQuery *inQuery = [PFQuery queryWithClassName:@"Yolo"]; + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a IN %@", inQuery]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$inQuery" : inQuery }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a IN { 1, 2, 3 }"]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], (@{ @"$in" : @[ @1, @2, @3 ] })); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a IN SELF"]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$exists" : @YES }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"NOT (a IN %@)", inQuery]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$notInQuery" : inQuery }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"NOT (a IN %@)", @[ @1, @2, @3 ]]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], (@{ @"$nin" : @[ @1, @2, @3 ] })); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"NOT (a IN { 1, 2, 3 })"]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], (@{ @"$nin" : @[ @1, @2, @3 ] })); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"NOT (a IN SELF)"]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$exists" : @NO }); +} + +- (void)testQueryFromInvalidComparisonPredicate { + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a = b"]]); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a CONTAINS \"b\""]]); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a ENDSWITH \"b\""]]); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a MATCHES \"b\""]]); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a LIKE \"b\""]]); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a IN b"]]); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"NOT (a IN b)"]]); + + NSComparisonPredicate *mockPredicate = PFClassMock([NSComparisonPredicate class]); + OCMStub(mockPredicate.predicateOperatorType).andReturn(100500); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:mockPredicate]); + + NSComparisonPredicate *predicate = [[NSComparisonPredicate alloc] initWithLeftExpression:[NSExpression expressionForAnyKey] + rightExpression:[NSExpression expressionForAnyKey] + customSelector:@selector(isEqual:)]; + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:predicate]); + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:(NSPredicate *)@"Yolo"]); +} + +- (void)testQueryFromValidCompoundPredicate { + PFQuery *query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a != \"b\" AND a != \"c\""]]; + XCTAssertEqualObjects(query.state.conditions[@"a"], @{ @"$ne" : @"c" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a != \"b\" OR a != \"c\""]]; + XCTAssertEqual([query.state.conditions[@"$or"] count], 2); + XCTAssertEqualObjects([(PFQuery *)[query.state.conditions[@"$or"] firstObject] state].conditions[@"a"], @{ @"$ne" : @"b" }); + XCTAssertEqualObjects([(PFQuery *)[query.state.conditions[@"$or"] lastObject] state].conditions[@"a"], @{ @"$ne" : @"c" }); + + query = [PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"(a != \"b\" AND b != \"c\") OR (a != \"b\" AND b != \"e\")"]]; + XCTAssertEqual([query.state.conditions[@"$or"] count], 2); + XCTAssertEqualObjects([(PFQuery *)[query.state.conditions[@"$or"] firstObject] state].conditions[@"b"], @{ @"$ne" : @"c" }); + XCTAssertEqualObjects([(PFQuery *)[query.state.conditions[@"$or"] lastObject] state].conditions[@"b"], @{ @"$ne" : @"e" }); +} + +- (void)testQueryFromInvalidCompoundPredicate { + PFAssertThrowsInconsistencyException([PFQuery queryWithClassName:@"A" predicate:[NSPredicate predicateWithFormat:@"a != \"b\" OR a != \"c\" OR a != \"d\" OR a != \"e\" OR a != \"f\""]]); +} + +#pragma mark NSObject + +- (void)testHash { + PFQuery *queryA = [PFQuery queryWithClassName:@"aClass"]; + PFQuery *queryB = [PFQuery queryWithClassName:@"aClass"]; + XCTAssertEqual([queryA hash], [queryB hash]); +} + +@end diff --git a/Tests/Unit/QueryUtilitiesTests.m b/Tests/Unit/QueryUtilitiesTests.m new file mode 100644 index 000000000..6b8f18de1 --- /dev/null +++ b/Tests/Unit/QueryUtilitiesTests.m @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFConstants.h" +#import "PFQueryUtilities.h" +#import "PFTestCase.h" + +@interface QueryUtilitiesTests : PFTestCase + +@end + +@implementation QueryUtilitiesTests + +///-------------------------------------- +#pragma mark - Utilities +///-------------------------------------- + +- (NSPredicate *)sampleCompoundPredicate { + return [NSPredicate predicateWithFormat:@""]; +} + +- (void)assertPredicateFormat:(NSString *)toNormalize normalizesToFormat:(NSString *)expected { + NSPredicate *normalizedPredicate = [NSPredicate predicateWithFormat:toNormalize]; + NSPredicate *expectedPredicate = [NSPredicate predicateWithFormat:expected]; + XCTAssertEqualObjects(expectedPredicate, [PFQueryUtilities predicateByNormalizingPredicate:normalizedPredicate]); + XCTAssertEqualObjects(expectedPredicate, [PFQueryUtilities predicateByNormalizingPredicate:expectedPredicate]); +} + +- (void)assertPredicateThrows:(NSString *)expected { + NSPredicate *expectedPredicate = [NSPredicate predicateWithFormat:expected]; + XCTAssertThrows([PFQueryUtilities predicateByNormalizingPredicate:expectedPredicate]); +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testReversesYodaExpressions { + [self assertPredicateFormat:@"3 == x" normalizesToFormat:@"x == 3"]; + [self assertPredicateFormat:@"3 != x" normalizesToFormat:@"x != 3"]; + [self assertPredicateFormat:@"3 > x" normalizesToFormat:@"x < 3"]; + [self assertPredicateFormat:@"3 < x" normalizesToFormat:@"x > 3"]; + [self assertPredicateFormat:@"3 >= x" normalizesToFormat:@"x <= 3"]; + [self assertPredicateFormat:@"3 <= x" normalizesToFormat:@"x >= 3"]; + [self assertPredicateFormat:@"3 in x" normalizesToFormat:@"x = 3"]; + + [self assertPredicateFormat:@"x contains y" normalizesToFormat:@"x contains y"]; +} + +- (void)testPushesNegatesToLeaves { + [self assertPredicateFormat:@"!(y == 4)" normalizesToFormat:@"y != 4"]; + [self assertPredicateFormat:@"!(y == 4 || !(y == 3))" normalizesToFormat:@"y != 4 && y == 3"]; + [self assertPredicateFormat:@"!(y > 3)" normalizesToFormat:@"y <= 3"]; + [self assertPredicateFormat:@"!(y < 3)" normalizesToFormat:@"y >= 3"]; + [self assertPredicateFormat:@"!(y >= 3)" normalizesToFormat:@"y < 3"]; + [self assertPredicateFormat:@"!(y <= 3)" normalizesToFormat:@"y > 3"]; + [self assertPredicateFormat:@"!(y IN {x, y, z})" normalizesToFormat:@"y notContainedIn: {x, y, z}"]; + + [self assertPredicateThrows:@"!(y MATCHES x)"]; +} + +- (void)testRemovesBeweens { + [self assertPredicateFormat:@"x BETWEEN {1, 10}" normalizesToFormat:@"x >= 1 && x <= 10"]; + + [self assertPredicateThrows:@"x BETWEEN y"]; + [self assertPredicateThrows:@"x BETWEEN {x, y, z}"]; +} + +- (void)testConvertToDNF { + [self assertPredicateFormat:@"(x == 0) || ((x == 2) && (y != 3 || y == 2))" + normalizesToFormat:@"(x == 0) || (x == 2 && y != 3) || (x == 2 && y == 2)"]; +} + +- (void)testMergeCommonPredicate { + [self assertPredicateFormat:@"((x == 0) && (y == 1)) || ((x == 0) && (z == 1))" + normalizesToFormat:@"(y == 1 || z == 1) && (x == 0)"]; +} + +- (void)testBadQueries { + [self assertPredicateThrows:@"ANY x == y"]; + + NSExpression *expression = [NSExpression expressionWithFormat:@"12345"]; + XCTAssertThrows([PFQueryUtilities predicateByNormalizingPredicate:(NSPredicate *)expression]); +} + +- (void)testRemovesDoubleNegatives { + NSPredicate *negatedPredicate = [NSPredicate predicateWithFormat:@"!(y != 5)"]; + NSPredicate *normalizedPredicate = [NSPredicate predicateWithFormat:@"y == 5"]; + + XCTAssertNotEqualObjects(negatedPredicate, normalizedPredicate); + + XCTAssertEqualObjects(normalizedPredicate, [PFQueryUtilities predicateByNormalizingPredicate:negatedPredicate]); + XCTAssertEqualObjects(normalizedPredicate, [PFQueryUtilities predicateByNormalizingPredicate:normalizedPredicate]); +} + +- (void)testRegexString { + NSString *inputString = @"Hello!"; + XCTAssertEqualObjects([PFQueryUtilities regexStringForString:inputString], @"\\QHello!\\E"); + + inputString = @"Hello\\E"; + XCTAssertEqualObjects([PFQueryUtilities regexStringForString:inputString], @"\\QHello\\E\\\\E\\Q\\E"); + + inputString = @"\\QHello"; + XCTAssertEqualObjects([PFQueryUtilities regexStringForString:inputString], @"\\Q\\QHello\\E"); +} + +- (void)testErrors { + NSError *error = [PFQueryUtilities objectNotFoundError]; + + XCTAssertNotNil(error); + XCTAssertEqual(error.code, kPFErrorObjectNotFound); + XCTAssertEqual(error.domain, PFParseErrorDomain); +} + +@end diff --git a/Tests/Unit/RelationStateTests.m b/Tests/Unit/RelationStateTests.m new file mode 100644 index 000000000..95e95efa0 --- /dev/null +++ b/Tests/Unit/RelationStateTests.m @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFMutableRelationState.h" +#import "PFRelationState.h" +#import "PFTestCase.h" + +@interface RelationStateTests : PFTestCase + +@end + +@implementation RelationStateTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFRelationState *)sampleRelationStateWithParent:(PFObject *)parent { + PFMutableRelationState *state = [[PFMutableRelationState alloc] init]; + + state.parent = nil; + state.key = @"Treasure"; + state.targetClass = @"Ship"; + + return [state copy]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testInit { + PFRelationState *state = [[PFRelationState alloc] init]; + XCTAssertNotNil(state); + + XCTAssertNil(state.parent); + XCTAssertNil(state.parentObjectId); + XCTAssertNil(state.parentClassName); + XCTAssertNil(state.key); + XCTAssertNil(state.targetClass); + XCTAssertNotNil(state.knownObjects); + + state = [[PFMutableRelationState alloc] init]; + XCTAssertNotNil(state); + + XCTAssertNil(state.parent); + XCTAssertNil(state.parentObjectId); + XCTAssertNil(state.parentClassName); + XCTAssertNil(state.targetClass); + XCTAssertNil(state.key); + XCTAssertNotNil(state.knownObjects); +} + +- (void)testInitWithState { + PFRelationState *sampleState = [self sampleRelationStateWithParent:nil]; + + PFRelationState *state = [[PFRelationState alloc] initWithState:sampleState]; + XCTAssertEqualObjects(state, sampleState); + + state = [PFRelationState stateWithState:sampleState]; + XCTAssertEqualObjects(state, sampleState); + + state = [[PFMutableRelationState alloc] initWithState:sampleState]; + XCTAssertEqualObjects(state, sampleState); + + state = [PFMutableRelationState stateWithState:sampleState]; + XCTAssertEqualObjects(state, sampleState); +} + +- (void)testCopying { + PFRelationState *sampleState = [self sampleRelationStateWithParent:nil]; + XCTAssertEqualObjects([sampleState copy], sampleState); + + sampleState = [PFMutableRelationState stateWithState:sampleState]; + XCTAssertEqualObjects([sampleState copy], sampleState); +} + +- (void)testMutableCopy { + PFMutableRelationState *sampleState = [[self sampleRelationStateWithParent:nil] mutableCopy]; + sampleState.knownObjects = [NSMutableSet setWithObjects:@1, nil]; + + XCTAssertEqualObjects([sampleState mutableCopy], sampleState); +} + +@end diff --git a/Tests/Unit/RelationUnitTests.m b/Tests/Unit/RelationUnitTests.m new file mode 100644 index 000000000..09633332b --- /dev/null +++ b/Tests/Unit/RelationUnitTests.m @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "PFDecoder.h" +#import "PFQueryPrivate.h" +#import "PFRelation.h" +#import "PFRelationPrivate.h" +#import "PFUnitTestCase.h" + +@interface RelationUnitTests : PFUnitTestCase + +@end + +@implementation RelationUnitTests + +- (void)testConstructors { + PFRelation *relation = [[PFRelation alloc] init]; + XCTAssertNotNil(relation); + XCTAssertNil(relation.targetClass); + + id mockedObject = PFStrictClassMock([PFObject class]); + OCMStub([mockedObject parseClassName]).andReturn(@"SomeClass"); + OCMStub([mockedObject objectId]).andReturn(@"objectId"); + + relation = [PFRelation relationForObject:mockedObject forKey:@"key"]; + + XCTAssertNotNil(relation); + XCTAssertNil(relation.targetClass); + + XCTAssertNoThrow([relation ensureParentIs:mockedObject andKeyIs:@"key"]); + + relation = [PFRelation relationWithTargetClass:@"targetClass"]; + XCTAssertNotNil(relation); + XCTAssertEqualObjects(relation.targetClass, @"targetClass"); +} + +- (void)testQuery { + PFRelation *testRelation = nil; + + @autoreleasepool { + PFObject *parentObject = [[PFObject alloc] initWithClassName:@"SomeClass"]; + parentObject.objectId = @"objectId"; + + testRelation = [PFRelation relationForObject:parentObject forKey:@"aKey"]; + testRelation.targetClass = @"TargetClass"; + + PFQuery *query = testRelation.query; + PFQuery *expectedQuery = [PFQuery queryWithClassName:@"TargetClass"]; + [expectedQuery whereRelatedToObject:parentObject fromKey:@"aKey"]; + + XCTAssertEqualObjects(expectedQuery.state, query.state); + + query = nil; + expectedQuery = nil; + parentObject = nil; + } + + OSMemoryBarrier(); + + @autoreleasepool { + PFQuery *query = testRelation.query; + + XCTAssertEqualObjects(query.state.conditions[@"$relatedTo"][@"key"], @"aKey"); + XCTAssertEqualObjects([query.state.conditions[@"$relatedTo"][@"object"] parseClassName], @"SomeClass"); + XCTAssertEqualObjects([query.state.conditions[@"$relatedTo"][@"object"] objectId], @"objectId"); + + testRelation.targetClass = nil; + query = testRelation.query; + + XCTAssertEqualObjects(query.state.conditions[@"$relatedTo"][@"key"], @"aKey"); + XCTAssertEqualObjects([query.state.conditions[@"$relatedTo"][@"object"] parseClassName], @"SomeClass"); + XCTAssertEqualObjects([query.state.conditions[@"$relatedTo"][@"object"] objectId], @"objectId");; + } +} + +- (void)testAddObject { + PFRelation *relation = [PFRelation relationWithTargetClass:@"TargetClass"]; + + id mockedObject = PFClassMock([PFObject class]); + OCMStub([mockedObject parseClassName]).andReturn(@"TargetClass"); + + [relation addObject:mockedObject]; + + XCTAssertTrue([relation _hasKnownObject:mockedObject]); +} + +- (void)testRemoveObject { + PFRelation *relation = [PFRelation relationWithTargetClass:@"TargetClass"]; + + id mockedObject = PFClassMock([PFObject class]); + OCMStub([mockedObject parseClassName]).andReturn(@"TargetClass"); + + [relation addObject:mockedObject]; + [relation removeObject:mockedObject]; + + XCTAssertFalse([relation _hasKnownObject:mockedObject]); +} + +- (void)testKnownObjects { + PFRelation *relation = [[PFRelation alloc] init]; + + id mockedObject1 = PFStrictClassMock([PFObject class]); + id mockedObject2 = PFStrictClassMock([PFObject class]); + + OCMStub([mockedObject1 parseClassName]).andReturn(@"TargetClass1"); + OCMStub([mockedObject2 parseClassName]).andReturn(@"TargetClass2"); + + [relation addObject:mockedObject1]; + + XCTAssertTrue([relation _hasKnownObject:mockedObject1]); + XCTAssertFalse([relation _hasKnownObject:mockedObject2]); + + XCTAssertEqualObjects(relation.targetClass, @"TargetClass1"); + + [relation _addKnownObject:mockedObject2]; + [relation _removeKnownObject:mockedObject1]; + + XCTAssertFalse([relation _hasKnownObject:mockedObject1]); + XCTAssertTrue([relation _hasKnownObject:mockedObject2]); + + XCTAssertEqualObjects(relation.targetClass, @"TargetClass1"); +} + +- (void)testEncode { + id mockedObject = PFStrictClassMock([PFObject class]); + OCMStub([mockedObject parseClassName]).andReturn(@"SomeClass"); + OCMStub([mockedObject objectId]).andReturn(@"objectId"); + + PFRelation *relation = [[PFRelation alloc] init]; + relation.targetClass = @"TargetClass"; + + [relation _addKnownObject:mockedObject]; + + NSDictionary *encoded = [relation encodeIntoDictionary]; + XCTAssertEqual(1, [encoded[@"objects"] count]); + XCTAssertNotNil(encoded); +} + +- (void)testDecode { + NSDictionary *toDecode = @{ + @"__type": @"Relation", + @"className": @"TargetClass", + @"objects": + @[ + @{ + @"__type": @"Pointer", + @"className": @"SomeClass", + @"objectId": @"objectId", + } + ] + }; + + PFRelation *decoded = [PFRelation relationFromDictionary:toDecode withDecoder:[PFDecoder objectDecoder]]; + XCTAssertEqualObjects(decoded.targetClass, @"TargetClass"); +} + +- (void)testDescription { + PFRelation *relation = [[PFRelation alloc] init]; + relation.targetClass = @"SomeClass"; + + XCTAssertTrue([relation.description rangeOfString:@"SomeClass"].location != NSNotFound); +} + +@end diff --git a/Tests/Unit/RoleUnitTests.m b/Tests/Unit/RoleUnitTests.m new file mode 100644 index 000000000..b16ca5b15 --- /dev/null +++ b/Tests/Unit/RoleUnitTests.m @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFMockURLProtocol.h" +#import "PFRelation.h" +#import "PFRole.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface RoleUnitTests : PFUnitTestCase + +@end + +@implementation RoleUnitTests + +- (void)testConstructors { + PFRole *role = [PFRole roleWithName:@"someName"]; + XCTAssertEqual(role.name, @"someName"); + XCTAssertNil(role.ACL); + + PFACL *acl = [PFACL ACL]; + role = [PFRole roleWithName:@"someName" acl:acl]; + XCTAssertEqual(role.name, @"someName"); + XCTAssertEqual(role.ACL, acl); +} + +- (void)testInvalidConstructors { + PFAssertThrowsInvalidArgumentException([[PFRole alloc] initWithClassName:@"YoloSwag"]); +} + +- (void)testRelations { + PFRole *parentRole = [PFRole roleWithName:@"parent" acl:nil]; + + PFAssertIsKindOfClass(parentRole.roles, [PFRelation class]); + PFAssertIsKindOfClass(parentRole.users, [PFRelation class]); +} + +- (void)testInvalidName { + PFRole *sampleRole = [PFRole objectWithoutDataWithObjectId:@"leroy"]; + PFAssertThrowsInconsistencyException(sampleRole.name = @"jenkins"); + + PFAssertThrowsInvalidArgumentException([PFRole roleWithName:(NSString *)@100]); + PFAssertThrowsInvalidArgumentException([PFRole roleWithName:@"??!!"]); +} + +- (void)testCannotSave { + [NSURLProtocol registerClass:[PFMockURLProtocol class]]; + + XCTestExpectation *failedSaveExpectation = [self currentSelectorTestExpectation]; + [PFMockURLProtocol mockRequestsWithResponse:^PFMockURLResponse *(NSURLRequest *request) { + return [PFMockURLResponse responseWithString:@"{ \"error\": \"some error\", \"code\": 101 }" statusCode:400 delay:0]; + }]; + + PFRole *theRole = [[PFRole alloc] init]; + PFAssertThrowsInconsistencyException([theRole saveInBackground]); + + theRole.name = @"SomeName"; + [[theRole saveInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.faulted); + [failedSaveExpectation fulfill]; + return nil; + }]; + + [self waitForTestExpectations]; + + [PFMockURLProtocol removeAllMocking]; + [NSURLProtocol unregisterClass:[PFMockURLProtocol class]]; +} + +@end diff --git a/Tests/Unit/SQLiteDatabaseTest.m b/Tests/Unit/SQLiteDatabaseTest.m new file mode 100644 index 000000000..f88889c05 --- /dev/null +++ b/Tests/Unit/SQLiteDatabaseTest.m @@ -0,0 +1,600 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "BFTask+Private.h" +#import "PFFileManager.h" +#import "PFSQLiteDatabase.h" +#import "PFSQLiteDatabaseResult.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface SQLiteDatabaseTest : PFUnitTestCase { + PFSQLiteDatabase *database; +} +@end + +@implementation SQLiteDatabaseTest + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (NSString *)databasePath { + return [NSTemporaryDirectory() stringByAppendingPathComponent:@"test.db"]; +} + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + database = [PFSQLiteDatabase databaseWithPath:[self databasePath]]; +} + +- (void)tearDown { + if (database != NULL) { + [[[database isOpenAsync] continueWithBlock:^id(BFTask *task) { + BOOL isOpen = [task.result boolValue]; + + if (isOpen) { + return [database closeAsync]; + } + return task; + }] waitUntilFinished]; + } + // delete DB file; + [[NSFileManager defaultManager] removeItemAtPath:[self databasePath] error:NULL]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +// Should return BFTask to not waste `waitUntilFinished` +- (BFTask *)createDatabaseAsync { + // Drop existing database first if any. + return [[[[database openAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"DROP TABLE test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + return [database openAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"CREATE TABLE test (a text, b text, c integer, d double)" + withArgumentsInArray:nil]; + }]; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testOpen { + [[[[[[[[[database openAsync] continueWithBlock:^id(BFTask *task) { + return [database isOpenAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue([task.result boolValue]); + return [database openAsync]; + }] continueWithBlock:^id(BFTask *task) { + // Should error because DB is opened + XCTAssertNotNil(task.error); + return [database closeAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database isOpenAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse([task.result boolValue]); + return [database closeAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + return [database openAsync]; + }] continueWithBlock:^id(BFTask *task) { + // Should fail because database was closed already, and reopened. + XCTAssertNotNil(task.error); + return task; + }] waitUntilFinished]; +} + +- (void)testCRUD { + [[[[[[[[[[[[database openAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"CREATE TABLE test (a text, b text, c integer, d double)" + withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Make sure it success + XCTAssertNil(task.error); + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"one", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(3, [result intForColumnIndex:2]); + // Make sure there's nothing more + XCTAssertFalse([result next]); + + // Test the cached statement + // TODO (hallucinogen): how can we be sure we're getting this from cached statement? + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"one", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(3, [result intForColumnIndex:2]); + + // Make sure there's nothing more + XCTAssertFalse([result next]); + + return [database executeSQLAsync:@"UPDATE test SET a = ?, c = ? WHERE c = ?" + withArgumentsInArray:[NSArray arrayWithObjects:@"onenew", @5, @3, nil]]; + }] continueWithBlock:^id(BFTask *task) { + // Make sure there's nothing wrong + XCTAssertNil(task.error); + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"onenew", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(5, [result intForColumnIndex:2]); + + // Make sure there's nothing more + XCTAssertFalse([result next]); + + return [database executeSQLAsync:@"DELETE FROM test WHERE c = ?" + withArgumentsInArray:[NSArray arrayWithObjects:@5, nil]]; + }] continueWithBlock:^id(BFTask *task) { + // Make sure there's nothing wrong + XCTAssertNil(task.error); + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Make sure there's nothing wrong + XCTAssertNil(task.error); + PFSQLiteDatabaseResult *result = task.result; + // Make sure there's nothing more + XCTAssertFalse(result.next); + + // Clean up + return [database executeSQLAsync:@"DROP TABLE test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Make sure there's nothing wrong + XCTAssertNil(task.error); + return task; + }] waitUntilFinished]; +} + +// TODO (hallucinogen): this test consists of three units which can be separated. +- (void)testTransaction { + [[[[[[[[[[[[[[[[[self createDatabaseAsync] continueWithBlock:^id(BFTask *task) { + return [database beginTransactionAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"one", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(3, [result intForColumnIndex:2]); + + // Make sure there's nothing more + XCTAssertFalse(result.next); + + // Commit + return [database commitAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"one", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(3, [result intForColumnIndex:2]); + + // Make sure there's nothing more + XCTAssertFalse(result.next); + + return [database beginTransactionAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"oneone", @"twotwo", @33, @44.44, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // should have two results + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + BOOL nextResult = [result next]; + // There's second element + XCTAssertTrue(nextResult); + nextResult = [result next]; + // There's nothing more + XCTAssertFalse(nextResult); + + // Rollback + return [database rollbackAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Should have one result + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + BOOL nextResult = [result next]; + // There's nothing more + XCTAssertFalse(nextResult); + + // Now let's try making transaction, then close the database wbile it's in transaction + return [database beginTransactionAsync]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"oneone", @"twotwo", @33, @44.44, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Should have two results + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + BOOL nextResult = [result next]; + XCTAssertTrue(nextResult); + nextResult = [result next]; + XCTAssertFalse(nextResult); + + // Let's close the database while in transaction + // The expected result: close successfully and the transaction would be rolled back + return [database closeAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + return [BFTask taskWithResult:nil]; + }] waitUntilFinished]; + + database = [PFSQLiteDatabase databaseWithPath:[self databasePath]]; + [[[[[[[database openAsync] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"oneone", @"twotwo", @33, @44.44, nil]]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Should have two results because the last one is rolled back + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + BOOL nextResult = [result next]; + XCTAssertTrue(nextResult); + nextResult = [result next]; + XCTAssertFalse(nextResult); + + // Try rolling back previous transaction (which should fail because the database has been + // closed and currently there's no transaction) + return [database rollbackAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Should still have two results + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + BOOL nextResult = [result next]; + XCTAssertTrue(nextResult); + nextResult = [result next]; + XCTAssertFalse(nextResult); + + return [database closeAsync]; + }] waitUntilFinished]; +} + +- (void)testOperationOnNonExistentTable { + [[[[[[[self createDatabaseAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO testFake (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + return [database executeCachedQueryAsync:@"SELECT * FROM testFake" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + // Should have one result + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + BOOL nextResult = [result next]; + XCTAssertFalse(nextResult); + + // Clean up + return [database closeAsync]; + }] waitUntilFinished]; +} + +- (void)testQuery { + [[[[[[[[[self createDatabaseAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"oneone", @"twotwo", @33, @44.44, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + // Should have two results + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + BOOL nextResult = [result next]; + XCTAssertTrue(nextResult); + nextResult = [result next]; + XCTAssertFalse(nextResult); + + return [database executeCachedQueryAsync:@"SELECT * FROM test WHERE c = ?" + withArgumentsInArray:[NSArray arrayWithObjects:@3, nil]]; + }] continueWithBlock:^id(BFTask *task) { + // Check result + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"one", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(3, [result intForColumnIndex:2]); + + // Should have one result + BOOL nextResult = [result next]; + XCTAssertFalse(nextResult); + + return [database executeSQLAsync:@"UPDATE test SET a = ?, c = ? WHERE c = ?" + withArgumentsInArray:[NSArray arrayWithObjects:@"onenew", @5, @3, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test WHERE c = ?" + withArgumentsInArray:[NSArray arrayWithObjects:@5, nil]]; + }] continueWithBlock:^id(BFTask *task) { + // Check result + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"onenew", [result stringForColumn:@"a"]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(5, [result intForColumnIndex:2]); + + // Should have one result + BOOL nextResult = [result next]; + XCTAssertFalse(nextResult); + + // Clean up + return [database closeAsync]; + }] waitUntilFinished]; +} + +- (void)testCursorAndOperationOnDifferentThread { + BFTask *taskWithCursor = [[[[[self createDatabaseAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"oneone", @"twotwo", @33, @44.44, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithExecutor:[BFExecutor defaultPriorityBackgroundExecutor] withBlock:^id(BFTask *task) { + // Execute this in background + PFSQLiteDatabaseResult *result = task.result; + // Make sure first element exists + XCTAssertTrue([result next]); + + // Check values + XCTAssertEqualObjects(@"one", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(3, [result intForColumnIndex:2]); + + return result; + }]; + + // Make sure we can read result from main thread + [taskWithCursor waitUntilFinished]; + PFSQLiteDatabaseResult *result = taskWithCursor.result; + + // Try to access result in main thread + XCTAssertEqualObjects(@"one", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"two", [result stringForColumnIndex:1]); + XCTAssertEqual(3, [result intForColumnIndex:2]); + XCTAssertTrue([result next]); + XCTAssertEqualObjects(@"oneone", [result stringForColumnIndex:0]); + XCTAssertEqualObjects(@"twotwo", [result stringForColumnIndex:1]); + XCTAssertEqual(33, [result intForColumnIndex:2]); + + // Test clean up fail + [[[[[[database executeSQLAsync:@"DROP TABLE test" withArgumentsInArray:nil] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + return task; + }] continueWithExecutor:[BFExecutor defaultExecutor] withBlock:^id(BFTask *task) { + // `result` should not increase + XCTAssertFalse([result next]); + + return [database executeSQLAsync:@"DROP TABLE test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + return task; + //return [database2 closeAsync]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + return [database closeAsync]; + }] waitUntilFinished]; +} + +- (void)testInvalidArgumentCount { + [[[[self createDatabaseAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + XCTAssertEqual(PFSQLiteDatabaseInvalidArgumenCountErrorCode, [task.error.userInfo[@"code"] integerValue]); + return [database closeAsync]; + }] waitUntilFinished]; +} + +- (void)testInvalidSQL { + [[[[[self createDatabaseAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:[NSArray arrayWithObjects:@"one", @"two", @3, @4.4, nil]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + XCTAssertEqual(PFSQLiteDatabaseInvalidSQL, [task.error.userInfo[@"code"] integerValue]); + return [database closeAsync]; + }] waitUntilFinished]; +} + +- (void)testColumnTypes { + [[[[[self createDatabaseAsync] continueWithBlock:^id(BFTask *task) { + return [database executeSQLAsync:@"INSERT INTO test (a, b, c, d) VALUES (?, ?, ?, ?)" + withArgumentsInArray:@[ @1, [NSNull null], @"string", @13.37 ]]; + }] continueWithBlock:^id(BFTask *task) { + return [database executeCachedQueryAsync:@"SELECT * FROM test" withArgumentsInArray:nil]; + }] continueWithBlock:^id(BFTask *task) { + XCTAssertNil(task.error); + PFSQLiteDatabaseResult *result = task.result; + XCTAssertTrue([result next]); + + XCTAssertEqual([result intForColumn:@"a"], 1); + XCTAssertEqual([result intForColumn:@"b"], 0); + XCTAssertEqual([result intForColumn:@"c"], 0); + XCTAssertEqual([result intForColumn:@"d"], 13); + + XCTAssertEqual([result intForColumnIndex:0], 1); + XCTAssertEqual([result intForColumnIndex:1], 0); + XCTAssertEqual([result intForColumnIndex:2], 0); + XCTAssertEqual([result intForColumnIndex:3], 13); + + XCTAssertEqual([result longForColumn:@"a"], 1); + XCTAssertEqual([result longForColumn:@"b"], 0); + XCTAssertEqual([result longForColumn:@"c"], 0); + XCTAssertEqual([result longForColumn:@"d"], 13); + + XCTAssertEqual([result longForColumnIndex:0], 1); + XCTAssertEqual([result longForColumnIndex:1], 0); + XCTAssertEqual([result longForColumnIndex:2], 0); + XCTAssertEqual([result longForColumnIndex:3], 13); + + XCTAssertEqual([result boolForColumn:@"a"], YES); + XCTAssertEqual([result boolForColumn:@"b"], NO); + XCTAssertEqual([result boolForColumn:@"c"], NO); + XCTAssertEqual([result boolForColumn:@"d"], YES); + + XCTAssertEqual([result boolForColumnIndex:0], YES); + XCTAssertEqual([result boolForColumnIndex:1], NO); + XCTAssertEqual([result boolForColumnIndex:2], NO); + XCTAssertEqual([result boolForColumnIndex:3], YES); + + XCTAssertEqual([result doubleForColumn:@"a"], 1); + XCTAssertEqual([result doubleForColumn:@"b"], 0); + XCTAssertEqual([result doubleForColumn:@"c"], 0); + XCTAssertEqual([result doubleForColumn:@"d"], 13.37); + + XCTAssertEqual([result doubleForColumnIndex:0], 1); + XCTAssertEqual([result doubleForColumnIndex:1], 0); + XCTAssertEqual([result doubleForColumnIndex:2], 0); + XCTAssertEqual([result doubleForColumnIndex:3], 13.37); + + XCTAssertEqualObjects([result stringForColumn:@"a"], @"1"); + XCTAssertEqualObjects([result stringForColumn:@"b"], nil); + XCTAssertEqualObjects([result stringForColumn:@"c"], @"string"); + XCTAssertEqualObjects([result stringForColumn:@"d"], @"13.37"); + + XCTAssertEqualObjects([result stringForColumnIndex:0], @"1"); + XCTAssertEqualObjects([result stringForColumnIndex:1], nil); + XCTAssertEqualObjects([result stringForColumnIndex:2], @"string"); + XCTAssertEqualObjects([result stringForColumnIndex:3], @"13.37"); + + XCTAssertEqualObjects([result dateForColumn:@"a"], [NSDate dateWithTimeIntervalSince1970:1]); + XCTAssertEqualObjects([result dateForColumn:@"b"], [NSDate dateWithTimeIntervalSince1970:0]); + XCTAssertEqualObjects([result dateForColumn:@"c"], [NSDate dateWithTimeIntervalSince1970:0]); + XCTAssertEqualObjects([result dateForColumn:@"d"], [NSDate dateWithTimeIntervalSince1970:13.37]); + + XCTAssertEqualObjects([result dateForColumnIndex:0], [NSDate dateWithTimeIntervalSince1970:1]); + XCTAssertEqualObjects([result dateForColumnIndex:1], [NSDate dateWithTimeIntervalSince1970:0]); + XCTAssertEqualObjects([result dateForColumnIndex:2], [NSDate dateWithTimeIntervalSince1970:0]); + XCTAssertEqualObjects([result dateForColumnIndex:3], [NSDate dateWithTimeIntervalSince1970:13.37]); + + XCTAssertEqualObjects([result dataForColumn:@"a"], [NSData dataWithBytes:(char[]) { '1' } length:1]); + XCTAssertEqualObjects([result dataForColumn:@"b"], nil); + XCTAssertEqualObjects([result dataForColumn:@"c"], [NSData dataWithBytes:"string"length:6]); + XCTAssertEqualObjects([result dataForColumn:@"d"], [NSData dataWithBytes:"13.37" length:5]); + + XCTAssertEqualObjects([result dataForColumnIndex:0], [NSData dataWithBytes:(char[]) { '1' } length:1]); + XCTAssertEqualObjects([result dataForColumnIndex:1], nil); + XCTAssertEqualObjects([result dataForColumnIndex:2], [NSData dataWithBytes:"string"length:6]); + XCTAssertEqualObjects([result dataForColumnIndex:3], [NSData dataWithBytes:"13.37" length:5]); + + XCTAssertEqualObjects([result objectForColumn:@"a"], @"1"); + XCTAssertEqualObjects([result objectForColumn:@"b"], nil); + XCTAssertEqualObjects([result objectForColumn:@"c"], @"string"); + XCTAssertEqualObjects([result objectForColumn:@"d"], @13.37); + + XCTAssertEqualObjects([result objectForColumnIndex:0], @"1"); + XCTAssertEqualObjects([result objectForColumnIndex:1], nil); + XCTAssertEqualObjects([result objectForColumnIndex:2], @"string"); + XCTAssertEqualObjects([result objectForColumnIndex:3], @13.37); + + XCTAssertFalse([result columnIsNull:@"a"]); + XCTAssertTrue([result columnIsNull:@"b"]); + XCTAssertFalse([result columnIsNull:@"c"]); + XCTAssertFalse([result columnIsNull:@"d"]); + + XCTAssertFalse([result columnIndexIsNull:0]); + XCTAssertTrue([result columnIndexIsNull:1]); + XCTAssertFalse([result columnIndexIsNull:2]); + XCTAssertFalse([result columnIndexIsNull:3]); + + return [database closeAsync]; + }] waitUntilFinished]; +} + +@end diff --git a/Tests/Unit/SessionControllerTests.m b/Tests/Unit/SessionControllerTests.m new file mode 100644 index 000000000..f66e4bd36 --- /dev/null +++ b/Tests/Unit/SessionControllerTests.m @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "BFTask+Private.h" +#import "OCMock+Parse.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFObjectPrivate.h" +#import "PFRESTCommand.h" +#import "PFSessionController.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface SessionControllerTests : PFUnitTestCase + +@end + +@implementation SessionControllerTests + +///-------------------------------------- +#pragma mark - XCTestCase +///-------------------------------------- + +- (void)setUp { + [super setUp]; + + [PFSession registerSubclass]; +} + +- (void)tearDown { + [PFObject unregisterSubclass:[PFSession class]]; + + [super tearDown]; +} + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)controllerDataSourceWithCommandResult:(PFCommandResult *)result error:(NSError *)error { + id providerMock = PFProtocolMock(@protocol(PFCommandRunnerProvider)); + + BFTask *task = nil; + if (error) { + task = [BFTask taskWithError:error]; + } else { + task = [BFTask taskWithResult:result]; + } + + id runnerMock = PFStrictProtocolMock(@protocol(PFCommandRunning)); + OCMStub([[runnerMock ignoringNonObjectArgs] runCommandAsync:OCMOCK_ANY + withOptions:0]).andReturn(task); + + OCMStub([providerMock commandRunner]).andReturn(runnerMock); + return providerMock; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id providerMock = [self controllerDataSourceWithCommandResult:nil error:nil]; + + PFSessionController *controller = [[PFSessionController alloc] initWithDataSource:providerMock]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, providerMock); + + controller = [PFSessionController controllerWithDataSource:providerMock]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.dataSource, providerMock); +} + +- (void)testGetSessionParameters { + id providerMock = [self controllerDataSourceWithCommandResult:nil error:nil]; + + PFSessionController *controller = [PFSessionController controllerWithDataSource:providerMock]; + [[controller getCurrentSessionAsyncWithSessionToken:@"yolo"] waitUntilFinished]; + + OCMVerify([[[providerMock ignoringNonObjectArgs] commandRunner] runCommandAsync:[OCMArg checkWithBlock:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertNotEqual([command.httpPath rangeOfString:@"sessions/me"].location, NSNotFound); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); + XCTAssertNil(command.parameters); + + return YES; + }] + withOptions:0 + cancellationToken:[OCMArg checkWithBlock:^BOOL(id obj) { + XCTAssertNil(obj); + return YES; + }]]); +} + +- (void)testGetSessionResult { + PFCommandResult *result = [PFCommandResult commandResultWithResult:@{ @"objectId" : @"something", + @"a" : @"El Capitan" } + resultString:nil + httpResponse:nil]; + id providerMock = [self controllerDataSourceWithCommandResult:result error:nil]; + + PFSessionController *controller = [PFSessionController controllerWithDataSource:providerMock]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller getCurrentSessionAsyncWithSessionToken:@"yolo"] continueWithSuccessBlock:^id(BFTask *task) { + PFSession *session = task.result; + XCTAssertNotNil(session); + PFAssertIsKindOfClass(session, [PFSession class]); + XCTAssertNotNil(session.objectId); + XCTAssertEqualObjects(session[@"a"], @"El Capitan"); + + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetSessionError { + NSError *error = [NSError errorWithDomain:@"TestErrorDomain" code:100500 userInfo:nil]; + id providerMock = [self controllerDataSourceWithCommandResult:nil error:error]; + + PFSessionController *controller = [PFSessionController controllerWithDataSource:providerMock]; + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller getCurrentSessionAsyncWithSessionToken:@"yolo"] continueWithBlock:^id(BFTask *task) { + XCTAssertNotNil(task.error); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/SessionUnitTests.m b/Tests/Unit/SessionUnitTests.m new file mode 100644 index 000000000..4ecae4e57 --- /dev/null +++ b/Tests/Unit/SessionUnitTests.m @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "PFCoreManager.h" +#import "PFObjectPrivate.h" +#import "PFSessionController.h" +#import "PFSession_Private.h" +#import "PFUnitTestCase.h" +#import "Parse_Private.h" + +@interface SessionUnitTests : PFUnitTestCase + +@end + +@implementation SessionUnitTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (PFSessionController *)sessionControllerMockWithSessionResult:(PFSession *)session error:(NSError *)error { + BFTask *task = nil; + if (error) { + task = [BFTask taskWithError:error]; + } else { + task = [BFTask taskWithResult:session]; + } + + id controllerMock = PFClassMock([PFSessionController class]); + OCMStub([controllerMock getCurrentSessionAsyncWithSessionToken:OCMOCK_ANY]).andReturn(task); + return controllerMock; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testSessionClassIsRegistered { + [PFObject unregisterSubclass:[PFSession class]]; + [Parse setApplicationId:@"a" clientKey:@"b"]; + XCTAssertNotNil([PFSession query]); + + [[Parse _currentManager] clearEventuallyQueue]; + [Parse _clearCurrentManager]; +} + +- (void)testConstructorsClassNameValidation { + PFAssertThrowsInvalidArgumentException([[PFSession alloc] initWithClassName:@"yarrclass"], + @"Should throw an exception for invalid classname"); +} + +- (void)testSessionImmutableFieldsCannotBeChanged { + [PFSession registerSubclass]; + + PFSession *session = [PFSession object]; + session[@"yolo"] = @"El Capitan!"; // Test for regular mutability + PFAssertThrowsInvalidArgumentException(session[@"sessionToken"] = @"a"); + PFAssertThrowsInvalidArgumentException(session[@"restricted"] = @"a"); + PFAssertThrowsInvalidArgumentException(session[@"createdWith"] = @"a"); + PFAssertThrowsInvalidArgumentException(session[@"installationId"] = @"a"); + PFAssertThrowsInvalidArgumentException(session[@"user"] = @"a"); + PFAssertThrowsInvalidArgumentException(session[@"expiresAt"] = @"a"); +} + +- (void)testSessionImmutableFieldsCannotBeDeleted { + [PFSession registerSubclass]; + + PFSession *session = [PFSession object]; + + [session removeObjectForKey:@"yolo"];// Test for regular mutability + + PFAssertThrowsInvalidArgumentException([session removeObjectForKey:@"sessionToken"]); + PFAssertThrowsInvalidArgumentException([session removeObjectForKey:@"restricted"]); + PFAssertThrowsInvalidArgumentException([session removeObjectForKey:@"createdWith"]); + PFAssertThrowsInvalidArgumentException([session removeObjectForKey:@"installationId"]); + PFAssertThrowsInvalidArgumentException([session removeObjectForKey:@"user"]); + PFAssertThrowsInvalidArgumentException([session removeObjectForKey:@"expiresAt"]); + + [session removeObjectsInArray:@[ @"El Capitan" ] forKey:@"yolo"]; // Test for regular mutability + + PFAssertThrowsInvalidArgumentException([session removeObjectsInArray:@[@"1"] forKey:@"sessionToken"]); + PFAssertThrowsInvalidArgumentException([session removeObjectsInArray:@[@"1"] forKey:@"restricted"]); + PFAssertThrowsInvalidArgumentException([session removeObjectsInArray:@[@"1"] forKey:@"createdWith"]); + PFAssertThrowsInvalidArgumentException([session removeObjectsInArray:@[@"1"] forKey:@"installationId"]); + PFAssertThrowsInvalidArgumentException([session removeObjectsInArray:@[@"1"] forKey:@"user"]); + PFAssertThrowsInvalidArgumentException([session removeObjectsInArray:@[@"1"] forKey:@"expiresAt"]); +} + +- (void)testDefaultACLNotSetOnSession { + [PFACL setDefaultACL:[PFACL ACL] withAccessForCurrentUser:NO]; + XCTAssertNil([PFSession object].ACL); +} + +- (void)testSessionControllerFromSession { + [Parse _currentManager].coreManager.sessionController = [self sessionControllerMockWithSessionResult:nil error:nil]; + XCTAssertEqual([Parse _currentManager].coreManager.sessionController, [PFSession sessionController]); +} + +- (void)testGetCurrentSessionViaTask { + PFSession *session = [PFSession object]; + [Parse _currentManager].coreManager.sessionController = [self sessionControllerMockWithSessionResult:session + error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFSession getCurrentSessionInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, session); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetCurrentSessionViaBlock { + PFSession *session = [PFSession object]; + [Parse _currentManager].coreManager.sessionController = [self sessionControllerMockWithSessionResult:session + error:nil]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFSession getCurrentSessionInBackgroundWithBlock:^(PFSession *object, NSError *error) { + XCTAssertEqual(object, session); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetCurrentSessionErrorViaTask { + NSError *error = [NSError errorWithDomain:@"Test" code:100500 userInfo:nil]; + [Parse _currentManager].coreManager.sessionController = [self sessionControllerMockWithSessionResult:nil + error:error]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[PFSession getCurrentSessionInBackground] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.error, error); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testGetCurrentSessionErrorViaBlock { + NSError *error = [NSError errorWithDomain:@"Test" code:100500 userInfo:nil]; + [Parse _currentManager].coreManager.sessionController = [self sessionControllerMockWithSessionResult:nil + error:error]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [PFSession getCurrentSessionInBackgroundWithBlock:^(PFSession *session, NSError *blockError) { + XCTAssertNil(session); + XCTAssertEqual(error, blockError); + [expectation fulfill]; + }]; + [self waitForTestExpectations]; +} + +@end diff --git a/Tests/Unit/SessionUtilitiesTests.m b/Tests/Unit/SessionUtilitiesTests.m new file mode 100644 index 000000000..e63d3bede --- /dev/null +++ b/Tests/Unit/SessionUtilitiesTests.m @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFSessionUtilities.h" +#import "PFTestCase.h" + +@interface SessionUtilitiesTests : PFTestCase + +@end + +@implementation SessionUtilitiesTests + +- (void)testRevocableSessionToken { + XCTAssertTrue([PFSessionUtilities isSessionTokenRevocable:@"r:blahblahblah"]); +} + +- (void)testRevocableSesionTokenWithMiddleToken { + XCTAssertTrue([PFSessionUtilities isSessionTokenRevocable:@"blahr:blah"]); +} + +- (void)testRevocableSessionTokenFromNil { + XCTAssertFalse([PFSessionUtilities isSessionTokenRevocable:nil]); +} + +- (void)testRevocableSessionTokenFromBadToken { + XCTAssertFalse([PFSessionUtilities isSessionTokenRevocable:@"blahblah"]); +} + +@end diff --git a/Tests/Unit/URLSessionCommandRunnerTests.m b/Tests/Unit/URLSessionCommandRunnerTests.m new file mode 100644 index 000000000..81349d2bd --- /dev/null +++ b/Tests/Unit/URLSessionCommandRunnerTests.m @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import "PFCommandResult.h" +#import "PFCommandRunningConstants.h" +#import "PFCommandURLRequestConstructor.h" +#import "PFRESTCommand.h" +#import "PFTestCase.h" +#import "PFURLSession.h" +#import "PFURLSessionCommandRunner_Private.h" + +@interface URLSessionCommandRunnerTests : PFTestCase + +@end + +@implementation URLSessionCommandRunnerTests + +- (void)testConstructors { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFInstallationIdentifierStoreProvider)); + + PFURLSessionCommandRunner *commandRunner = [[PFURLSessionCommandRunner alloc] initWithDataSource:mockedDataSource + applicationId:@"appId" + clientKey:@"clientKey"]; + XCTAssertNotNil(commandRunner); + XCTAssertEqual(mockedDataSource, (id)commandRunner.dataSource); + XCTAssertEqualObjects(@"appId", commandRunner.applicationId); + XCTAssertEqualObjects(@"clientKey", commandRunner.clientKey); + XCTAssertEqual(commandRunner.initialRetryDelay, PFCommandRunningDefaultRetryDelay); + + commandRunner = [PFURLSessionCommandRunner commandRunnerWithDataSource:mockedDataSource + applicationId:@"appId" + clientKey:@"clientKey"]; + XCTAssertNotNil(commandRunner); + XCTAssertEqual(mockedDataSource, (id)commandRunner.dataSource); + XCTAssertEqualObjects(@"appId", commandRunner.applicationId); + XCTAssertEqualObjects(@"clientKey", commandRunner.clientKey); + XCTAssertEqual(commandRunner.initialRetryDelay, PFCommandRunningDefaultRetryDelay); + + PFAssertThrowsInconsistencyException([PFURLSessionCommandRunner new]); +} + +- (void)testRunCommand { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFInstallationIdentifierStoreProvider)); + id mockedSession = PFStrictClassMock([PFURLSession class]); + id mockedRequestConstructor = PFStrictClassMock([PFCommandURLRequestConstructor class]); + + id mockedCommand = PFStrictClassMock([PFRESTCommand class]); + id mockedCommandResult = PFStrictClassMock([PFCommandResult class]); + + NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://foo.bar"]]; + + OCMStub([mockedCommand resolveLocalIds]); + + OCMStub([mockedRequestConstructor dataURLRequestForCommand:mockedCommand]).andReturn(urlRequest); + [OCMExpect([mockedSession performDataURLRequestAsync:urlRequest + forCommand:mockedCommand + cancellationToken:nil]) andReturn:[BFTask taskWithResult:mockedCommandResult]]; + + OCMStub([mockedSession invalidateAndCancel]); + + PFURLSessionCommandRunner *commandRunner = [[PFURLSessionCommandRunner alloc] initWithDataSource:mockedDataSource + session:mockedSession + requestConstructor:mockedRequestConstructor]; + + XCTestExpectation *expecatation = [self currentSelectorTestExpectation]; + [[commandRunner runCommandAsync:mockedCommand withOptions:0] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedCommandResult); + + [expecatation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(mockedSession); +} + +- (void)testRunCommandCancel { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFInstallationIdentifierStoreProvider)); + id mockedSession = PFStrictClassMock([PFURLSession class]); + id mockedRequestConstructor = PFStrictClassMock([PFCommandURLRequestConstructor class]); + + id mockedCommand = PFStrictClassMock([PFRESTCommand class]); + + OCMStub([mockedSession invalidateAndCancel]); + + PFURLSessionCommandRunner *commandRunner = [[PFURLSessionCommandRunner alloc] initWithDataSource:mockedDataSource + session:mockedSession + requestConstructor:mockedRequestConstructor]; + + BFCancellationTokenSource *cancellationToken = [BFCancellationTokenSource cancellationTokenSource]; + [cancellationToken cancel]; + + XCTestExpectation *expecatation = [self currentSelectorTestExpectation]; + [[commandRunner runCommandAsync:mockedCommand + withOptions:0 + cancellationToken:cancellationToken.token] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + + [expecatation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testRunCommandRetry { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFInstallationIdentifierStoreProvider)); + id mockedSession = PFStrictClassMock([PFURLSession class]); + id mockedRequestConstructor = PFStrictClassMock([PFCommandURLRequestConstructor class]); + + id mockedCommand = PFStrictClassMock([PFRESTCommand class]); + + NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://foo.bar"]]; + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain + code:1337 + userInfo:@{ @"temporary" : @YES }]; + + __block int performDataURLRequestCount = 0; + + OCMStub([mockedCommand resolveLocalIds]); + OCMStub([mockedRequestConstructor dataURLRequestForCommand:mockedCommand]).andReturn(urlRequest); + + [OCMStub([mockedSession performDataURLRequestAsync:urlRequest + forCommand:mockedCommand + cancellationToken:nil]).andDo(^(NSInvocation *_) { + performDataURLRequestCount++; + }) andReturn:[BFTask taskWithError:expectedError]]; + + OCMStub([mockedSession invalidateAndCancel]); + + PFURLSessionCommandRunner *commandRunner = [[PFURLSessionCommandRunner alloc] initWithDataSource:mockedDataSource + session:mockedSession + requestConstructor:mockedRequestConstructor]; + commandRunner.initialRetryDelay = DBL_MIN; // Lets not needlessly sleep here. + + XCTestExpectation *expecatation = [self currentSelectorTestExpectation]; + [[commandRunner runCommandAsync:mockedCommand + withOptions:PFCommandRunningOptionRetryIfFailed] continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(task.error, expectedError); + + XCTAssertEqual(performDataURLRequestCount, PFCommandRunningDefaultMaxAttemptsCount); + + [expecatation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(mockedSession); +} + +- (void)testRunFileUpload { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFInstallationIdentifierStoreProvider)); + id mockedSession = PFStrictClassMock([PFURLSession class]); + id mockedRequestConstructor = PFStrictClassMock([PFCommandURLRequestConstructor class]); + + id mockedCommand = PFStrictClassMock([PFRESTCommand class]); + id mockedCommandResult = PFStrictClassMock([PFCommandResult class]); + + NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://foo.bar"]]; + + __block int lastProgress = -1; + PFProgressBlock progressBlock = [^(int progress) { + XCTAssertGreaterThanOrEqual(progress, lastProgress); + lastProgress = progress; + } copy]; + + OCMStub([mockedCommand resolveLocalIds]); + + OCMExpect([mockedRequestConstructor fileUploadURLRequestForCommand:mockedCommand + withContentType:@"content-type" + contentSourceFilePath:@"content-path"]).andReturn(urlRequest); + + [OCMExpect([mockedSession performFileUploadURLRequestAsync:urlRequest + forCommand:mockedCommand + withContentSourceFilePath:@"content-path" + cancellationToken:nil + progressBlock:progressBlock]) + andReturn:[BFTask taskWithResult:mockedCommandResult]]; + + OCMStub([mockedSession invalidateAndCancel]); + + PFURLSessionCommandRunner *commandRunner = [[PFURLSessionCommandRunner alloc] initWithDataSource:mockedDataSource + session:mockedSession + requestConstructor:mockedRequestConstructor]; + + XCTestExpectation *expecatation = [self currentSelectorTestExpectation]; + [[commandRunner runFileUploadCommandAsync:mockedCommand + withContentType:@"content-type" + contentSourceFilePath:@"content-path" + options:0 + cancellationToken:nil + progressBlock:progressBlock] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedCommandResult); + [expecatation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(mockedSession); +} + +- (void)testLocalIdResolution { + id mockedDataSource = PFStrictProtocolMock(@protocol(PFInstallationIdentifierStoreProvider)); + id mockedSession = PFStrictClassMock([PFURLSession class]); + id mockedRequestConstructor = PFStrictClassMock([PFCommandURLRequestConstructor class]); + + id mockedCommand = PFStrictClassMock([PFRESTCommand class]); + id mockedCommandResult = PFStrictClassMock([PFCommandResult class]); + + NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://foo.bar"]]; + + OCMExpect([mockedCommand resolveLocalIds]); + + OCMStub([mockedRequestConstructor dataURLRequestForCommand:mockedCommand]).andReturn(urlRequest); + [OCMStub([mockedSession performDataURLRequestAsync:urlRequest + forCommand:mockedCommand + cancellationToken:nil]) andReturn:[BFTask taskWithResult:mockedCommandResult]]; + + OCMStub([mockedSession invalidateAndCancel]); + + PFURLSessionCommandRunner *commandRunner = [[PFURLSessionCommandRunner alloc] initWithDataSource:mockedDataSource + session:mockedSession + requestConstructor:mockedRequestConstructor]; + + XCTestExpectation *expecatation = [self currentSelectorTestExpectation]; + [[commandRunner runCommandAsync:mockedCommand withOptions:0] continueWithBlock:^id(BFTask *task) { + XCTAssertEqual(task.result, mockedCommandResult); + + [expecatation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(mockedCommand); +} + +@end diff --git a/Tests/Unit/URLSessionDataTaskDelegateTests.m b/Tests/Unit/URLSessionDataTaskDelegateTests.m new file mode 100644 index 000000000..2c1580b3c --- /dev/null +++ b/Tests/Unit/URLSessionDataTaskDelegateTests.m @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import "PFCommandResult.h" +#import "PFConstants.h" +#import "PFTestCase.h" +#import "PFURLSessionJSONDataTaskDelegate.h" + +@interface URLSessionDataTaskDelegateTests : PFTestCase + +@end + +@implementation URLSessionDataTaskDelegateTests + +- (void)testConstructors { + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + BFCancellationTokenSource *tokenSource = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionJSONDataTaskDelegate *delegate = [[PFURLSessionJSONDataTaskDelegate alloc] initForDataTask:mockedTask + withCancellationToken:tokenSource.token]; + XCTAssertNotNil(delegate); + XCTAssertEqual(mockedTask, delegate.dataTask); + XCTAssertNotNil(delegate.resultTask); + + delegate = [PFURLSessionJSONDataTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:tokenSource.token]; + XCTAssertNotNil(delegate); + XCTAssertEqual(mockedTask, delegate.dataTask); + XCTAssertNotNil(delegate.resultTask); + + PFAssertThrowsInconsistencyException([PFURLSessionJSONDataTaskDelegate new]); +} + +- (void)testCancel { + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionJSONDataTaskDelegate *delegate = [PFURLSessionJSONDataTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + XCTAssertFalse(delegate.resultTask.cancelled); + + OCMStub([mockedTask cancel]).andDo(^(NSInvocation *invocation) { + [delegate URLSession:[NSURLSession sharedSession] + task:mockedTask + didCompleteWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil]]; + }); + [source cancel]; + + XCTAssertTrue(delegate.resultTask.cancelled); +} + +- (void)testSuccess { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionJSONDataTaskDelegate *delegate = [PFURLSessionJSONDataTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSData *chunkA = [@"{ \"foo\" :" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *chunkB = [@" \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + + NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:chunkA.length + chunkB.length + textEncodingName:@"UTF-8"]; + + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunkA]; + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunkB]; + + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:nil]; + + PFCommandResult *commandResult = delegate.resultTask.result; + XCTAssertEqualObjects([commandResult result], (@{ @"foo" : @"bar" })); +} + +- (void)testUnknownError { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionJSONDataTaskDelegate *delegate = [PFURLSessionJSONDataTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain code:1337 userInfo:nil]; + + NSData *chunk = [@"{ \"foo\" :" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + statusCode:500 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunk]; + + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:expectedError]; + + XCTAssertEqualObjects(delegate.resultTask.error.userInfo[@"originalError"], expectedError); +} + +- (void)testJSONError { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionJSONDataTaskDelegate *delegate = [PFURLSessionJSONDataTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSError *expectedError = [NSError errorWithDomain:NSCocoaErrorDomain code:3840 userInfo:nil]; + + NSData *chunk = [@"{ \"foo\" :" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunk]; + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:nil]; + + XCTAssertEqualObjects(delegate.resultTask.error.domain, expectedError.domain); + XCTAssertEqual(delegate.resultTask.error.code, expectedError.code); +} + +- (void)testHTTPError { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionJSONDataTaskDelegate *delegate = [PFURLSessionJSONDataTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain + code:1337 + userInfo:@{ + NSLocalizedDescriptionKey : @"An error", + @"temporary" : @1, + @"error" : @"An error", + @"code" : @1337 + }]; + + NSData *chunk = [@"{ \"error\" : \"An error\", \"code\": 1337 }" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + statusCode:500 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunk]; + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:nil]; + + XCTAssertEqualObjects(delegate.resultTask.error, expectedError); +} + +@end diff --git a/Tests/Unit/URLSessionTests.m b/Tests/Unit/URLSessionTests.m new file mode 100644 index 000000000..9fd3b645e --- /dev/null +++ b/Tests/Unit/URLSessionTests.m @@ -0,0 +1,503 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import "PFCommandResult.h" +#import "PFMacros.h" +#import "PFRESTCommand.h" +#import "PFTestCase.h" +#import "PFURLSession.h" +#import "PFURLSession_Private.h" + +// NOTE: NSURLSessionTasks do some *weird* runtime hackery which causes the taskIdentifier property to not +// *actually* exist. This means OCMock cannot stub it. Let's make our own subclass that forces this to work. Note that +// We do not inherit from NSURLSessionTask, as we want this to be as close to a 'strict' mock as possible. +@interface MockedSessionTask : NSObject + +@property (atomic, assign) NSUInteger taskIdentifier; + +@property (nonatomic, strong) void (^resumeBlock)(); +@property (nonatomic, strong) void (^cancelBlock)(); + +@end + +@implementation MockedSessionTask + +@synthesize taskIdentifier; + +- (void)resume { + if (self.resumeBlock) { + self.resumeBlock(); + } else { + [NSException raise:NSInternalInconsistencyException format:@"Resume block not set!"]; + } +} + +- (void)cancel { + if (self.cancelBlock) { + self.cancelBlock(); + } else { + [NSException raise:NSInternalInconsistencyException format:@"Cancel block not set!"]; + } +} + +@end + +@interface URLSessionTests : PFTestCase + +@end + +@implementation URLSessionTests + +- (void)testConstructors { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *URLSession = [NSURLSession sharedSession]; + + PFURLSession *session = [[PFURLSession alloc] initWithConfiguration:configuration]; + XCTAssertNotNil(session); + [session invalidateAndCancel]; + + session = [PFURLSession sessionWithConfiguration:configuration]; + XCTAssertNotNil(session); + [session invalidateAndCancel]; + + session = [[PFURLSession alloc] initWithURLSession:URLSession]; + XCTAssertNotNil(URLSession); + + session = [PFURLSession sessionWithURLSession:URLSession]; + XCTAssertNotNil(session); + + PFAssertThrowsInconsistencyException([PFURLSession new]); +} + +- (void)testPerformDataRequestSuccess { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + MockedSessionTask *mockedDataTask = [[MockedSessionTask alloc] init]; + + __block id sessionDelegate = nil; + + OCMExpect([mockedURLSession dataTaskWithRequest:mockedURLRequest]).andReturn(mockedDataTask); + + mockedDataTask.taskIdentifier = 1337; + + @weakify(mockedDataTask); + mockedDataTask.resumeBlock = ^{ + @strongify(mockedDataTask); + + NSData *dataRecieved = [@"{ \"foo\": \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:dataRecieved.length + textEncodingName:@"UTF-8"]; + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedDataTask + didReceiveResponse:response + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + [sessionDelegate URLSession:mockedURLSession dataTask:(id)mockedDataTask didReceiveData:dataRecieved]; + + [sessionDelegate URLSession:mockedURLSession task:(id)mockedDataTask didCompleteWithError:nil]; + }; + + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + sessionDelegate = (id)session; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[session performDataURLRequestAsync:mockedURLRequest forCommand:mockedCommand cancellationToken:nil] continueWithBlock:^id(BFTask *task) { + PFCommandResult *actualResult = task.result; + XCTAssertEqualObjects(actualResult.result, (@{ @"foo" : @"bar" })); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)mockedURLSession); + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + +- (void)testPerformDataRequesPreCancel { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + + [cancellationTokenSource cancel]; + [[session performDataURLRequestAsync:mockedURLRequest + forCommand:mockedCommand + cancellationToken:cancellationTokenSource.token] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + + +- (void)testPerformDataRequestCancellation { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + MockedSessionTask *mockedDataTask = [[MockedSessionTask alloc] init]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + __block id sessionDelegate = nil; + + OCMExpect([mockedURLSession dataTaskWithRequest:mockedURLRequest]).andReturn(mockedDataTask); + + mockedDataTask.taskIdentifier = 1337; + + @weakify(mockedDataTask); + mockedDataTask.resumeBlock = ^{ + @strongify(mockedDataTask); + + NSData *dataRecieved = [@"{ \"foo\": \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:dataRecieved.length + textEncodingName:@"UTF-8"]; + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedDataTask + didReceiveResponse:response + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + [sessionDelegate URLSession:mockedURLSession dataTask:(id)mockedDataTask didReceiveData:dataRecieved]; + [cancellationTokenSource cancel]; + + [sessionDelegate URLSession:mockedURLSession + task:(id)mockedDataTask + didCompleteWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil]]; + }; + + XCTestExpectation *cancelExpectation = [self expectationWithDescription:@"cancel"]; + mockedDataTask.cancelBlock = ^{ + [cancelExpectation fulfill]; + }; + + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + sessionDelegate = (id)session; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[session performDataURLRequestAsync:mockedURLRequest + forCommand:mockedCommand + cancellationToken:cancellationTokenSource.token] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)mockedURLSession); + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + +- (void)testPerformDataRequestError { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + MockedSessionTask *mockedDataTask = [[MockedSessionTask alloc] init]; + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain code:1337 userInfo:nil]; + __block id sessionDelegate = nil; + + OCMExpect([mockedURLSession dataTaskWithRequest:mockedURLRequest]).andReturn(mockedDataTask); + + mockedDataTask.taskIdentifier = 1337; + + @weakify(mockedDataTask); + mockedDataTask.resumeBlock = ^{ + @strongify(mockedDataTask); + [sessionDelegate URLSession:mockedURLSession task:(id)mockedDataTask didCompleteWithError:expectedError]; + }; + + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + sessionDelegate = (id)session; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[session performDataURLRequestAsync:mockedURLRequest forCommand:mockedCommand cancellationToken:nil] + continueWithBlock:^id(BFTask *task) { + XCTAssertEqualObjects(expectedError, task.error.userInfo[@"originalError"]); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)mockedURLSession); + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + +- (void)testFileUploadRequestPreCancel { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + NSString *exampleFile = @"file.txt"; + + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [cancellationTokenSource cancel]; + [[session performFileUploadURLRequestAsync:mockedURLRequest + forCommand:mockedCommand + withContentSourceFilePath:exampleFile + cancellationToken:cancellationTokenSource.token + progressBlock:^(int percentDone) { + XCTFail(); + }] + continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + +- (void)testFileUploadSuccess { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + MockedSessionTask *mockedUploadTask = [[MockedSessionTask alloc] init]; + + NSString *exampleFile = @"file.txt"; + __block id sessionDelegate = nil; + + OCMExpect([mockedURLSession uploadTaskWithRequest:mockedURLRequest fromFile:[NSURL fileURLWithPath:exampleFile]]).andReturn(mockedUploadTask); + + mockedUploadTask.taskIdentifier = 1337; + + @weakify(mockedUploadTask); + mockedUploadTask.resumeBlock = ^{ + @strongify(mockedUploadTask); + NSData *dataToSend = [@"{ \"foo\": \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:dataToSend.length + textEncodingName:@"UTF-8"]; + + for (NSUInteger progress = 0; progress < dataToSend.length; progress++) { + [sessionDelegate URLSession:mockedURLSession + task:(id)mockedUploadTask + didSendBodyData:1 + totalBytesSent:progress + totalBytesExpectedToSend:dataToSend.length]; + } + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedUploadTask + didReceiveResponse:response + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedUploadTask + didReceiveData:dataToSend]; + + [sessionDelegate URLSession:mockedURLSession + task:(id)mockedUploadTask + didCompleteWithError:nil]; + }; + + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + sessionDelegate = (id)session; + + __block int lastProgress = 0; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[session performFileUploadURLRequestAsync:mockedURLRequest + forCommand:mockedCommand + withContentSourceFilePath:exampleFile + cancellationToken:nil + progressBlock:^(int percentDone) { + XCTAssertGreaterThanOrEqual(percentDone, lastProgress); + lastProgress = percentDone; + }] + continueWithBlock:^id(BFTask *task) { + PFCommandResult *actualResult = task.result; + XCTAssertEqualObjects(actualResult.result, (@{ @"foo" : @"bar" })); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)mockedURLSession); + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + + +- (void)testFileUploadRequestCancellation { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + MockedSessionTask *mockedUploadTask = [[MockedSessionTask alloc] init]; + + BFCancellationTokenSource *cancellationTokenSource = [BFCancellationTokenSource cancellationTokenSource]; + NSString *exampleFile = @"file.txt"; + __block id sessionDelegate = nil; + + OCMExpect([mockedURLSession uploadTaskWithRequest:mockedURLRequest fromFile:[NSURL fileURLWithPath:exampleFile]]).andReturn(mockedUploadTask); + + mockedUploadTask.taskIdentifier = 1337; + + @weakify(mockedUploadTask); + mockedUploadTask.resumeBlock = ^{ + @strongify(mockedUploadTask); + + NSData *dataToSend = [@"{ \"foo\": \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:dataToSend.length + textEncodingName:@"UTF-8"]; + + for (NSUInteger progress = 0; progress < dataToSend.length; progress++) { + [sessionDelegate URLSession:mockedURLSession + task:(id)mockedUploadTask + didSendBodyData:1 + totalBytesSent:progress + totalBytesExpectedToSend:dataToSend.length]; + } + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedUploadTask + didReceiveResponse:response + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedUploadTask + didReceiveData:dataToSend]; + + [cancellationTokenSource cancel]; + + [sessionDelegate URLSession:mockedURLSession + task:(id)mockedUploadTask + didCompleteWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil]]; + }; + + XCTestExpectation *cancelExpectation = [self expectationWithDescription:@"cancel"]; + mockedUploadTask.cancelBlock = ^{ + [cancelExpectation fulfill]; + }; + + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + sessionDelegate = (id)session; + + __block int lastProgress = 0; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[session performFileUploadURLRequestAsync:mockedURLRequest + forCommand:mockedCommand + withContentSourceFilePath:exampleFile + cancellationToken:cancellationTokenSource.token + progressBlock:^(int percentDone) { + XCTAssertGreaterThanOrEqual(percentDone, lastProgress); + lastProgress = percentDone; + }] + continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.cancelled); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)mockedURLSession); + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + +- (void)testCaching { + NSURLSession *mockedURLSession = PFStrictClassMock([NSURLSession class]); + NSURLRequest *mockedURLRequest = PFStrictClassMock([NSURLRequest class]); + PFRESTCommand *mockedCommand = PFStrictClassMock([PFRESTCommand class]); + NSArray *mocks = @[ mockedURLSession, mockedURLRequest, mockedCommand ]; + + MockedSessionTask *mockedDataTask = [[MockedSessionTask alloc] init]; + + __block id sessionDelegate = nil; + + OCMStub([mockedURLSession dataTaskWithRequest:mockedURLRequest]).andReturn(mockedDataTask); + + mockedDataTask.taskIdentifier = 1337; + @weakify(mockedDataTask); + mockedDataTask.resumeBlock = ^{ + @strongify(mockedDataTask); + NSData *dataRecieved = [@"{ \"foo\": \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:dataRecieved.length + textEncodingName:@"UTF-8"]; + + NSCachedURLResponse *cachedResponse = + [[NSCachedURLResponse alloc] initWithResponse:response data:dataRecieved]; + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedDataTask + didReceiveResponse:response + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + [sessionDelegate URLSession:mockedURLSession dataTask:(id)mockedDataTask didReceiveData:dataRecieved]; + [sessionDelegate URLSession:mockedURLSession task:(id)mockedDataTask didCompleteWithError:nil]; + + [sessionDelegate URLSession:mockedURLSession + dataTask:(id)mockedDataTask + willCacheResponse:cachedResponse + completionHandler:^(NSCachedURLResponse *cached) { XCTAssertNil(cached); }]; + }; + + PFURLSession *session = [PFURLSession sessionWithURLSession:mockedURLSession]; + sessionDelegate = (id)session; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[session performDataURLRequestAsync:mockedURLRequest forCommand:mockedCommand cancellationToken:nil] + continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll((id)mockedURLSession); + [mocks makeObjectsPerformSelector:@selector(stopMocking)]; +} + +- (void)testInvalidate { + PFURLSession *session = [PFURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + XCTAssertNoThrow([session invalidateAndCancel]); // lol? +} + +@end diff --git a/Tests/Unit/URLSessionUploadTaskDelegateTests.m b/Tests/Unit/URLSessionUploadTaskDelegateTests.m new file mode 100644 index 000000000..c3b46b109 --- /dev/null +++ b/Tests/Unit/URLSessionUploadTaskDelegateTests.m @@ -0,0 +1,286 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import "PFCommandResult.h" +#import "PFTestCase.h" +#import "PFURLSessionUploadTaskDelegate.h" + +@interface URLSessionUploadTaskDelegateTests : PFTestCase + +@end + +@implementation URLSessionUploadTaskDelegateTests + +- (void)testConstructors { + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + BFCancellationTokenSource *tokenSource = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionUploadTaskDelegate *delegate = [[PFURLSessionUploadTaskDelegate alloc] initForDataTask:mockedTask + withCancellationToken:tokenSource.token]; + XCTAssertNotNil(delegate); + XCTAssertEqual(delegate.dataTask, mockedTask); + XCTAssertNotNil(delegate.resultTask); + + delegate = [[PFURLSessionUploadTaskDelegate alloc] initForDataTask:mockedTask + withCancellationToken:tokenSource.token + uploadProgressBlock:^(int percentDone) { + XCTFail(); + }]; + XCTAssertNotNil(delegate); + XCTAssertEqual(delegate.dataTask, mockedTask); + XCTAssertNotNil(delegate.resultTask); + + delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:tokenSource.token]; + XCTAssertNotNil(delegate); + XCTAssertEqual(delegate.dataTask, mockedTask); + XCTAssertNotNil(delegate.resultTask); + + delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:tokenSource.token + uploadProgressBlock:^(int percentDone) { + XCTFail(); + }]; + XCTAssertNotNil(delegate); + XCTAssertEqual(delegate.dataTask, mockedTask); + XCTAssertNotNil(delegate.resultTask); + + PFAssertThrowsInconsistencyException([PFURLSessionUploadTaskDelegate new]); +} + +- (void)testCancel { + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + BFCancellationTokenSource *tokenSource = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionUploadTaskDelegate *delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:tokenSource.token]; + XCTAssertFalse(delegate.resultTask.cancelled); + + OCMStub([mockedTask cancel]).andDo(^(NSInvocation *invocation) { + [delegate URLSession:[NSURLSession sharedSession] + task:mockedTask + didCompleteWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil]]; + }); + [tokenSource cancel]; + + XCTAssertTrue(delegate.resultTask.cancelled); +} + +- (void)testUploadProgress { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + __block int lastProgress = 0; + XCTestExpectation *progressExpectation = [self expectationWithDescription:@"progress"]; + PFProgressBlock progressBlock = ^(int percentDone) { + XCTAssertEqual(lastProgress + 10, percentDone); + lastProgress = percentDone; + + if (percentDone == 100) { + [progressExpectation fulfill]; + } + }; + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionUploadTaskDelegate *delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token + uploadProgressBlock:progressBlock]; + + NSData *chunkA = [@"{ \"foo\" :" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *chunkB = [@" \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + + NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:chunkA.length + chunkB.length + textEncodingName:@"UTF-8"]; + + for (int progress = 1; progress <= 10; progress++) { + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:1 + totalBytesSent:progress + totalBytesExpectedToSend:10]; + } + + [self waitForTestExpectations]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunkA]; + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunkB]; + + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:nil]; + + PFCommandResult *commandResult = delegate.resultTask.result; + XCTAssertEqual(lastProgress, 100); + XCTAssertEqualObjects([commandResult result], (@{ @"foo" : @"bar" })); +} + +///-------------------------------------- +#pragma mark - Data task delegate tests +///-------------------------------------- + +- (void)testSuccess { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionUploadTaskDelegate *delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSData *chunkA = [@"{ \"foo\" :" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *chunkB = [@" \"bar\" }" dataUsingEncoding:NSUTF8StringEncoding]; + + NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + MIMEType:@"application/json" + expectedContentLength:chunkA.length + chunkB.length + textEncodingName:@"UTF-8"]; + + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunkA]; + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunkB]; + + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:nil]; + + PFCommandResult *commandResult = delegate.resultTask.result; + XCTAssertEqualObjects([commandResult result], (@{ @"foo" : @"bar" })); +} + +- (void)testUnknownError { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionUploadTaskDelegate *delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain code:1337 userInfo:nil]; + + NSData *chunk = [@"{ \"foo\" :" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + statusCode:500 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunk]; + + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:expectedError]; + + XCTAssertEqualObjects(delegate.resultTask.error.userInfo[@"originalError"], expectedError); +} + +- (void)testJSONError { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionUploadTaskDelegate *delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSError *expectedError = [NSError errorWithDomain:NSCocoaErrorDomain code:3840 userInfo:nil]; + + NSData *chunk = [@"{ \"foo\" :" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunk]; + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:nil]; + + XCTAssertEqualObjects(delegate.resultTask.error.domain, expectedError.domain); + XCTAssertEqual(delegate.resultTask.error.code, expectedError.code); +} + +- (void)testHTTPError { + NSURLSession *mockedSession = PFStrictClassMock([NSURLSession class]); + id mockedTask = PFStrictClassMock([NSURLSessionTask class]); + + BFCancellationTokenSource *source = [BFCancellationTokenSource cancellationTokenSource]; + PFURLSessionUploadTaskDelegate *delegate = [PFURLSessionUploadTaskDelegate taskDelegateForDataTask:mockedTask + withCancellationToken:source.token]; + + NSError *expectedError = [NSError errorWithDomain:PFParseErrorDomain + code:1337 + userInfo:@{ + NSLocalizedDescriptionKey : @"An error", + @"temporary" : @1, + @"error" : @"An error", + @"code" : @1337 + }]; + + NSData *chunk = [@"{ \"error\" : \"An error\", \"code\": 1337 }" dataUsingEncoding:NSUTF8StringEncoding]; + NSURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://foo.bar"] + statusCode:500 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + [delegate URLSession:mockedSession + task:mockedTask + didSendBodyData:5 + totalBytesSent:5 +totalBytesExpectedToSend:5]; + + [delegate URLSession:mockedSession + dataTask:mockedTask + didReceiveResponse:urlResponse + completionHandler:^(NSURLSessionResponseDisposition disposition) { + XCTAssertEqual(disposition, NSURLSessionResponseAllow); + }]; + + [delegate URLSession:mockedSession dataTask:mockedTask didReceiveData:chunk]; + [delegate URLSession:mockedSession task:mockedTask didCompleteWithError:nil]; + + XCTAssertEqualObjects(delegate.resultTask.error, expectedError); +} + +@end diff --git a/Tests/Unit/UserCommandTests.m b/Tests/Unit/UserCommandTests.m new file mode 100644 index 000000000..148b00c1f --- /dev/null +++ b/Tests/Unit/UserCommandTests.m @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFHTTPRequest.h" +#import "PFRESTUserCommand.h" +#import "PFTestCase.h" + +@interface UserCommandTests : PFTestCase + +@end + +@implementation UserCommandTests + +- (void)testLogInCommand { + PFRESTUserCommand *command = [PFRESTUserCommand logInUserCommandWithUsername:@"a" + password:@"b" + revocableSession:YES]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"login"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNotNil(command.parameters); + XCTAssertNotNil(command.parameters[@"username"]); + XCTAssertNotNil(command.parameters[@"password"]); + XCTAssertEqual(command.additionalRequestHeaders.count, 1); + XCTAssertTrue(command.revocableSessionEnabled); + XCTAssertNil(command.sessionToken); + + command = [PFRESTUserCommand logInUserCommandWithUsername:@"a" + password:@"b" + revocableSession:NO]; + XCTAssertNotNil(command); + XCTAssertEqual(command.additionalRequestHeaders.count, 0); + XCTAssertFalse(command.revocableSessionEnabled); +} + +- (void)testServiceLoginCommandWithAuthTypeData { + PFRESTUserCommand *command = [PFRESTUserCommand serviceLoginUserCommandWithAuthenticationType:@"a" + authenticationData:@{ @"b" : @"c" } + revocableSession:YES]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"users"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters); + XCTAssertNotNil(command.parameters[@"authData"]); + XCTAssertEqualObjects(command.parameters[@"authData"], @{ @"a" : @{@"b" : @"c"} }); + XCTAssertEqual(command.additionalRequestHeaders.count, 1); + XCTAssertTrue(command.revocableSessionEnabled); + XCTAssertNil(command.sessionToken); + + command = [PFRESTUserCommand serviceLoginUserCommandWithAuthenticationType:@"a" + authenticationData:@{ @"b" : @"c" } + revocableSession:NO]; + XCTAssertNotNil(command); + XCTAssertEqual(command.additionalRequestHeaders.count, 0); + XCTAssertFalse(command.revocableSessionEnabled); +} + +- (void)testServiceLoginCommandWithParameters { + PFRESTUserCommand *command = [PFRESTUserCommand serviceLoginUserCommandWithParameters:@{ @"authData" : @{@"b" : @"c"} } + revocableSession:YES + sessionToken:@"Yarr"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"users"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters); + XCTAssertNotNil(command.parameters[@"authData"]); + XCTAssertEqualObjects(command.parameters[@"authData"], @{ @"b" : @"c" }); + XCTAssertEqual(command.additionalRequestHeaders.count, 1); + XCTAssertTrue(command.revocableSessionEnabled); + XCTAssertEqualObjects(command.sessionToken, @"Yarr"); + + command = [PFRESTUserCommand serviceLoginUserCommandWithParameters:@{ @"authData" : @{@"b" : @"c"} } + revocableSession:NO + sessionToken:@"Yarr!"]; + XCTAssertNotNil(command); + XCTAssertEqual(command.additionalRequestHeaders.count, 0); + XCTAssertFalse(command.revocableSessionEnabled); +} + +- (void)testSignUpCommand { + PFRESTUserCommand *command = [PFRESTUserCommand signUpUserCommandWithParameters:@{ @"k" : @"v" } + revocableSession:YES + sessionToken:@"Boom"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"users"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters[@"k"]); + XCTAssertEqual(command.additionalRequestHeaders.count, 1); + XCTAssertTrue(command.revocableSessionEnabled); + XCTAssertEqualObjects(command.sessionToken, @"Boom"); + + command = [PFRESTUserCommand signUpUserCommandWithParameters:@{ @"k" : @"v" } + revocableSession:NO + sessionToken:@"Boom"]; + XCTAssertNotNil(command); + XCTAssertEqual(command.additionalRequestHeaders.count, 0); + XCTAssertFalse(command.revocableSessionEnabled); +} + +- (void)testGetCurrentUserCommand { + PFRESTUserCommand *command = [PFRESTUserCommand getCurrentUserCommandWithSessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"users/me"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +- (void)testUpgradeToRevocableSessionCommand { + PFRESTUserCommand *command = [PFRESTUserCommand upgradeToRevocableSessionCommandWithSessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"upgradeToRevocableSession"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +- (void)testLogOutUserCommand { + PFRESTUserCommand *command = [PFRESTUserCommand logOutUserCommandWithSessionToken:@"yolo"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"logout"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNil(command.parameters); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); +} + +- (void)testResetPasswordCommand { + PFRESTUserCommand *command = [PFRESTUserCommand resetPasswordCommandForUserWithEmail:@"nlutsenko@me.com"]; + XCTAssertNotNil(command); + XCTAssertEqualObjects(command.httpPath, @"requestPasswordReset"); + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotNil(command.parameters[@"email"]); + XCTAssertNil(command.sessionToken); +} + +@end diff --git a/Tests/Unit/UserControllerTests.m b/Tests/Unit/UserControllerTests.m new file mode 100644 index 000000000..1241316e7 --- /dev/null +++ b/Tests/Unit/UserControllerTests.m @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "OCMock+Parse.h" +#import "PFCommandResult.h" +#import "PFCommandRunning.h" +#import "PFCurrentUserController.h" +#import "PFHTTPRequest.h" +#import "PFObjectControlling.h" +#import "PFRESTUserCommand.h" +#import "PFUnitTestCase.h" +#import "PFUser.h" +#import "PFUserController.h" + +@interface UserControllerTests : PFUnitTestCase + +@end + +@implementation UserControllerTests + +///-------------------------------------- +#pragma mark - Helpers +///-------------------------------------- + +- (id)mockedCommonDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFCommandRunnerProvider)); + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + OCMStub([dataSource commandRunner]).andReturn(commandRunner); + return dataSource; +} + +- (id)mockedCoreDataSource { + id dataSource = PFStrictProtocolMock(@protocol(PFCurrentUserControllerProvider)); + id currentUserController = PFStrictClassMock([PFCurrentUserController class]); + OCMStub([dataSource currentUserController]).andReturn(currentUserController); + return dataSource; +} + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructors { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + + PFUserController *controller = [[PFUserController alloc] initWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.commonDataSource, commonDataSource); + XCTAssertEqual((id)controller.coreDataSource, coreDataSource); + + controller = [PFUserController controllerWithCommonDataSource:commonDataSource coreDataSource:coreDataSource]; + XCTAssertNotNil(controller); + XCTAssertEqual((id)controller.commonDataSource, commonDataSource); + XCTAssertEqual((id)controller.coreDataSource, coreDataSource); +} + +- (void)testLogInCurrentUserWithSessionToken { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + id commandRunner = [commonDataSource commandRunner]; + + id commandResult = @{ @"objectId" : @"a", + @"yarr" : @1 }; + [commandRunner mockCommandResult:commandResult forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNotEqual([command.httpPath rangeOfString:@"users/me"].location, NSNotFound); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); + XCTAssertNil(command.parameters); + + return YES; + }]; + + __block PFUser *savedUser = nil; + + id currentUserController = [coreDataSource currentUserController]; + [OCMExpect([currentUserController saveCurrentObjectAsync:[OCMArg checkWithBlock:^BOOL(id obj) { + savedUser = obj; + return (savedUser != nil); + }]]) andReturn:[BFTask taskWithResult:nil]]; + + PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller logInCurrentUserAsyncWithSessionToken:@"yarr"] continueWithBlock:^id(BFTask *task) { + PFUser *user = task.result; + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.objectId, @"a"); + XCTAssertEqualObjects(user[@"yarr"], @1); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(currentUserController); +} + +- (void)testLogInCurrentUserWithSessionTokenNullResult { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + id commandRunner = [commonDataSource commandRunner]; + [commandRunner mockCommandResult:[NSNull null] forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNotEqual([command.httpPath rangeOfString:@"users/me"].location, NSNotFound); + XCTAssertEqualObjects(command.sessionToken, @"yarr"); + XCTAssertNil(command.parameters); + + return YES; + }]; + + PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller logInCurrentUserAsyncWithSessionToken:@"yarr"] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.faulted); + NSError *error = task.error; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, kPFErrorObjectNotFound); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; +} + +- (void)testLogInCurrentUserWithUsernamePassword { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + id commandRunner = [commonDataSource commandRunner]; + + id commandResult = @{ @"objectId" : @"a", + @"yarr" : @1 }; + [commandRunner mockCommandResult:commandResult forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNotEqual([command.httpPath rangeOfString:@"login"].location, NSNotFound); + XCTAssertNil(command.sessionToken); + XCTAssertEqualObjects(command.parameters, (@{ @"username" : @"yolo" , @"password" : @"yarr" })); + XCTAssertEqualObjects(command.additionalRequestHeaders, @{ @"X-Parse-Revocable-Session" : @"1" }); + + return YES; + }]; + + __block PFUser *savedUser = nil; + + id currentUserController = [coreDataSource currentUserController]; + [OCMExpect([currentUserController saveCurrentObjectAsync:[OCMArg checkWithBlock:^BOOL(id obj) { + savedUser = obj; + return (savedUser != nil); + }]]) andReturn:[BFTask taskWithResult:nil]]; + + PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller logInCurrentUserAsyncWithUsername:@"yolo" + password:@"yarr" + revocableSession:YES] continueWithBlock:^id(BFTask *task) { + PFUser *user = task.result; + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.objectId, @"a"); + XCTAssertEqualObjects(user[@"yarr"], @1); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(currentUserController); +} + +- (void)testLogInCurrentUserWithUsernamePasswordNullResult { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + id commandRunner = [commonDataSource commandRunner]; + [commandRunner mockCommandResult:[NSNull null] forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodGET); + XCTAssertNotEqual([command.httpPath rangeOfString:@"login"].location, NSNotFound); + XCTAssertNil(command.sessionToken); + XCTAssertEqualObjects(command.parameters, (@{ @"username" : @"yolo" , @"password" : @"yarr" })); + + return YES; + }]; + + PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller logInCurrentUserAsyncWithUsername:@"yolo" + password:@"yarr" + revocableSession:NO] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.faulted); + NSError *error = task.error; + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PFParseErrorDomain); + XCTAssertEqual(error.code, kPFErrorObjectNotFound); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(commandRunner); +} + +- (void)testRequestPasswordReset { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + id commandRunner = [commonDataSource commandRunner]; + [commandRunner mockCommandResult:@{ @"a" : @"b" } forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotEqual([command.httpPath rangeOfString:@"requestPasswordReset"].location, NSNotFound); + XCTAssertNil(command.sessionToken); + XCTAssertEqualObjects(command.parameters[@"email"], @"yarr@yolo.com"); + + return YES; + }]; + + PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller requestPasswordResetAsyncForEmail:@"yarr@yolo.com"] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertFalse(task.cancelled); + XCTAssertNil(task.result); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(commandRunner); +} + +- (void)testLogOutAsync { + id commonDataSource = [self mockedCommonDataSource]; + id coreDataSource = [self mockedCoreDataSource]; + id commandRunner = [commonDataSource commandRunner]; + [commandRunner mockCommandResult:@{ @"a" : @"b" } forCommandsPassingTest:^BOOL(id obj) { + PFRESTCommand *command = obj; + + XCTAssertEqualObjects(command.httpMethod, PFHTTPRequestMethodPOST); + XCTAssertNotEqual([command.httpPath rangeOfString:@"logout"].location, NSNotFound); + XCTAssertEqualObjects(command.sessionToken, @"yolo"); + + return YES; + }]; + + PFUserController *controller = [PFUserController controllerWithCommonDataSource:commonDataSource + coreDataSource:coreDataSource]; + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[controller logOutUserAsyncWithSessionToken:@"yolo"] continueWithBlock:^id(BFTask *task) { + XCTAssertFalse(task.faulted); + XCTAssertFalse(task.cancelled); + XCTAssertNil(task.result); + [expectation fulfill]; + return nil; + }]; + [self waitForTestExpectations]; + + OCMVerifyAll(commandRunner); +} + +@end diff --git a/Tests/Unit/UserFileCodingLogicTests.m b/Tests/Unit/UserFileCodingLogicTests.m new file mode 100644 index 000000000..c99d4d98c --- /dev/null +++ b/Tests/Unit/UserFileCodingLogicTests.m @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFDecoder.h" +#import "PFUnitTestCase.h" +#import "PFUserFileCodingLogic.h" +#import "PFUserPrivate.h" + +@interface UserFileCodingLogicTests : PFUnitTestCase + +@end + +@implementation UserFileCodingLogicTests + +- (void)testConstructors { + PFUserFileCodingLogic *logic = [[PFUserFileCodingLogic alloc] init]; + XCTAssertNotNil(logic); + + logic = [PFUserFileCodingLogic codingLogic]; + XCTAssertNotNil(logic); + + XCTAssertNotEqual([PFUserFileCodingLogic codingLogic], [PFUserFileCodingLogic codingLogic]); +} + +- (void)testUpdateObject { + NSDictionary *dictionary = @{ @"className" : @"Yolo", + @"data" : @{@"objectId" : @"100500", @"slogan" : @"yarr"}, + @"sessionToken" : @"tokenpff", + @"authData" : @{@"a" : @"b", @"c" : [NSNull null]} }; + PFUser *user = [PFUser user]; + user.authData[@"c"] = @"d"; + [user.linkedServiceNames addObject:@"c"]; + + PFUserFileCodingLogic *logic = [PFUserFileCodingLogic codingLogic]; + [logic updateObject:user fromDictionary:dictionary usingDecoder:[PFDecoder objectDecoder]]; + + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.objectId, @"100500"); + XCTAssertEqualObjects(user[@"slogan"], @"yarr"); + XCTAssertTrue(user.isDataAvailable); + + XCTAssertEqualObjects(user.authData, @{ @"a" : @"b" }); + XCTAssertEqualObjects(user.linkedServiceNames, [NSSet setWithObject:@"a"]); + XCTAssertEqualObjects(user.sessionToken, @"tokenpff"); +} + +- (void)testUpdateObjectWithLegacyKeys { + NSDictionary *dictionary = @{ @"session_token" : @"tokenpff", + @"auth_data" : @{@"a" : @"b", @"c" : [NSNull null]} }; + PFUser *user = [PFUser user]; + user.authData[@"c"] = @"d"; + [user.linkedServiceNames addObject:@"c"]; + + PFUserFileCodingLogic *logic = [PFUserFileCodingLogic codingLogic]; + [logic updateObject:user fromDictionary:dictionary usingDecoder:[PFDecoder objectDecoder]]; + + XCTAssertNotNil(user); + XCTAssertTrue(user.isDataAvailable); + + XCTAssertEqualObjects(user.authData, @{ @"a" : @"b" }); + XCTAssertEqualObjects(user.linkedServiceNames, [NSSet setWithObject:@"a"]); + XCTAssertEqualObjects(user.sessionToken, @"tokenpff"); +} + +@end diff --git a/Tests/Unit/UserUnitTests.m b/Tests/Unit/UserUnitTests.m new file mode 100644 index 000000000..3562c1e63 --- /dev/null +++ b/Tests/Unit/UserUnitTests.m @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFUnitTestCase.h" +#import "PFUser.h" + +@interface UserUnitTests : PFUnitTestCase + +@end + +@implementation UserUnitTests + +///-------------------------------------- +#pragma mark - Tests +///-------------------------------------- + +- (void)testConstructorsClassNameValidation { + PFAssertThrowsInvalidArgumentException([[PFUser alloc] initWithClassName:@"notuserclass"], + @"Should throw an exception for invalid classname"); +} + +- (void)testImmutableFieldsCannotBeChanged { + [PFUser registerSubclass]; + + PFUser *user = [PFUser object]; + PFAssertThrowsInvalidArgumentException(user[@"sessionToken"] = @"a"); +} + +- (void)testImmutableFieldsCannotBeDeleted { + [PFUser registerSubclass]; + + PFUser *user = [PFUser object]; + PFAssertThrowsInvalidArgumentException([user removeObjectForKey:@"username"]); + PFAssertThrowsInvalidArgumentException([user removeObjectForKey:@"sessionToken"]); +} + +@end diff --git a/Tests/testServer.config b/Tests/testServer.config new file mode 100644 index 000000000..e69de29bb diff --git a/Vendor/Bolts-ObjC b/Vendor/Bolts-ObjC new file mode 160000 index 000000000..5e4f650b2 --- /dev/null +++ b/Vendor/Bolts-ObjC @@ -0,0 +1 @@ +Subproject commit 5e4f650b22a6207e0685f22ca3b4b276e5acda56 diff --git a/Vendor/OCMock b/Vendor/OCMock new file mode 160000 index 000000000..cdc5c7c58 --- /dev/null +++ b/Vendor/OCMock @@ -0,0 +1 @@ +Subproject commit cdc5c7c5812e911677190ceb5d48609f2e5f41c8 diff --git a/third_party_licenses.txt b/third_party_licenses.txt new file mode 100644 index 000000000..a49d8d15a --- /dev/null +++ b/third_party_licenses.txt @@ -0,0 +1,68 @@ +THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THE PARSE PRODUCT. + +----- + +The following software may be included in this product: OAuthCore. This software contains the following license and notice below: + +Copyright (C) 2012 Loren Brichter + +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. + +----- + +The following software may be included in this product: google-breakpad. This software contains the following license and notice below: + +Copyright (c) 2006, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------- + +Copyright 2001-2004 Unicode, Inc. + +Disclaimer + +This source code is provided as is by Unicode, Inc. No claims are +made as to fitness for any particular purpose. No warranties of any +kind are expressed or implied. The recipient agrees to determine +applicability of information provided. If this file has been +purchased on magnetic or optical media from Unicode, Inc., the +sole remedy for any claim will be exchange of defective media +within 90 days of receipt. + +Limitations on Rights to Redistribute This Code + +Unicode, Inc. hereby grants the right to freely use the information +supplied in this file in the creation of products supporting the +Unicode Standard, and to make copies of this file in any form +for internal or external distribution as long as this notice +remains attached.