diff --git a/.codecov.yml b/.codecov.yml index 709600189..85e35079b 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,10 +5,10 @@ coverage: status: patch: default: - target: auto + target: 73 changes: false project: default: - target: 72 + target: 74 comment: require_changes: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 010cfa3d9..51a5d2c87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,9 +66,29 @@ jobs: run: xcrun llvm-cov export -format="lcov" .build/debug/ParseSwiftPackageTests.xctest/Contents/MacOS/ParseSwiftPackageTests -instr-profile .build/debug/codecov/default.profdata > info.lcov - name: Send codecov run: bash <(curl https://codecov.io/bash) - + + spm-test-5_2: + needs: xcode-build-watchos + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Create and set the default keychain + run: | + security create-keychain -p "" temporary + security default-keychain -s temporary + security unlock-keychain -p "" temporary + security set-keychain-settings -lut 7200 temporary + - name: Build + run: swift build -v + env: + DEVELOPER_DIR: ${{ env.CI_XCODE_VER }} + - name: Test + run: swift test --enable-code-coverage -v + env: + DEVELOPER_DIR: ${{ env.CI_XCODE_VER }} + docs: - needs: spm-test + needs: xcode-build-watchos runs-on: macos-latest steps: - uses: actions/checkout@v2 @@ -94,7 +114,7 @@ jobs: publish_dir: ./docs cocoapods: - needs: spm-test + needs: xcode-build-watchos runs-on: macos-latest steps: - uses: actions/checkout@v2 @@ -102,7 +122,7 @@ jobs: run: pod lib lint --allow-warnings carthage: - needs: spm-test + needs: xcode-build-watchos runs-on: macos-latest steps: - uses: actions/checkout@v2 diff --git a/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift new file mode 100644 index 000000000..d644f8a80 --- /dev/null +++ b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift @@ -0,0 +1,38 @@ +//: [Previous](@previous) + +import PlaygroundSupport +import Foundation +import ParseSwift + +PlaygroundPage.current.needsIndefiniteExecution = true +initializeParse() + +//: Create your own ValueTyped ParseCloud type +struct Cloud: ParseCloud { + //: These are required for Object + var functionJobName: String + + //: If your cloud function takes arguments, they can be passed by creating properties + //var argument1: [String: Int] = ["test": 5] +} + +/*: Assuming you have the Cloud Function named "hello" on your parse-server: + // main.js + Parse.Cloud.define('hello', async () => { + return 'Hello world!'; + }); + */ +let cloud = Cloud(functionJobName: "hello") + +cloud.callFunction { result in + switch result { + case .success(let response): + print("Response from cloud function: \(response)") + case .failure(let error): + assertionFailure("Error calling cloud function: \(error)") + } +} + +//: Jobs can be run the same way by using the method `callJob()` + +//: [Next](@next) diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 42b9eb0f0..e37676353 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -35,7 +35,7 @@ User.current?.save { results in case .success(let updatedUser): print("Successfully save myCustomKey to ParseServer: \(updatedUser)") case .failure(let error): - assertionFailure("Failed to update user: \(error)") + print("Failed to update user: \(error)") } } @@ -44,7 +44,7 @@ do { try User.logout() print("Successfully logged out") } catch let error { - assertionFailure("Error logging out: \(error)") + print("Error logging out: \(error)") } /*: Login - asynchronously - Performs work on background @@ -64,7 +64,7 @@ User.login(username: "hello", password: "world") { results in print("Successfully logged in as user: \(user)") case .failure(let error): - assertionFailure("Error logging in: \(error)") + print("Error logging in: \(error)") } } @@ -73,7 +73,23 @@ do { try User.logout() print("Successfully logged out") } catch let error { - assertionFailure("Error logging out: \(error)") + print("Error logging out: \(error)") +} + +//: Password Reset Request - synchronously +do { + try User.verificationEmailRequest(email: "hello@parse.org") + print("Successfully requested verification email be sent") +} catch let error { + print("Error requesting verification email be sent: \(error)") +} + +//: Password Reset Request - synchronously +do { + try User.passwordReset(email: "hello@parse.org") + print("Successfully requested password reset") +} catch let error { + print("Error requesting password reset: \(error)") } //: Another way to sign up @@ -88,7 +104,7 @@ newUser.signup { result in print("Parse signup successful: \(user)") case .failure(let error): - assertionFailure("Error logging in: \(error)") + print("Error logging in: \(error)") } } diff --git a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift index 8a4cde3a3..9570d10a6 100644 --- a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift @@ -90,7 +90,7 @@ score.save { result in } } -/*: Files can also be saved from data. Below is how to do it synchrously, but async is similar to above +/*: Files can also be saved from data. Below is how to do it synchronously, but async is similar to above Create a new ParseFile for your data */ let sampleData = "Hello World".data(using: .utf8)! @@ -116,7 +116,7 @@ do { print("The file is saved to your Parse Server at: \(url)") print("The full details of your data file are: \(myData)") - //: If you need to download your profilePicture + //: If you need to download your file let fetchedFile = try myData.fetch() if fetchedFile.localURL != nil { print("The file is now saved at: \(fetchedFile.localURL!)") diff --git a/ParseSwift.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground index dba126537..3db5401e4 100644 --- a/ParseSwift.playground/contents.xcplayground +++ b/ParseSwift.playground/contents.xcplayground @@ -10,5 +10,6 @@ + \ No newline at end of file diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 02b0435ad..42a75f82e 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 4AB8B5051F254AE10070F682 /* Parse.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AB8B4F71F254AE10070F682 /* Parse.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4AFDA72A1F26DAE1002AE4FC /* Parse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A82B7EE1F254B820063D731 /* Parse.swift */; }; 4AFDA7391F26DAF8002AE4FC /* Parse.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AB8B4F71F254AE10070F682 /* Parse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7003957625A0EE770052CB31 /* BatchUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */; }; + 7003957725A0EE770052CB31 /* BatchUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */; }; + 7003957825A0EE770052CB31 /* BatchUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */; }; 70110D52250680140091CC1D /* ParseConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D51250680140091CC1D /* ParseConstants.swift */; }; 70110D53250680140091CC1D /* ParseConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D51250680140091CC1D /* ParseConstants.swift */; }; 70110D54250680140091CC1D /* ParseConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D51250680140091CC1D /* ParseConstants.swift */; }; @@ -52,6 +55,14 @@ 705A9A3025991C1400B3547F /* Fileable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A9A2E25991C1400B3547F /* Fileable.swift */; }; 705A9A3125991C1400B3547F /* Fileable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A9A2E25991C1400B3547F /* Fileable.swift */; }; 705A9A3225991C1400B3547F /* Fileable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A9A2E25991C1400B3547F /* Fileable.swift */; }; + 70647E8E259E3375004C1004 /* LocallyIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */; }; + 70647E8F259E3375004C1004 /* LocallyIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */; }; + 70647E90259E3375004C1004 /* LocallyIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */; }; + 70647E91259E3375004C1004 /* LocallyIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */; }; + 70647E9C259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; + 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; + 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; + 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; 708D035225215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; 708D035325215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; 708D035425215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; @@ -114,6 +125,13 @@ 912C9BDC24D3011F009947C3 /* ParseSwift_tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 912C9BDA24D3011F009947C3 /* ParseSwift_tvOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 912C9BE024D302B0009947C3 /* Parse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A82B7EE1F254B820063D731 /* Parse.swift */; }; 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A82B7EE1F254B820063D731 /* Parse.swift */; }; + 916786E2259B7DDA00BB5B4E /* ParseCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */; }; + 916786E3259B7DDA00BB5B4E /* ParseCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */; }; + 916786E4259B7DDA00BB5B4E /* ParseCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */; }; + 916786E5259B7DDA00BB5B4E /* ParseCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */; }; + 91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; + 91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; + 9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; 9194657824F16E330070296B /* ACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ACLTests.swift */; }; F971F4F624DE381A006CB79B /* ParseEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */; }; F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; @@ -310,6 +328,7 @@ 4ACFC2E21F3CA21F0046F3A3 /* ParseSwift.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = ParseSwift.playground; sourceTree = ""; }; 4AFDA7121F26D9A5002AE4FC /* ParseSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ParseSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4AFDA7151F26D9A5002AE4FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchUtilsTests.swift; sourceTree = ""; }; 70110D51250680140091CC1D /* ParseConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConstants.swift; sourceTree = ""; }; 70110D562506CE890091CC1D /* BaseParseInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseParseInstallation.swift; sourceTree = ""; }; 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationTests.swift; sourceTree = ""; }; @@ -326,6 +345,8 @@ 705727882593FF8000F0ADD5 /* ParseFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileTests.swift; sourceTree = ""; }; 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileManagerTests.swift; sourceTree = ""; }; 705A9A2E25991C1400B3547F /* Fileable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fileable.swift; sourceTree = ""; }; + 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocallyIdentifiable.swift; sourceTree = ""; }; + 70647E9B259E3A9A004C1004 /* ParseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseType.swift; sourceTree = ""; }; 708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = ""; }; 709B98302556EC7400507778 /* ParseSwiftTeststvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ParseSwiftTeststvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 709B98342556EC7400507778 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -353,6 +374,8 @@ 912C9BDA24D3011F009947C3 /* ParseSwift_tvOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ParseSwift_tvOS.h; sourceTree = ""; }; 912C9BDB24D3011F009947C3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9158916A256A07DD0024BE9A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloud.swift; sourceTree = ""; }; + 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudTests.swift; sourceTree = ""; }; 9194657724F16E330070296B /* ACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACLTests.swift; sourceTree = ""; }; F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseEncoderTests.swift; sourceTree = ""; }; F97B45B424D9C6F200F4A88B /* ParseCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseCoding.swift; sourceTree = ""; }; @@ -503,18 +526,20 @@ children = ( 4AA8076D1F794C1C008CD551 /* Info.plist */, 9194657724F16E330070296B /* ACLTests.swift */, - 705726ED2592C91C00F0ADD5 /* HashTests.swift */, 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */, + 705726ED2592C91C00F0ADD5 /* HashTests.swift */, 4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */, + 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */, + 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */, F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */, + 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */, + 705727882593FF8000F0ADD5 /* ParseFileTests.swift */, 70BC0B32251903D1001556DB /* ParseGeoPointTests.swift */, 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, 70CE1D882545BF730018D572 /* ParsePointerTests.swift */, 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */, - 705727882593FF8000F0ADD5 /* ParseFileTests.swift */, - 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, 911DB12A24C3F7260027F3C7 /* NetworkMocking */, @@ -563,9 +588,11 @@ children = ( F97B45C924D9C6F200F4A88B /* API */, F97B45B324D9C6F200F4A88B /* Coding */, + 70110D5D250849B30091CC1D /* Internal */, F97B463F24D9C78B00F4A88B /* Mutation Operations */, - F97B45C324D9C6F200F4A88B /* Object Protocols */, - F97B45BA24D9C6F200F4A88B /* Parse Types */, + F97B45C324D9C6F200F4A88B /* Objects */, + 70110D5E25084AF80091CC1D /* Protocols */, + F97B45BA24D9C6F200F4A88B /* Types */, F97B45CB24D9C6F200F4A88B /* Storage */, 4A82B7EE1F254B820063D731 /* Parse.swift */, 70110D51250680140091CC1D /* ParseConstants.swift */, @@ -598,10 +625,12 @@ children = ( 708D035125215F9B00646C70 /* Deletable.swift */, F97B45C524D9C6F200F4A88B /* Fetchable.swift */, + 705A9A2E25991C1400B3547F /* Fileable.swift */, 70BC988F252A5B5C00FF3074 /* Objectable.swift */, F97B45C824D9C6F200F4A88B /* Queryable.swift */, F97B45C724D9C6F200F4A88B /* Savable.swift */, - 705A9A2E25991C1400B3547F /* Fileable.swift */, + 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */, + 70647E9B259E3A9A004C1004 /* ParseType.swift */, ); path = Protocols; sourceTree = ""; @@ -693,29 +722,28 @@ path = Coding; sourceTree = ""; }; - F97B45BA24D9C6F200F4A88B /* Parse Types */ = { + F97B45BA24D9C6F200F4A88B /* Types */ = { isa = PBXGroup; children = ( F97B45C024D9C6F200F4A88B /* ParseACL.swift */, + 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */, F97B45BF24D9C6F200F4A88B /* ParseError.swift */, F97B45C124D9C6F200F4A88B /* ParseFile.swift */, F97B45BC24D9C6F200F4A88B /* ParseGeoPoint.swift */, F97B45BE24D9C6F200F4A88B /* Pointer.swift */, F97B45BB24D9C6F200F4A88B /* Query.swift */, - 70110D5D250849B30091CC1D /* Internal */, ); - path = "Parse Types"; + path = Types; sourceTree = ""; }; - F97B45C324D9C6F200F4A88B /* Object Protocols */ = { + F97B45C324D9C6F200F4A88B /* Objects */ = { isa = PBXGroup; children = ( 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */, F97B45C624D9C6F200F4A88B /* ParseObject.swift */, F97B45C424D9C6F200F4A88B /* ParseUser.swift */, - 70110D5E25084AF80091CC1D /* Protocols */, ); - path = "Object Protocols"; + path = Objects; sourceTree = ""; }; F97B45C924D9C6F200F4A88B /* API */ = { @@ -1152,6 +1180,7 @@ buildActionMask = 2147483647; files = ( F97B463724D9C74400F4A88B /* Responses.swift in Sources */, + 916786E2259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B461624D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DA24D9C6F200F4A88B /* Extensions.swift in Sources */, F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */, @@ -1166,6 +1195,7 @@ F97B464624D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */, 705A9A2F25991C1400B3547F /* Fileable.swift in Sources */, F97B464A24D9C78B00F4A88B /* DeleteOperation.swift in Sources */, + 70647E8E259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460624D9C6F200F4A88B /* ParseUser.swift in Sources */, F97B465A24D9C78C00F4A88B /* IncrementOperation.swift in Sources */, F97B45E224D9C6F200F4A88B /* AnyEncodable.swift in Sources */, @@ -1192,6 +1222,7 @@ F97B45FA24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70BDA2B3250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462724D9C72700F4A88B /* API.swift in Sources */, + 70647E9C259E3A9A004C1004 /* ParseType.swift in Sources */, 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DE24D9C6F200F4A88B /* AnyCodable.swift in Sources */, ); @@ -1208,7 +1239,9 @@ 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */, 705727B12593FF8800F0ADD5 /* ParseFileTests.swift in Sources */, 70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */, + 7003957625A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99F9259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */, 7FFF552E2217E72A007C3B4E /* AnyEncodableTests.swift in Sources */, 7FFF55302217E72A007C3B4E /* AnyDecodableTests.swift in Sources */, 70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */, @@ -1228,6 +1261,7 @@ buildActionMask = 2147483647; files = ( F97B463824D9C74400F4A88B /* Responses.swift in Sources */, + 916786E3259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B461724D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DB24D9C6F200F4A88B /* Extensions.swift in Sources */, F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */, @@ -1242,6 +1276,7 @@ F97B464724D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */, 705A9A3025991C1400B3547F /* Fileable.swift in Sources */, F97B464B24D9C78B00F4A88B /* DeleteOperation.swift in Sources */, + 70647E8F259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460724D9C6F200F4A88B /* ParseUser.swift in Sources */, F97B465B24D9C78C00F4A88B /* IncrementOperation.swift in Sources */, F97B45E324D9C6F200F4A88B /* AnyEncodable.swift in Sources */, @@ -1268,6 +1303,7 @@ F97B45FB24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70BDA2B4250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462824D9C72700F4A88B /* API.swift in Sources */, + 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */, 70110D582506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DF24D9C6F200F4A88B /* AnyCodable.swift in Sources */, ); @@ -1293,7 +1329,9 @@ 709B98572556ECAA00507778 /* ACLTests.swift in Sources */, 705727BC2593FF8C00F0ADD5 /* ParseFileTests.swift in Sources */, 709B984F2556ECAA00507778 /* AnyCodableTests.swift in Sources */, + 7003957825A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FB259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, 709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */, 709B98522556ECAA00507778 /* ParseUserTests.swift in Sources */, 709B984E2556ECAA00507778 /* ParseGeoPointTests.swift in Sources */, @@ -1319,7 +1357,9 @@ 70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */, 705727BB2593FF8B00F0ADD5 /* ParseFileTests.swift in Sources */, 70F2E2BD254F283000B2EA5C /* AnyDecodableTests.swift in Sources */, + 7003957725A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FA259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, 70F2E2C1254F283000B2EA5C /* AnyCodableTests.swift in Sources */, 70F2E2B3254F283000B2EA5C /* ParseUserTests.swift in Sources */, 70F2E2C0254F283000B2EA5C /* MockURLResponse.swift in Sources */, @@ -1339,6 +1379,7 @@ buildActionMask = 2147483647; files = ( F97B45D524D9C6F200F4A88B /* AnyDecodable.swift in Sources */, + 916786E5259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B45E924D9C6F200F4A88B /* Query.swift in Sources */, F97B463624D9C74400F4A88B /* URLSession+extensions.swift in Sources */, F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */, @@ -1353,6 +1394,7 @@ F97B45ED24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, 705A9A3225991C1400B3547F /* Fileable.swift in Sources */, F97B45F524D9C6F200F4A88B /* Pointer.swift in Sources */, + 70647E91259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460924D9C6F200F4A88B /* ParseUser.swift in Sources */, F97B463A24D9C74400F4A88B /* Responses.swift in Sources */, F97B45DD24D9C6F200F4A88B /* Extensions.swift in Sources */, @@ -1379,6 +1421,7 @@ F97B463224D9C74400F4A88B /* BatchUtils.swift in Sources */, F97B45F124D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D924D9C6F200F4A88B /* ParseEncoder.swift in Sources */, + 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */, 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */, F97B461924D9C6F200F4A88B /* Queryable.swift in Sources */, ); @@ -1389,6 +1432,7 @@ buildActionMask = 2147483647; files = ( F97B45D424D9C6F200F4A88B /* AnyDecodable.swift in Sources */, + 916786E4259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B45E824D9C6F200F4A88B /* Query.swift in Sources */, F97B463524D9C74400F4A88B /* URLSession+extensions.swift in Sources */, F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */, @@ -1403,6 +1447,7 @@ F97B45EC24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, 705A9A3125991C1400B3547F /* Fileable.swift in Sources */, F97B45F424D9C6F200F4A88B /* Pointer.swift in Sources */, + 70647E90259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460824D9C6F200F4A88B /* ParseUser.swift in Sources */, F97B463924D9C74400F4A88B /* Responses.swift in Sources */, F97B45DC24D9C6F200F4A88B /* Extensions.swift in Sources */, @@ -1429,6 +1474,7 @@ F97B463124D9C74400F4A88B /* BatchUtils.swift in Sources */, F97B45F024D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D824D9C6F200F4A88B /* ParseEncoder.swift in Sources */, + 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */, 912C9BE024D302B0009947C3 /* Parse.swift in Sources */, F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */, ); diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index f88e97250..5da5f3e84 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -11,9 +11,10 @@ import Foundation import FoundationNetworking #endif +// MARK: API.Command internal extension API { // swiftlint:disable:next type_body_length - struct Command: Encodable where T: Encodable { + struct Command: Encodable where T: ParseType { typealias ReturnType = U // swiftlint:disable:this nesting let method: API.Method let path: API.Endpoint @@ -50,7 +51,7 @@ internal extension API { // MARK: Synchronous Execution func executeStream(options: API.Options, - childObjects: [NSDictionary: PointerType]? = nil, + childObjects: [String: PointerType]? = nil, childFiles: [UUID: ParseFile]? = nil, uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil, stream: InputStream) throws { @@ -77,7 +78,7 @@ internal extension API { } func execute(options: API.Options, - childObjects: [NSDictionary: PointerType]? = nil, + childObjects: [String: PointerType]? = nil, childFiles: [UUID: ParseFile]? = nil, uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil, downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil) throws -> U { @@ -105,7 +106,7 @@ internal extension API { // MARK: Asynchronous Execution // swiftlint:disable:next function_body_length cyclomatic_complexity func executeAsync(options: API.Options, callbackQueue: DispatchQueue?, - childObjects: [NSDictionary: PointerType]? = nil, + childObjects: [String: PointerType]? = nil, childFiles: [UUID: ParseFile]? = nil, uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil, downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil, @@ -249,7 +250,7 @@ internal extension API { // MARK: URL Preperation func prepareURLRequest(options: API.Options, - childObjects: [NSDictionary: PointerType]? = nil, + childObjects: [String: PointerType]? = nil, childFiles: [UUID: ParseFile]? = nil) -> Result { let params = self.params?.getQueryItems() let headers = API.getHeaders(options: options) @@ -270,23 +271,22 @@ internal extension API { var urlRequest = URLRequest(url: urlComponents) urlRequest.allHTTPHeaderFields = headers if let urlBody = body { - if let childObjects = childObjects { - guard let bodyData = try? ParseCoding - .parseEncoder() - .encode(urlBody, collectChildren: false, - objectsSavedBeforeThisOne: childObjects, filesSavedBeforeThisOne: childFiles) else { - return .failure(ParseError(code: .unknownError, + if (urlBody as? ParseCloud) != nil { + guard let bodyData = try? ParseCoding.parseEncoder().encode(urlBody, skipKeys: .cloud) else { + return .failure(ParseError(code: .unknownError, message: "couldn't encode body \(urlBody)")) } - urlRequest.httpBody = bodyData.encoded + urlRequest.httpBody = bodyData } else { guard let bodyData = try? ParseCoding .parseEncoder() - .encode(urlBody) else { + .encode(urlBody, collectChildren: false, + objectsSavedBeforeThisOne: childObjects, + filesSavedBeforeThisOne: childFiles) else { return .failure(ParseError(code: .unknownError, message: "couldn't encode body \(urlBody)")) } - urlRequest.httpBody = bodyData + urlRequest.httpBody = bodyData.encoded } } urlRequest.httpMethod = method.rawValue @@ -347,7 +347,6 @@ internal extension API.Command { try FileManager.default.moveItem(at: tempFileLocation, to: fileLocation) var object = object object.localURL = fileLocation - _ = object.localUUID //Ensure downloaded file has a localUUID return object } } @@ -381,7 +380,7 @@ internal extension API.Command { } private static func updateCommand(_ object: T) -> API.Command where T: ParseObject { - let mapper = { (data: Data) -> T in + let mapper = { (data) -> T in try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: object) } return API.Command(method: .PUT, @@ -446,20 +445,6 @@ internal extension API.Command { try ParseCoding.jsonDecoder().decode(T.self, from: data) } } - - // MARK: Deleting - static func deleteCommand(_ object: T) throws -> API.Command where T: ParseObject { - guard object.isSaved else { - throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") - } - - return API.Command( - method: .DELETE, - path: object.endpoint - ) { (data) -> ParseError? in - try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) - } - } } // MARK: Batch - Saving, Fetching @@ -467,7 +452,7 @@ extension API.Command where T: ParseObject { internal var data: Data? { guard let body = body else { return nil } - return try? body.getEncoder().encode(body) + return try? body.getEncoder().encode(body, skipKeys: .object) } static func batch(commands: [API.Command]) -> RESTBatchCommandType { @@ -517,10 +502,11 @@ extension API.Command where T: ParseObject { } // MARK: Batch - Deleting - static func batch(commands: [API.Command]) -> RESTBatchCommandNoBodyType { - let commands = commands.compactMap { (command) -> API.Command? in + // swiftlint:disable:next line_length + static func batch(commands: [API.NonParseBodyCommand]) -> RESTBatchCommandNoBodyType { + let commands = commands.compactMap { (command) -> API.NonParseBodyCommand? in let path = ParseConfiguration.mountPath + command.path.urlComponent - return API.Command( + return API.NonParseBodyCommand( method: command.method, path: .any(path), mapper: command.mapper) } @@ -546,17 +532,20 @@ extension API.Command where T: ParseObject { } } - let batchCommand = BatchCommand(requests: commands) + let batchCommand = BatchCommandNoBody(requests: commands) return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } } +//This has been disabled, looking into getting it working in the future. +//It's only needed for sending batches of childObjects which currently isn't being used. +/* // MARK: Batch - Child Objects -extension API.Command where T: Encodable { +extension API.Command where T: ParseType { internal var data: Data? { guard let body = body else { return nil } - return try? ParseCoding.parseEncoder().encode(body) + return try? ParseCoding.jsonEncoder().encode(body) } static func batch(commands: [API.Command]) -> RESTBatchCommandTypeEncodable { @@ -603,4 +592,127 @@ extension API.Command where T: Encodable { let batchCommand = BatchCommand(requests: commands) return RESTBatchCommandTypeEncodable(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } +}*/ + +// MARK: API.NonParseBodyCommand +internal extension API { + struct NonParseBodyCommand: Encodable where T: Encodable { + typealias ReturnType = U // swiftlint:disable:this nesting + let method: API.Method + let path: API.Endpoint + let body: T? + let mapper: ((Data) throws -> U) + let params: [String: String?]? + + init(method: API.Method, + path: API.Endpoint, + params: [String: String]? = nil, + body: T? = nil, + mapper: @escaping ((Data) throws -> U)) { + self.method = method + self.path = path + self.body = body + self.mapper = mapper + self.params = params + } + + func execute(options: API.Options) throws -> U { + var responseResult: Result? + let group = DispatchGroup() + group.enter() + self.executeAsync(options: options, + callbackQueue: nil) { result in + responseResult = result + group.leave() + } + group.wait() + + guard let response = responseResult else { + throw ParseError(code: .unknownError, + message: "couldn't unrwrap server response") + } + return try response.get() + } + + // MARK: Asynchronous Execution + func executeAsync(options: API.Options, callbackQueue: DispatchQueue?, + completion: @escaping(Result) -> Void) { + + switch self.prepareURLRequest(options: options) { + case .success(let urlRequest): + URLSession.shared.dataTask(with: urlRequest, mapper: mapper) { result in + switch result { + + case .success(let decoded): + if let callbackQueue = callbackQueue { + callbackQueue.async { completion(.success(decoded)) } + } else { + completion(.success(decoded)) + } + + case .failure(let error): + if let callbackQueue = callbackQueue { + callbackQueue.async { completion(.failure(error)) } + } else { + completion(.failure(error)) + } + } + } + case .failure(let error): + completion(.failure(error)) + } + } + + // MARK: URL Preperation + func prepareURLRequest(options: API.Options) -> Result { + let params = self.params?.getQueryItems() + let headers = API.getHeaders(options: options) + let url = ParseConfiguration.serverURL.appendingPathComponent(path.urlComponent) + + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return .failure(ParseError(code: .unknownError, + message: "couldn't unrwrap url components for \(url)")) + } + components.queryItems = params + + guard let urlComponents = components.url else { + return .failure(ParseError(code: .unknownError, + message: "couldn't create url from components for \(components)")) + } + + var urlRequest = URLRequest(url: urlComponents) + urlRequest.allHTTPHeaderFields = headers + if let urlBody = body { + guard let bodyData = try? ParseCoding.jsonEncoder().encode(urlBody) else { + return .failure(ParseError(code: .unknownError, + message: "couldn't encode body \(urlBody)")) + } + urlRequest.httpBody = bodyData + } + urlRequest.httpMethod = method.rawValue + + return .success(urlRequest) + } + + enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting + case method, body, path + } + } +} + +internal extension API.NonParseBodyCommand { + // MARK: Deleting + // swiftlint:disable:next line_length + static func deleteCommand(_ object: T) throws -> API.NonParseBodyCommand where T: ParseObject { + guard object.isSaved else { + throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") + } + + return API.NonParseBodyCommand( + method: .DELETE, + path: object.endpoint + ) { (data) -> ParseError? in + try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + } + } } // swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index 618429a02..8b61110ef 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -18,10 +18,17 @@ public struct API { case batch case objects(className: String) case object(className: String, objectId: String) + case users + case user(objectId: String) + case installations + case installation(objectId: String) case login - case signup case logout case file(fileName: String) + case passwordReset + case verificationEmailRequest + case functions(name: String) + case jobs(name: String) case any(String) var urlComponent: String { @@ -32,14 +39,28 @@ public struct API { return "/classes/\(className)" case .object(let className, let objectId): return "/classes/\(className)/\(objectId)" + case .users: + return "/users" + case .user(let objectId): + return "/users/\(objectId)" + case .installations: + return "/installations" + case .installation(let objectId): + return "/installations/\(objectId)" case .login: return "/login" - case .signup: - return "/users" case .logout: - return "/users/logout" + return "/logout" case .file(let fileName): return "/files/\(fileName)" + case .passwordReset: + return "/requestPasswordReset" + case .verificationEmailRequest: + return "/verificationEmailRequest" + case .functions(name: let name): + return "/functions/\(name)" + case .jobs(name: let name): + return "/jobs/\(name)" case .any(let path): return path } diff --git a/Sources/ParseSwift/API/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift index 1847caa51..7e6dd92bd 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -13,17 +13,38 @@ typealias ParseObjectBatchResponse = [(Result)] // swiftlint:disable line_length typealias RESTBatchCommandType = API.Command, ParseObjectBatchResponse> where T: ParseObject -typealias ParseObjectBatchCommandNoBody = BatchCommand +typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody typealias ParseObjectBatchResponseNoBody = [ParseError?] -typealias RESTBatchCommandNoBodyType = API.Command, ParseObjectBatchResponseNoBody> where T: Codable - -typealias ParseObjectBatchCommandEncodable = BatchCommand where T: Encodable +typealias RESTBatchCommandNoBodyType = API.NonParseBodyCommand, ParseObjectBatchResponseNoBody> where T: Encodable +/* +typealias ParseObjectBatchCommandEncodable = BatchCommand where T: ParseType typealias ParseObjectBatchResponseEncodable = [(Result)] // swiftlint:disable line_length -typealias RESTBatchCommandTypeEncodable = API.Command, ParseObjectBatchResponseEncodable> where T: Encodable +typealias RESTBatchCommandTypeEncodable = API.Command, ParseObjectBatchResponseEncodable> where T: ParseType + // swiftlint:enable line_length +*/ +internal struct BatchCommand: ParseType where T: ParseType { + let requests: [API.Command] +} -// swiftlint:enable line_length +internal struct BatchCommandNoBody: Encodable where T: Encodable { + let requests: [API.NonParseBodyCommand] +} -internal struct BatchCommand: Encodable where T: Encodable { - let requests: [API.Command] +struct BatchUtils { + static func splitArray(_ array: [U], valuesPerSegment: Int) -> [[U]] { + if array.count < valuesPerSegment { + return [array] + } + + var returnArray = [[U]]() + var index = 0 + while index < array.count { + let length = Swift.min(array.count - index, valuesPerSegment) + let subArray = Array(array[index.. U) -> Result { if let responseError = responseError { guard let parseError = responseError as? ParseError else { - return .failure(ParseError(code: .unknownError, + return .failure(ParseError(code: .invalidServerResponse, message: "Unable to sync with parse-server: \(responseError)")) } return .failure(parseError) - } else if let responseData = responseData { + } + + if let responseData = responseData { do { return try .success(mapper(responseData)) } catch { - let parseError = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: responseData) - return .failure(parseError ?? ParseError(code: .unknownError, - // swiftlint:disable:next line_length - message: "Error decoding parse-server response: \(error.localizedDescription)")) + if let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: responseData) { + return .failure(error) + } + guard let parseError = error as? ParseError else { + return .failure(ParseError(code: .unknownError, + // swiftlint:disable:next line_length + message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription)")) + } + return .failure(parseError) } - } else { - return .failure(ParseError(code: .unknownError, - // swiftlint:disable:next line_length - message: "Unable to sync with parse-server: \(String(describing: urlResponse)).")) } + + return .failure(ParseError(code: .unknownError, + message: "Unable to sync with parse-server: \(String(describing: urlResponse)).")) } internal func makeResult(location: URL?, @@ -105,24 +111,28 @@ extension URLSession { mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { guard let parseError = responseError as? ParseError else { - return .failure(ParseError(code: .unknownError, + return .failure(ParseError(code: .invalidServerResponse, message: "Unable to sync with parse-server: \(responseError)")) } return .failure(parseError) - } else if let location = location { + } + + if let location = location { do { let data = try ParseCoding.jsonEncoder().encode(location) return try .success(mapper(data)) } catch { - return .failure(ParseError(code: .unknownError, - // swiftlint:disable:next line_length - message: "Error decoding parse-server response: \(error.localizedDescription)")) + guard let parseError = error as? ParseError else { + return .failure(ParseError(code: .unknownError, + // swiftlint:disable:next line_length + message: "Error decoding parse-server response: \(String(describing: urlResponse)) with error: \(error.localizedDescription)")) + } + return .failure(parseError) } - } else { - return .failure(ParseError(code: .unknownError, - // swiftlint:disable:next line_length - message: "Unable to sync with parse-server: \(String(describing: urlResponse)).")) } + + return .failure(ParseError(code: .unknownError, + message: "Unable to sync with parse-server: \(String(describing: urlResponse)).")) } internal func dataTask( diff --git a/Sources/ParseSwift/Coding/Extensions.swift b/Sources/ParseSwift/Coding/Extensions.swift index 14425606d..0dd4f4ac7 100644 --- a/Sources/ParseSwift/Coding/Extensions.swift +++ b/Sources/ParseSwift/Coding/Extensions.swift @@ -31,8 +31,8 @@ extension JSONEncoder { // MARK: ParseObject public extension ParseObject { - func getEncoder(skipKeys: Bool = true) -> ParseEncoder { - return ParseCoding.parseEncoder(skipKeys: skipKeys) + func getEncoder() -> ParseEncoder { + return ParseCoding.parseEncoder() } func getJSONEncoder() -> JSONEncoder { diff --git a/Sources/ParseSwift/Coding/ParseCoding.swift b/Sources/ParseSwift/Coding/ParseCoding.swift index 9be1c2085..70289d819 100644 --- a/Sources/ParseSwift/Coding/ParseCoding.swift +++ b/Sources/ParseSwift/Coding/ParseCoding.swift @@ -13,7 +13,6 @@ public enum ParseCoding {} // MARK: Coders extension ParseCoding { - private static let forbiddenKeys = Set(["createdAt", "updatedAt", "objectId", "className"]) ///This should only be used for Unit tests, don't use in SDK static func jsonEncoder() -> JSONEncoder { @@ -28,10 +27,9 @@ extension ParseCoding { return decoder } - static func parseEncoder(skipKeys: Bool = true) -> ParseEncoder { + static func parseEncoder() -> ParseEncoder { ParseEncoder( - dateEncodingStrategy: parseDateEncodingStrategy, - skippingKeys: skipKeys ? forbiddenKeys : [] + dateEncodingStrategy: parseDateEncodingStrategy ) } } diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index 4b6ee2546..04733a5a5 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -51,39 +51,66 @@ extension Dictionary: _JSONStringDictionaryEncodableMarker where Key == String, // MARK: ParseEncoder public struct ParseEncoder { let dateEncodingStrategy: AnyCodable.DateEncodingStrategy? - let jsonEncoder: JSONEncoder - let skippedKeys: Set + + public enum SkippedKeys { + case object + case cloud + case none + case custom(Set) + + func keys() -> Set { + switch self { + + case .object: + return Set(["createdAt", "updatedAt", "objectId", "className"]) + case .cloud: + return Set(["functionJobName"]) + case .none: + return .init() + case .custom(let keys): + return keys + } + } + } init( - dateEncodingStrategy: AnyCodable.DateEncodingStrategy? = nil, - jsonEncoder: JSONEncoder = JSONEncoder(), - skippingKeys: Set = [] + dateEncodingStrategy: AnyCodable.DateEncodingStrategy? = nil ) { self.dateEncodingStrategy = dateEncodingStrategy - self.jsonEncoder = jsonEncoder - self.skippedKeys = skippingKeys + } + + func encode(_ value: Encodable) throws -> Data { + let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: SkippedKeys.none.keys()) if let dateEncodingStrategy = dateEncodingStrategy { - self.jsonEncoder.dateEncodingStrategy = .custom(dateEncodingStrategy) + encoder.dateEncodingStrategy = .custom(dateEncodingStrategy) } + return try encoder.encodeObject(value, collectChildren: false, objectsSavedBeforeThisOne: nil, filesSavedBeforeThisOne: nil).encoded } - public func encode(_ value: T) throws -> Data { - try jsonEncoder.encode(value) + public func encode(_ value: T, skipKeys: SkippedKeys) throws -> Data { + let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: skipKeys.keys()) + if let dateEncodingStrategy = dateEncodingStrategy { + encoder.dateEncodingStrategy = .custom(dateEncodingStrategy) + } + return try encoder.encodeObject(value, collectChildren: false, objectsSavedBeforeThisOne: nil, filesSavedBeforeThisOne: nil).encoded } - public func encode(_ value: T) throws -> Data { - let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: skippedKeys) + // swiftlint:disable large_tuple + internal func encode(_ value: T, + objectsSavedBeforeThisOne: [String: PointerType]?, + filesSavedBeforeThisOne: [UUID: ParseFile]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) { + let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: SkippedKeys.object.keys()) if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = .custom(dateEncodingStrategy) } - return try encoder.encodeObject(value, collectChildren: false, objectsSavedBeforeThisOne: nil, filesSavedBeforeThisOne: nil).encoded + return try encoder.encodeObject(value, collectChildren: true, objectsSavedBeforeThisOne: objectsSavedBeforeThisOne, filesSavedBeforeThisOne: filesSavedBeforeThisOne) } // swiftlint:disable large_tuple - internal func encode(_ value: Encodable, collectChildren: Bool, - objectsSavedBeforeThisOne: [NSDictionary: PointerType]?, + internal func encode(_ value: ParseType, collectChildren: Bool, + objectsSavedBeforeThisOne: [String: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) { - let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: skippedKeys) + let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: SkippedKeys.object.keys()) if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = .custom(dateEncodingStrategy) } @@ -100,7 +127,7 @@ private class _ParseEncoder: JSONEncoder, Encoder { var uniqueFiles = Set() var newObjects = [Encodable]() var collectChildren = false - var objectsSavedBeforeThisOne: [NSDictionary: PointerType]? + var objectsSavedBeforeThisOne: [String: PointerType]? var filesSavedBeforeThisOne: [UUID: ParseFile]? /// The encoder's storage. var storage: _ParseEncodingStorage @@ -150,9 +177,8 @@ private class _ParseEncoder: JSONEncoder, Encoder { throw ParseError(code: .unknownError, message: "This method shouldn't be used. Either use the JSONEncoder or if you are encoding a ParseObject use \"encodeObject\"") } - // swiftlint:disable large_tuple func encodeObject(_ value: Encodable, collectChildren: Bool, - objectsSavedBeforeThisOne: [NSDictionary: PointerType]?, + objectsSavedBeforeThisOne: [String: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) { let encoder = _ParseEncoder(codingPath: codingPath, dictionary: dictionary, skippingKeys: skippedKeys) @@ -240,7 +266,7 @@ private class _ParseEncoder: JSONEncoder, Encoder { valueToEncode = value.toPointer() } } else { - let hashOfCurrentObject = BaseObjectable.createHash(value) + let hashOfCurrentObject = try BaseObjectable.createHash(value) if self.collectChildren { if let pointerForCurrentObject = self.objectsSavedBeforeThisOne?[hashOfCurrentObject] { valueToEncode = pointerForCurrentObject @@ -250,7 +276,7 @@ private class _ParseEncoder: JSONEncoder, Encoder { } } else if let pointerForCurrentObject = self.objectsSavedBeforeThisOne?[hashOfCurrentObject] { valueToEncode = pointerForCurrentObject - } else if codingPath.count > 0 { + } else if dictionary.count > 0 { //Only top level objects can be saved without a pointer throw ParseError(code: .unknownError, message: "Error. Couldn't resolve unsaved object while encoding.") } @@ -269,18 +295,16 @@ private class _ParseEncoder: JSONEncoder, Encoder { valueToEncode = value } } else { - var mutableValue = value - let uuid = mutableValue.localUUID if self.collectChildren { - if let updatedFile = self.filesSavedBeforeThisOne?[uuid] { + if let updatedFile = self.filesSavedBeforeThisOne?[value.localId] { valueToEncode = updatedFile } else { //New object needs to be saved before it can be stored self.newObjects.append(value) } - } else if let currentFile = self.filesSavedBeforeThisOne?[uuid] { + } else if let currentFile = self.filesSavedBeforeThisOne?[value.localId] { valueToEncode = currentFile - } else if codingPath.count > 0 { + } else if dictionary.count > 0 { //Only top level objects can be saved without a pointer throw ParseError(code: .unknownError, message: "Error. Couldn't resolve unsaved file while encoding.") } @@ -879,7 +903,7 @@ private class _ParseReferencingEncoder: _ParseEncoder { // MARK: - Initialization /// Initializes `self` by referencing the given array container in the given encoder. - init(referencing encoder: _ParseEncoder, at index: Int, wrapping array: NSMutableArray, skippingKeys: Set, collectChildren: Bool, objectsSavedBeforeThisOne: [NSDictionary: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) { + init(referencing encoder: _ParseEncoder, at index: Int, wrapping array: NSMutableArray, skippingKeys: Set, collectChildren: Bool, objectsSavedBeforeThisOne: [String: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) { self.encoder = encoder self.reference = .array(array, index) super.init(codingPath: encoder.codingPath, dictionary: NSMutableDictionary(), skippingKeys: skippingKeys) @@ -890,7 +914,7 @@ private class _ParseReferencingEncoder: _ParseEncoder { } /// Initializes `self` by referencing the given dictionary container in the given encoder. - init(referencing encoder: _ParseEncoder, key: CodingKey, wrapping dictionary: NSMutableDictionary, skippingKeys: Set, collectChildren: Bool, objectsSavedBeforeThisOne: [NSDictionary: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) { + init(referencing encoder: _ParseEncoder, key: CodingKey, wrapping dictionary: NSMutableDictionary, skippingKeys: Set, collectChildren: Bool, objectsSavedBeforeThisOne: [String: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) { self.encoder = encoder self.reference = .dictionary(dictionary, key.stringValue) super.init(codingPath: encoder.codingPath, dictionary: dictionary, skippingKeys: skippingKeys) diff --git a/Sources/ParseSwift/Parse Types/Internal/BaseParseInstallation.swift b/Sources/ParseSwift/Internal/BaseParseInstallation.swift similarity index 100% rename from Sources/ParseSwift/Parse Types/Internal/BaseParseInstallation.swift rename to Sources/ParseSwift/Internal/BaseParseInstallation.swift diff --git a/Sources/ParseSwift/Parse Types/Internal/BaseParseUser.swift b/Sources/ParseSwift/Internal/BaseParseUser.swift similarity index 100% rename from Sources/ParseSwift/Parse Types/Internal/BaseParseUser.swift rename to Sources/ParseSwift/Internal/BaseParseUser.swift diff --git a/Sources/ParseSwift/Parse Types/Internal/NoBody.swift b/Sources/ParseSwift/Internal/NoBody.swift similarity index 100% rename from Sources/ParseSwift/Parse Types/Internal/NoBody.swift rename to Sources/ParseSwift/Internal/NoBody.swift diff --git a/Sources/ParseSwift/Parse Types/Internal/ParseHash.swift b/Sources/ParseSwift/Internal/ParseHash.swift similarity index 100% rename from Sources/ParseSwift/Parse Types/Internal/ParseHash.swift rename to Sources/ParseSwift/Internal/ParseHash.swift diff --git a/Sources/ParseSwift/Object Protocols/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift similarity index 70% rename from Sources/ParseSwift/Object Protocols/ParseInstallation.swift rename to Sources/ParseSwift/Objects/ParseInstallation.swift index 20fd72c36..f19d0fa7d 100644 --- a/Sources/ParseSwift/Object Protocols/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -84,6 +84,17 @@ public extension ParseInstallation { } } +// MARK: Convenience +extension ParseInstallation { + var endpoint: API.Endpoint { + if let objectId = objectId { + return .installation(objectId: objectId) + } + + return .installations + } +} + // MARK: CurrentInstallationContainer struct CurrentInstallationContainer: Codable { var currentInstallation: T? @@ -314,7 +325,7 @@ extension ParseInstallation { Fetches the `ParseInstallation` *synchronously* with the current data from the server and sets an error if one occurs. - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An Error of `ParseError` type. - important: If an object fetched has the same objectId as current, it will automatically update the current. */ @@ -327,7 +338,7 @@ extension ParseInstallation { /** Fetches the `ParseInstallation` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. @@ -352,6 +363,17 @@ extension ParseInstallation { completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } + + func fetchCommand() throws -> API.Command { + guard isSaved else { + throw ParseError(code: .unknownError, message: "Cannot fetch an object without id") + } + + return API.Command(method: .GET, + path: endpoint) { (data) -> Self in + try ParseCoding.jsonDecoder().decode(Self.self, from: data) + } + } } // MARK: Savable @@ -360,13 +382,13 @@ extension ParseInstallation { /** Saves the `ParseInstallation` *synchronously* and throws an error if there's an issue. - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: A Error of type `ParseError`. - returns: Returns saved `ParseInstallation`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ public func save(options: API.Options = []) throws -> Self { - var childObjects: [NSDictionary: PointerType]? + var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? var error: ParseError? let group = DispatchGroup() @@ -394,7 +416,7 @@ extension ParseInstallation { /** Saves the `ParseInstallation` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. @@ -421,6 +443,34 @@ extension ParseInstallation { completion(.failure(parseError)) } } + + func saveCommand() -> API.Command { + if isSaved { + return updateCommand() + } + return createCommand() + } + + // MARK: Saving ParseObjects - private + private func createCommand() -> API.Command { + let mapper = { (data) -> Self in + try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: self) + } + return API.Command(method: .POST, + path: endpoint, + body: self, + mapper: mapper) + } + + private func updateCommand() -> API.Command { + let mapper = { (data) -> Self in + try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: self) + } + return API.Command(method: .PUT, + path: endpoint, + body: self, + mapper: mapper) + } } // MARK: Deletable @@ -429,7 +479,7 @@ extension ParseInstallation { Deletes the `ParseInstallation` *synchronously* with the current data from the server and sets an error if one occurs. - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An Error of `ParseError` type. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ @@ -441,7 +491,7 @@ extension ParseInstallation { /** Deletes the `ParseInstallation` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. @@ -470,6 +520,19 @@ extension ParseInstallation { completion(ParseError(code: .unknownError, message: error.localizedDescription)) } } + + func deleteCommand() throws -> API.NonParseBodyCommand { + guard isSaved else { + throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") + } + + return API.NonParseBodyCommand( + method: .DELETE, + path: endpoint + ) { (data) -> ParseError? in + try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + } + } } // MARK: Batch Support @@ -477,55 +540,170 @@ public extension Sequence where Element: ParseInstallation { /** Saves a collection of installations *synchronously* all at once and throws an error if necessary. - - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` - important: If an object saved has the same objectId as current, it will automatically update the current. */ - func saveAll(options: API.Options = []) throws -> [(Result)] { + func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length + options: API.Options = []) throws -> [(Result)] { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var childObjects = [String: PointerType]() + var childFiles = [UUID: ParseFile]() + var error: ParseError? + + let installations = map { $0 } + for installation in installations { + let group = DispatchGroup() + group.enter() + installation.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + //If an error occurs, everything should be skipped + if parseError != nil { + error = parseError + } + savedChildObjects.forEach {(key, value) in + if error != nil { + return + } + if childObjects[key] == nil { + childObjects[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + savedChildFiles.forEach {(key, value) in + if error != nil { + return + } + if childFiles[key] == nil { + childFiles[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + group.leave() + } + group.wait() + if let error = error { + throw error + } + } + + var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } - let returnResults = try API.Command - .batch(commands: commands) - .execute(options: options) - try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) - return returnResults + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + try batches.forEach { + let currentBatch = try API.Command + .batch(commands: $0) + .execute(options: options, + childObjects: childObjects, + childFiles: childFiles) + returnBatch.append(contentsOf: currentBatch) + } + try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) + return returnBatch } /** Saves a collection of installations all at once *asynchronously* and executes the completion block when done. - - - parameter options: A set of options used to save installations. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[(Result)], ParseError>)`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ - func saveAll( + func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity + batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var childObjects = [String: PointerType]() + var childFiles = [UUID: ParseFile]() + var error: ParseError? + + let installations = map { $0 } + for installation in installations { + let group = DispatchGroup() + group.enter() + installation.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + //If an error occurs, everything should be skipped + if parseError != nil { + error = parseError + } + savedChildObjects.forEach {(key, value) in + if error != nil { + return + } + if childObjects[key] == nil { + childObjects[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + savedChildFiles.forEach {(key, value) in + if error != nil { + return + } + if childFiles[key] == nil { + childFiles[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + group.leave() + } + group.wait() + if let error = error { + completion(.failure(error)) + return + } + } + + var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } - API.Command - .batch(commands: commands) - .executeAsync(options: options, callbackQueue: callbackQueue) { results in + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch) + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: childObjects, + childFiles: childFiles) { results in switch results { case .success(let saved): - try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0}) - completion(.success(saved)) + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) + completion(.success(returnBatch)) + } + completed += 1 case .failure(let error): completion(.failure(error)) + return } } + } } /** Fetches a collection of installations *synchronously* all at once and throws an error if necessary. - - parameter options: A set of options used to fetch installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a fetch was successful or a `ParseError` if it failed. - throws: `ParseError` @@ -561,7 +739,7 @@ public extension Sequence where Element: ParseInstallation { /** Fetches a collection of installations all at once *asynchronously* and executes the completion block when done. - - parameter options: A set of options used to fetch installations. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[(Result)], ParseError>)`. @@ -607,8 +785,10 @@ public extension Sequence where Element: ParseInstallation { /** Deletes a collection of installations *synchronously* all at once and throws an error if necessary. - - - parameter options: A set of options used to delete installations. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns `nil` if the delete successful or a `ParseError` if it failed. 1. A `ParseError.Code.aggregateError`. This object's "errors" property is an @@ -621,20 +801,29 @@ public extension Sequence where Element: ParseInstallation { - throws: `ParseError` - important: If an object deleted has the same objectId as current, it will automatically update the current. */ - func deleteAll(options: API.Options = []) throws -> [ParseError?] { + func deleteAll(batchLimit limit: Int? = nil, + options: API.Options = []) throws -> [ParseError?] { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var returnBatch = [ParseError?]() let commands = try map { try $0.deleteCommand() } - let returnResults = try API.Command - .batch(commands: commands) - .execute(options: options) + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + try batches.forEach { + let currentBatch = try API.Command + .batch(commands: $0) + .execute(options: options) + returnBatch.append(contentsOf: currentBatch) + } try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) - return returnResults + return returnBatch } /** Deletes a collection of installations all at once *asynchronously* and executes the completion block when done. - - - parameter options: A set of options used to delete installations. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[ParseError?], ParseError>)`. @@ -649,25 +838,37 @@ public extension Sequence where Element: ParseInstallation { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func deleteAll( + batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[ParseError?], ParseError>) -> Void ) { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { + var returnBatch = [ParseError?]() let commands = try map({ try $0.deleteCommand() }) - API.Command - .batch(commands: commands) - .executeAsync(options: options, - callbackQueue: callbackQueue) { results in + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch) + .executeAsync(options: options, + callbackQueue: callbackQueue) { results in switch results { - case .success(let deleted): - try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0}) - completion(.success(deleted)) + case .success(let saved): + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0}) + completion(.success(returnBatch)) + } + completed += 1 case .failure(let error): completion(.failure(error)) + return } } + } } catch { guard let parseError = error as? ParseError else { completion(.failure(ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/Object Protocols/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift similarity index 63% rename from Sources/ParseSwift/Object Protocols/ParseObject.swift rename to Sources/ParseSwift/Objects/ParseObject.swift index 18be7dfd8..834e1ae80 100644 --- a/Sources/ParseSwift/Object Protocols/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -24,7 +24,12 @@ import Foundation and relying on that for `Equatable` and `Hashable`, otherwise it's possible you will get "circular dependency errors" depending on your implementation. */ -public protocol ParseObject: Objectable, Fetchable, Savable, Deletable, Hashable, CustomDebugStringConvertible {} +public protocol ParseObject: Objectable, + Fetchable, + Savable, + Deletable, + Equatable, + CustomDebugStringConvertible {} // MARK: Default Implementations extension ParseObject { @@ -54,42 +59,166 @@ public extension Sequence where Element: ParseObject { /** Saves a collection of objects *synchronously* all at once and throws an error if necessary. - - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` */ - func saveAll(options: API.Options = []) throws -> [(Result)] { + func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length + options: API.Options = []) throws -> [(Result)] { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var childObjects = [String: PointerType]() + var childFiles = [UUID: ParseFile]() + var error: ParseError? + + let objects = map { $0 } + for object in objects { + let group = DispatchGroup() + group.enter() + object.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + //If an error occurs, everything should be skipped + if parseError != nil { + error = parseError + } + savedChildObjects.forEach {(key, value) in + if error != nil { + return + } + if childObjects[key] == nil { + childObjects[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + savedChildFiles.forEach {(key, value) in + if error != nil { + return + } + if childFiles[key] == nil { + childFiles[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + group.leave() + } + group.wait() + if let error = error { + throw error + } + } + + var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } - return try API.Command - .batch(commands: commands) - .execute(options: options) + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + try batches.forEach { + let currentBatch = try API.Command + .batch(commands: $0) + .execute(options: options, + childObjects: childObjects, + childFiles: childFiles) + returnBatch.append(contentsOf: currentBatch) + } + return returnBatch } /** Saves a collection of objects all at once *asynchronously* and executes the completion block when done. - - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[(Result)], ParseError>)`. */ - func saveAll( + func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity + batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var childObjects = [String: PointerType]() + var childFiles = [UUID: ParseFile]() + var error: ParseError? + + let objects = map { $0 } + for object in objects { + let group = DispatchGroup() + group.enter() + object.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + //If an error occurs, everything should be skipped + if parseError != nil { + error = parseError + } + savedChildObjects.forEach {(key, value) in + if error != nil { + return + } + if childObjects[key] == nil { + childObjects[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + savedChildFiles.forEach {(key, value) in + if error != nil { + return + } + if childFiles[key] == nil { + childFiles[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + group.leave() + } + group.wait() + if let error = error { + completion(.failure(error)) + return + } + } + + var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } - API.Command - .batch(commands: commands) - .executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch) + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: childObjects, + childFiles: childFiles) { results in + switch results { + + case .success(let saved): + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + completion(.success(returnBatch)) + } + completed += 1 + case .failure(let error): + completion(.failure(error)) + return + } + } + } } /** Fetches a collection of objects *synchronously* all at once and throws an error if necessary. - - parameter options: A set of options used to fetch objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a fetch was successful or a `ParseError` if it failed. - throws: `ParseError` @@ -123,7 +252,7 @@ public extension Sequence where Element: ParseObject { /** Fetches a collection of objects all at once *asynchronously* and executes the completion block when done. - - parameter options: A set of options used to fetch objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[(Result)], ParseError>)`. @@ -167,8 +296,10 @@ public extension Sequence where Element: ParseObject { /** Deletes a collection of objects *synchronously* all at once and throws an error if necessary. - - - parameter options: A set of options used to delete objects. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns `nil` if the delete successful or a `ParseError` if it failed. 1. A `ParseError.Code.aggregateError`. This object's "errors" property is an @@ -180,17 +311,27 @@ public extension Sequence where Element: ParseObject { instance, a connection failure in the middle of the delete). - throws: `ParseError` */ - func deleteAll(options: API.Options = []) throws -> [ParseError?] { + func deleteAll(batchLimit limit: Int? = nil, + options: API.Options = []) throws -> [ParseError?] { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var returnBatch = [ParseError?]() let commands = try map { try $0.deleteCommand() } - return try API.Command - .batch(commands: commands) - .execute(options: options) + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + try batches.forEach { + let currentBatch = try API.Command + .batch(commands: $0) + .execute(options: options) + returnBatch.append(contentsOf: currentBatch) + } + return returnBatch } /** Deletes a collection of objects all at once *asynchronously* and executes the completion block when done. - - - parameter options: A set of options used to delete objects. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[ParseError?], ParseError>)`. @@ -204,17 +345,36 @@ public extension Sequence where Element: ParseObject { instance, a connection failure in the middle of the delete). */ func deleteAll( + batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[ParseError?], ParseError>) -> Void ) { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { + var returnBatch = [ParseError?]() let commands = try map({ try $0.deleteCommand() }) - API.Command - .batch(commands: commands) - .executeAsync(options: options, - callbackQueue: callbackQueue, - completion: completion) + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch) + .executeAsync(options: options, + callbackQueue: callbackQueue) { results in + switch results { + + case .success(let saved): + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + completion(.success(returnBatch)) + } + completed += 1 + case .failure(let error): + completion(.failure(error)) + return + } + } + } } catch { guard let parseError = error as? ParseError else { completion(.failure(ParseError(code: .unknownError, @@ -227,28 +387,28 @@ public extension Sequence where Element: ParseObject { } // MARK: Batch Support -internal extension Sequence where Element: Encodable { +/*internal extension Sequence where Element: ParseType { /** Saves a collection of objects *synchronously* all at once and throws an error if necessary. - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` */ func saveAll(options: API.Options = []) throws -> [(Result)] { let commands = try map { try $0.saveCommand() } - return try API.Command + return try API.Command .batch(commands: commands) .execute(options: options) } -} +}*/ // MARK: CustomDebugStringConvertible extension ParseObject { public var debugDescription: String { - guard let descriptionData = try? ParseCoding.parseEncoder(skipKeys: false).encode(self), + guard let descriptionData = try? ParseCoding.parseEncoder().encode(self, skipKeys: .none), let descriptionString = String(data: descriptionData, encoding: .utf8) else { return "\(className) ()" } @@ -263,7 +423,7 @@ extension ParseObject { /** Fetches the `ParseObject` *synchronously* with the current data from the server and sets an error if one occurs. - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An Error of `ParseError` type. */ public func fetch(options: API.Options = []) throws -> Self { @@ -273,7 +433,7 @@ extension ParseObject { /** Fetches the `ParseObject` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. @@ -327,13 +487,13 @@ extension ParseObject { /** Saves the `ParseObject` *synchronously* and throws an error if there's an issue. - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: A Error of type `ParseError`. - returns: Returns saved `ParseObject`. */ public func save(options: API.Options = []) throws -> Self { - var childObjects: [NSDictionary: PointerType]? + var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? var error: ParseError? let group = DispatchGroup() @@ -356,7 +516,7 @@ extension ParseObject { /** Saves the `ParseObject` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. @@ -385,47 +545,48 @@ extension ParseObject { // swiftlint:disable:next function_body_length internal func ensureDeepSave(options: API.Options = [], - completion: @escaping ([NSDictionary: PointerType], + completion: @escaping ([String: PointerType], [UUID: ParseFile], ParseError?) -> Void) { let queue = DispatchQueue(label: "com.parse.deepSave", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) queue.sync { - var objectsFinishedSaving = [NSDictionary: PointerType]() + var objectsFinishedSaving = [String: PointerType]() var filesFinishedSaving = [UUID: ParseFile]() do { let object = try ParseCoding.parseEncoder() - .encode(self, collectChildren: true, - objectsSavedBeforeThisOne: nil, filesSavedBeforeThisOne: nil) + .encode(self, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil) var waitingToBeSaved = object.unsavedChildren while waitingToBeSaved.count > 0 { var savableObjects = [Encodable]() var savableFiles = [ParseFile]() - var nextBatch = [Encodable]() + var nextBatch = [ParseType]() try waitingToBeSaved.forEach { parseType in if let parseFile = parseType as? ParseFile { //ParseFiles can be saved now savableFiles.append(parseFile) - } else { + } else if let parseObject = parseType as? Objectable { //This is a ParseObject let waitingObjectInfo = try ParseCoding .parseEncoder() - .encode(parseType, + .encode(parseObject, collectChildren: true, objectsSavedBeforeThisOne: objectsFinishedSaving, filesSavedBeforeThisOne: filesFinishedSaving) if waitingObjectInfo.unsavedChildren.count == 0 { //If this ParseObject has no additional children, it can be saved now - savableObjects.append(parseType) + savableObjects.append(parseObject) } else { //Else this ParseObject needs to wait until it's children are saved - nextBatch.append(parseType) + nextBatch.append(parseObject) } } } @@ -440,15 +601,19 @@ extension ParseObject { } //Currently, batch isn't working for Encodable - //savableObjects.saveAll(encodableObjects: savable) + /*if let parseTypes = savableObjects as? [ParseType] { + let savedChildObjects = try self.saveAll(options: options, objects: parseTypes) + }*/ try savableObjects.forEach { - let hash = BaseObjectable.createHash($0) - objectsFinishedSaving[hash] = try $0.save(options: options) + let hash = try BaseObjectable.createHash($0) + if let parseType = $0 as? ParseType { + objectsFinishedSaving[hash] = try parseType.save(options: options) + } } try savableFiles.forEach { - var file = $0 - filesFinishedSaving[file.localUUID] = try $0.save(options: options) + let file = $0 + filesFinishedSaving[file.localId] = try $0.save(options: options) } } completion(objectsFinishedSaving, filesFinishedSaving, nil) @@ -466,7 +631,7 @@ extension ParseObject { } // MARK: Savable Encodable Version -internal extension Encodable { +internal extension ParseType { func save(options: API.Options = []) throws -> PointerType { try saveCommand().execute(options: options) } @@ -474,14 +639,13 @@ internal extension Encodable { func saveCommand() throws -> API.Command { try API.Command.saveCommand(self) } - - func saveAll(options: API.Options = [], - encodableObjects: [T]) throws -> [(Result)] { - let commands = try encodableObjects.map { try $0.saveCommand() } - return try API.Command +/* + func saveAll(options: API.Options = [], objects: [T]) throws -> [(Result)] { + let commands = try objects.map { try API.Command.saveCommand($0) } + return try API.Command .batch(commands: commands) .execute(options: options) - } + }*/ } // MARK: Deletable @@ -489,7 +653,7 @@ extension ParseObject { /** Deletes the `ParseObject` *synchronously* with the current data from the server and sets an error if one occurs. - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An Error of `ParseError` type. */ public func delete(options: API.Options = []) throws { @@ -501,7 +665,7 @@ extension ParseObject { /** Deletes the `ParseObject` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. @@ -529,7 +693,7 @@ extension ParseObject { } } - internal func deleteCommand() throws -> API.Command { - try API.Command.deleteCommand(self) + internal func deleteCommand() throws -> API.NonParseBodyCommand { + try API.NonParseBodyCommand.deleteCommand(self) } }// swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/Object Protocols/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift similarity index 59% rename from Sources/ParseSwift/Object Protocols/ParseUser.swift rename to Sources/ParseSwift/Objects/ParseUser.swift index 89690b64b..2f4b10bc1 100644 --- a/Sources/ParseSwift/Object Protocols/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -25,6 +25,17 @@ public protocol ParseUser: ParseObject { var password: String? { get set } } +// MARK: SignupBody +struct SignupBody: Encodable { + let username: String + let password: String +} + +// MARK: EmailBody +struct EmailBody: Encodable { + let email: String +} + // MARK: Default Implementations public extension ParseUser { static var className: String { @@ -32,6 +43,17 @@ public extension ParseUser { } } +// MARK: Convenience +extension ParseUser { + var endpoint: API.Endpoint { + if let objectId = objectId { + return .user(objectId: objectId) + } + + return .users + } +} + // MARK: CurrentUserContainer struct CurrentUserContainer: Codable { var currentUser: T? @@ -104,8 +126,8 @@ extension ParseUser { If login failed due to either an incorrect password or incorrect username, it throws a `ParseError`. */ public static func login(username: String, - password: String) throws -> Self { - try loginCommand(username: username, password: password).execute(options: []) + password: String, options: API.Options = []) throws -> Self { + try loginCommand(username: username, password: password).execute(options: options) } /** @@ -122,21 +144,22 @@ extension ParseUser { public static func login( username: String, password: String, + options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void ) { loginCommand(username: username, password: password) - .executeAsync(options: [], callbackQueue: callbackQueue, completion: completion) + .executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } - private static func loginCommand(username: String, - password: String) -> API.Command { + internal static func loginCommand(username: String, + password: String) -> API.NonParseBodyCommand { let params = [ "username": username, "password": password ] - return API.Command(method: .GET, + return API.NonParseBodyCommand(method: .GET, path: .login, params: params) { (data) -> Self in let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) @@ -175,20 +198,139 @@ extension ParseUser { - parameter completion: A block that will be called when logging out, completes or fails. */ public static func logout(options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void) { + completion: @escaping (ParseError?) -> Void) { logoutCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result.map { true }) + switch result { + + case .success: + completion(nil) + case .failure(let error): + completion(error) + } } } - private static func logoutCommand() -> API.Command { - return API.Command(method: .POST, path: .logout) { (_) -> Void in + internal static func logoutCommand() -> API.NonParseBodyCommand { + return API.NonParseBodyCommand(method: .POST, path: .logout) { (data) -> NoBody in + var parseError: ParseError? + var serverResponse = NoBody() + do { + serverResponse = try ParseCoding.jsonDecoder().decode(NoBody.self, from: data) + } catch { + if let foundError = error as? ParseError { + parseError = foundError + } else { + parseError = ParseError(code: .unknownError, message: error.localizedDescription) + } + } + //Always let user logout locally, no matter the error. deleteCurrentContainerFromKeychain() currentUserContainer = nil + guard let error = parseError else { + return serverResponse + } + throw error } } } +// MARK: Password Reset +extension ParseUser { + + /** + Requests *synchronously* a password reset email to be sent to the specified email address + associated with the user account. This email allows the user to securely reset their password on the web. + - parameter email: The email address associated with the user that forgot their password. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + */ + public static func passwordReset(email: String, options: API.Options = []) throws { + if let error = try passwordResetCommand(email: email).execute(options: options) { + throw error + } + } + + /** + Requests *asynchronously* a password reset email to be sent to the specified email address + associated with the user account. This email allows the user to securely reset their password on the web. + - parameter email: The email address associated with the user that forgot their password. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: A block that will be called when the password reset completes or fails. + */ + public static func passwordReset(email: String, options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (ParseError?) -> Void) { + passwordResetCommand(email: email).executeAsync(options: options, callbackQueue: callbackQueue) { result in + switch result { + + case .success(let error): + completion(error) + case .failure(let error): + completion(error) + } + } + } + + internal static func passwordResetCommand(email: String) -> API.NonParseBodyCommand { + let emailBody = EmailBody(email: email) + return API.NonParseBodyCommand(method: .POST, path: .passwordReset, body: emailBody) { (data) -> ParseError? in + try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + } + } +} + +// MARK: Verification Email Request +extension ParseUser { + + /** + Requests *synchronously* a verification email be sent to the specified email address + associated with the user account. + - parameter email: The email address associated with the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + */ + public static func verificationEmailRequest(email: String, + options: API.Options = []) throws { + if let error = try verificationEmailRequestCommand(email: email).execute(options: options) { + throw error + } + } + + /** + Requests *asynchronously* a verification email be sent to the specified email address + associated with the user account. + - parameter email: The email address associated with the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: A block that will be called when the verification request completes or fails. + */ + public static func verificationEmailRequest(email: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (ParseError?) -> Void) { + verificationEmailRequestCommand(email: email) + .executeAsync(options: options, + callbackQueue: callbackQueue) { result in + switch result { + + case .success(let error): + completion(error) + case .failure(let error): + completion(error) + } + } + } + + // swiftlint:disable:next line_length + internal static func verificationEmailRequestCommand(email: String) -> API.NonParseBodyCommand { + let emailBody = EmailBody(email: email) + return API.NonParseBodyCommand(method: .POST, + path: .verificationEmailRequest, + body: emailBody) { (data) -> ParseError? in + try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + } + } +} + // MARK: Signing Up extension ParseUser { /** @@ -199,7 +341,7 @@ extension ParseUser { - warning: Make sure that password and username are set before calling this method. - parameter username: The username of the user. - parameter password: The password of the user. - - parameter options: A set of options used to sign up users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns whether the sign up was successful. */ public static func signup(username: String, @@ -213,7 +355,7 @@ extension ParseUser { This will also enforce that the username isn't already taken. - warning: Make sure that password and username are set before calling this method. - - parameter options: A set of options used to sign up users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns whether the sign up was successful. */ public func signup(options: API.Options = []) throws -> Self { @@ -226,7 +368,7 @@ extension ParseUser { This will also enforce that the username isn't already taken. - warning: Make sure that password and username are set before calling this method. - - parameter options: A set of options used to sign up users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. @@ -244,7 +386,7 @@ extension ParseUser { - warning: Make sure that password and username are set before calling this method. - parameter username: The username of the user. - parameter password: The password of the user. - - parameter options: A set of options used to sign up users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. @@ -257,14 +399,14 @@ extension ParseUser { completion: @escaping (Result) -> Void ) { signupCommand(username: username, password: password) - .executeAsync(options: [], callbackQueue: callbackQueue, completion: completion) + .executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } - private static func signupCommand(username: String, - password: String) -> API.Command { + internal static func signupCommand(username: String, + password: String) -> API.NonParseBodyCommand { let body = SignupBody(username: username, password: password) - return API.Command(method: .POST, path: .signup, body: body) { (data) -> Self in + return API.NonParseBodyCommand(method: .POST, path: .users, body: body) { (data) -> Self in let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) @@ -280,8 +422,8 @@ extension ParseUser { } } - private func signupCommand() -> API.Command { - return API.Command(method: .POST, path: .signup, body: self) { (data) -> Self in + internal func signupCommand() -> API.Command { + return API.Command(method: .POST, path: .users, body: self) { (data) -> Self in let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) @@ -298,12 +440,6 @@ extension ParseUser { } } -// MARK: SignupBody -private struct SignupBody: Codable { - let username: String - let password: String -} - // MARK: Fetchable extension ParseUser { internal static func updateKeychainIfNeeded(_ results: [Self], deleting: Bool = false) throws { @@ -332,7 +468,7 @@ extension ParseUser { /** Fetches the `ParseUser` *synchronously* with the current data from the server and sets an error if one occurs. - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An Error of `ParseError` type. - important: If an object fetched has the same objectId as current, it will automatically update the current. */ @@ -345,7 +481,7 @@ extension ParseUser { /** Fetches the `ParseUser` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. @@ -370,6 +506,17 @@ extension ParseUser { completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } + + func fetchCommand() throws -> API.Command { + guard isSaved else { + throw ParseError(code: .unknownError, message: "Cannot fetch an object without id") + } + + return API.Command(method: .GET, + path: endpoint) { (data) -> Self in + try ParseCoding.jsonDecoder().decode(Self.self, from: data) + } + } } // MARK: Savable @@ -378,13 +525,13 @@ extension ParseUser { /** Saves the `ParseUser` *synchronously* and throws an error if there's an issue. - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: A Error of type `ParseError`. - returns: Returns saved `ParseUser`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ public func save(options: API.Options = []) throws -> Self { - var childObjects: [NSDictionary: PointerType]? + var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? var error: ParseError? let group = DispatchGroup() @@ -412,7 +559,7 @@ extension ParseUser { /** Saves the `ParseUser` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. @@ -439,6 +586,34 @@ extension ParseUser { completion(.failure(parseError)) } } + + func saveCommand() -> API.Command { + if isSaved { + return updateCommand() + } + return createCommand() + } + + // MARK: Saving ParseObjects - private + private func createCommand() -> API.Command { + let mapper = { (data) -> Self in + try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: self) + } + return API.Command(method: .POST, + path: endpoint, + body: self, + mapper: mapper) + } + + private func updateCommand() -> API.Command { + let mapper = { (data) -> Self in + try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: self) + } + return API.Command(method: .PUT, + path: endpoint, + body: self, + mapper: mapper) + } } // MARK: Deletable @@ -446,7 +621,7 @@ extension ParseUser { /** Deletes the `ParseUser` *synchronously* with the current data from the server and sets an error if one occurs. - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An Error of `ParseError` type. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ @@ -458,7 +633,7 @@ extension ParseUser { /** Deletes the `ParseUser` *asynchronously* and executes the given callback block. - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. @@ -487,6 +662,19 @@ extension ParseUser { completion(ParseError(code: .unknownError, message: error.localizedDescription)) } } + + func deleteCommand() throws -> API.NonParseBodyCommand { + guard isSaved else { + throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") + } + + return API.NonParseBodyCommand( + method: .DELETE, + path: endpoint + ) { (data) -> ParseError? in + try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + } + } } // MARK: Batch Support @@ -494,55 +682,169 @@ public extension Sequence where Element: ParseUser { /** Saves a collection of users *synchronously* all at once and throws an error if necessary. - - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. - throws: `ParseError` - important: If an object saved has the same objectId as current, it will automatically update the current. */ - func saveAll(options: API.Options = []) throws -> [(Result)] { + func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length + options: API.Options = []) throws -> [(Result)] { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var childObjects = [String: PointerType]() + var childFiles = [UUID: ParseFile]() + var error: ParseError? + let users = map { $0 } + for user in users { + let group = DispatchGroup() + group.enter() + user.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + //If an error occurs, everything should be skipped + if parseError != nil { + error = parseError + } + savedChildObjects.forEach {(key, value) in + if error != nil { + return + } + if childObjects[key] == nil { + childObjects[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + savedChildFiles.forEach {(key, value) in + if error != nil { + return + } + if childFiles[key] == nil { + childFiles[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + group.leave() + } + group.wait() + if let error = error { + throw error + } + } + + var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } - let returnResults = try API.Command - .batch(commands: commands) - .execute(options: options) - try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) - return returnResults + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + try batches.forEach { + let currentBatch = try API.Command + .batch(commands: $0) + .execute(options: options, + childObjects: childObjects, + childFiles: childFiles) + returnBatch.append(contentsOf: currentBatch) + } + try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) + return returnBatch } /** Saves a collection of users all at once *asynchronously* and executes the completion block when done. - - - parameter options: A set of options used to save users. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[(Result)], ParseError>)`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ - func saveAll( + func saveAll( // swiftlint:disable:this function_body_length cyclomatic_complexity + batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var childObjects = [String: PointerType]() + var childFiles = [UUID: ParseFile]() + var error: ParseError? + + let users = map { $0 } + for user in users { + let group = DispatchGroup() + group.enter() + user.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) -> Void in + //If an error occurs, everything should be skipped + if parseError != nil { + error = parseError + } + savedChildObjects.forEach {(key, value) in + if error != nil { + return + } + if childObjects[key] == nil { + childObjects[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + savedChildFiles.forEach {(key, value) in + if error != nil { + return + } + if childFiles[key] == nil { + childFiles[key] = value + } else { + error = ParseError(code: .unknownError, message: "circular dependency") + return + } + } + group.leave() + } + group.wait() + if let error = error { + completion(.failure(error)) + return + } + } + + var returnBatch = [(Result)]() let commands = map { $0.saveCommand() } - API.Command - .batch(commands: commands) - .executeAsync(options: options, callbackQueue: callbackQueue) { results in + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch) + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: childObjects, + childFiles: childFiles) { results in switch results { case .success(let saved): - try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0}) - completion(.success(saved)) + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()}) + completion(.success(returnBatch)) + } + completed += 1 case .failure(let error): completion(.failure(error)) + return } } + } } /** Fetches a collection of users *synchronously* all at once and throws an error if necessary. - - parameter options: A set of options used to fetch users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns a Result enum with the object if a fetch was successful or a `ParseError` if it failed. - throws: `ParseError` @@ -578,7 +880,7 @@ public extension Sequence where Element: ParseUser { /** Fetches a collection of users all at once *asynchronously* and executes the completion block when done. - - parameter options: A set of options used to fetch users. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[(Result)], ParseError>)`. @@ -624,8 +926,10 @@ public extension Sequence where Element: ParseUser { /** Deletes a collection of users *synchronously* all at once and throws an error if necessary. - - - parameter options: A set of options used to delete users. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns `nil` if the delete successful or a `ParseError` if it failed. 1. A `ParseError.Code.aggregateError`. This object's "errors" property is an @@ -638,20 +942,28 @@ public extension Sequence where Element: ParseUser { - throws: `ParseError` - important: If an object deleted has the same objectId as current, it will automatically update the current. */ - func deleteAll(options: API.Options = []) throws -> [ParseError?] { + func deleteAll(batchLimit limit: Int? = nil, + options: API.Options = []) throws -> [ParseError?] { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit + var returnBatch = [ParseError?]() let commands = try map { try $0.deleteCommand() } - let returnResults = try API.Command - .batch(commands: commands) - .execute(options: options) - + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + try batches.forEach { + let currentBatch = try API.Command + .batch(commands: $0) + .execute(options: options) + returnBatch.append(contentsOf: currentBatch) + } try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) - return returnResults + return returnBatch } /** Deletes a collection of users all at once *asynchronously* and executes the completion block when done. - - - parameter options: A set of options used to delete users. Defaults to an empty set. + - parameter batchLimit: The maximum number of objects to send in each batch. If the items to be batched + is greater than the `batchLimit`, the objects will be sent to the server in waves up to the `batchLimit`. + Defaults to 50. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[ParseError?], ParseError>)`. @@ -666,25 +978,37 @@ public extension Sequence where Element: ParseUser { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func deleteAll( + batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[ParseError?], ParseError>) -> Void ) { + let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { + var returnBatch = [ParseError?]() let commands = try map({ try $0.deleteCommand() }) - API.Command - .batch(commands: commands) - .executeAsync(options: options, - callbackQueue: callbackQueue) { results in + let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) + var completed = 0 + for batch in batches { + API.Command + .batch(commands: batch) + .executeAsync(options: options, + callbackQueue: callbackQueue) { results in switch results { - case .success(let deleted): - try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0}) - completion(.success(deleted)) + case .success(let saved): + returnBatch.append(contentsOf: saved) + if completed == (batches.count - 1) { + try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0}) + completion(.success(returnBatch)) + } + completed += 1 case .failure(let error): completion(.failure(error)) + return } } + } } catch { guard let parseError = error as? ParseError else { completion(.failure(ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 8f3572aec..2394e9bbf 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -15,6 +15,7 @@ enum ParseConstants { static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" static let fileDownloadsDirectory = "Downloads" + static let batchLimit = 50 #if os(iOS) static let deviceType = "ios" #elseif os(macOS) diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Deletable.swift b/Sources/ParseSwift/Protocols/Deletable.swift similarity index 100% rename from Sources/ParseSwift/Object Protocols/Protocols/Deletable.swift rename to Sources/ParseSwift/Protocols/Deletable.swift diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift b/Sources/ParseSwift/Protocols/Fetchable.swift similarity index 100% rename from Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift rename to Sources/ParseSwift/Protocols/Fetchable.swift diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Fileable.swift b/Sources/ParseSwift/Protocols/Fileable.swift similarity index 63% rename from Sources/ParseSwift/Object Protocols/Protocols/Fileable.swift rename to Sources/ParseSwift/Protocols/Fileable.swift index f0467a074..a5465fab6 100644 --- a/Sources/ParseSwift/Object Protocols/Protocols/Fileable.swift +++ b/Sources/ParseSwift/Protocols/Fileable.swift @@ -8,11 +8,10 @@ import Foundation -protocol Fileable: Encodable { +protocol Fileable: ParseType, Decodable, LocallyIdentifiable { var __type: String { get } // swiftlint:disable:this identifier_name var name: String { get set } var url: URL? { get set } - var localUUID: UUID { mutating get } } extension Fileable { @@ -20,20 +19,19 @@ extension Fileable { return url != nil } - // Equatable + mutating func hash(into hasher: inout Hasher) { + if let url = url { + hasher.combine(url) + } else { + hasher.combine(self.localId) + } + } + public static func == (lhs: Self, rhs: Self) -> Bool { guard let lURL = lhs.url, let rURL = rhs.url else { - var lhs = lhs - var rhs = rhs - return lhs.localUUID == rhs.localUUID + return lhs.localId == rhs.localId } return lURL == rURL } - - //Hashable - public func hash(into hasher: inout Hasher) { - var fileable = self - hasher.combine(fileable.localUUID) - } } diff --git a/Sources/ParseSwift/Protocols/LocallyIdentifiable.swift b/Sources/ParseSwift/Protocols/LocallyIdentifiable.swift new file mode 100644 index 000000000..d6dc08677 --- /dev/null +++ b/Sources/ParseSwift/Protocols/LocallyIdentifiable.swift @@ -0,0 +1,24 @@ +// +// LocallyIdentifiable.swift +// ParseSwift +// +// Created by Corey Baker on 12/31/20. +// Copyright © 2020 Parse Community. All rights reserved. +// + +import Foundation + +public protocol LocallyIdentifiable: Encodable, Hashable { + var localId: UUID { get set } +} + +extension LocallyIdentifiable { + + mutating func hash(into hasher: inout Hasher) { + hasher.combine(self.localId) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.localId == rhs.localId + } +} diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Objectable.swift b/Sources/ParseSwift/Protocols/Objectable.swift similarity index 77% rename from Sources/ParseSwift/Object Protocols/Protocols/Objectable.swift rename to Sources/ParseSwift/Protocols/Objectable.swift index 1edf94d0b..1c84ee0b2 100644 --- a/Sources/ParseSwift/Object Protocols/Protocols/Objectable.swift +++ b/Sources/ParseSwift/Protocols/Objectable.swift @@ -8,7 +8,7 @@ import Foundation -public protocol Objectable: Codable { +public protocol Objectable: ParseType, Decodable { /** The class name of the object. */ @@ -51,14 +51,9 @@ extension Objectable { return Self.className } - static func createHash(_ object: Encodable) -> NSDictionary { - let hash: NSDictionary = [ParseConstants.hashingKey: object] - return hash - } - - internal func getUniqueObject() throws -> UniqueObject { - let encoded = try ParseCoding.parseEncoder(skipKeys: false).encode(self) - return try ParseCoding.jsonDecoder().decode(UniqueObject.self, from: encoded) + static func createHash(_ object: Encodable) throws -> String { + let encoded = try ParseCoding.parseEncoder().encode(object) + return ParseHash.md5HashFromData(encoded) } } @@ -81,9 +76,17 @@ extension Objectable { } } -internal struct UniqueObject: Hashable, Codable { +internal struct UniqueObject: Encodable, Decodable, Hashable { let objectId: String + init?(target: Encodable) { + guard let objectable = target as? Objectable, + let objectId = objectable.objectId else { + return nil + } + self.objectId = objectId + } + init?(target: Objectable) { if let objectId = target.objectId { self.objectId = objectId diff --git a/Sources/ParseSwift/Protocols/ParseType.swift b/Sources/ParseSwift/Protocols/ParseType.swift new file mode 100644 index 000000000..83f53b329 --- /dev/null +++ b/Sources/ParseSwift/Protocols/ParseType.swift @@ -0,0 +1,11 @@ +// +// ParseType.swift +// ParseSwift +// +// Created by Corey Baker on 12/31/20. +// Copyright © 2020 Parse Community. All rights reserved. +// + +import Foundation + +public protocol ParseType: Encodable {} diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Queryable.swift b/Sources/ParseSwift/Protocols/Queryable.swift similarity index 100% rename from Sources/ParseSwift/Object Protocols/Protocols/Queryable.swift rename to Sources/ParseSwift/Protocols/Queryable.swift diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Savable.swift b/Sources/ParseSwift/Protocols/Savable.swift similarity index 100% rename from Sources/ParseSwift/Object Protocols/Protocols/Savable.swift rename to Sources/ParseSwift/Protocols/Savable.swift diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift index 5432139df..def6081e6 100644 --- a/Sources/ParseSwift/Storage/ParseFileManager.swift +++ b/Sources/ParseSwift/Storage/ParseFileManager.swift @@ -106,7 +106,7 @@ internal struct ParseFileManager { completion(ParseError(code: .unknownError, message: "Couldn't convert string to utf8")) return } - try data.write(to: filePath, options: defaultDataWritingOptions) + try data.write(to: filePath, options: self.defaultDataWritingOptions) completion(nil) } catch { completion(error) @@ -117,7 +117,7 @@ internal struct ParseFileManager { func writeData(_ data: Data, filePath: URL, completion: @escaping(Error?) -> Void) { synchronizationQueue.async { do { - try data.write(to: filePath, options: defaultDataWritingOptions) + try data.write(to: filePath, options: self.defaultDataWritingOptions) completion(nil) } catch { completion(error) @@ -155,7 +155,7 @@ internal struct ParseFileManager { return } - try createDirectoryIfNeeded(toPath.path) + try self.createDirectoryIfNeeded(toPath.path) let contents = try FileManager.default.contentsOfDirectory(atPath: fromPath.path) if contents.count == 0 { completion(nil) diff --git a/Sources/ParseSwift/Parse Types/ParseACL.swift b/Sources/ParseSwift/Types/ParseACL.swift similarity index 98% rename from Sources/ParseSwift/Parse Types/ParseACL.swift rename to Sources/ParseSwift/Types/ParseACL.swift index a1aad370a..6232754e8 100644 --- a/Sources/ParseSwift/Parse Types/ParseACL.swift +++ b/Sources/ParseSwift/Types/ParseACL.swift @@ -15,7 +15,7 @@ import Foundation "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. */ -public struct ParseACL: Codable, Equatable, Hashable { +public struct ParseACL: ParseType, Decodable, Equatable, Hashable { private static let publicScope = "*" private var acl: [String: [Access: Bool]]? @@ -316,7 +316,7 @@ extension ParseACL { extension ParseACL: CustomDebugStringConvertible { public var debugDescription: String { - guard let descriptionData = try? JSONEncoder().encode(self), + guard let descriptionData = try? ParseCoding.jsonEncoder().encode(self), let descriptionString = String(data: descriptionData, encoding: .utf8) else { return "ACL ()" } diff --git a/Sources/ParseSwift/Types/ParseCloud.swift b/Sources/ParseSwift/Types/ParseCloud.swift new file mode 100644 index 000000000..27410a00f --- /dev/null +++ b/Sources/ParseSwift/Types/ParseCloud.swift @@ -0,0 +1,120 @@ +// +// ParseCloud.swift +// ParseSwift +// +// Created by Corey Baker on 12/29/20. +// Copyright © 2020 Parse Community. All rights reserved. +// + +import Foundation + +/** + Objects that conform to the `ParseCloud` protocol are able to call Parse Cloud Functions and Jobs. + An object should be should be instantiated for each function and job type. When conforming to + `ParseCloud`, any properties added will be passed as parameters to your Cloud Function or Job. +*/ +public protocol ParseCloud: ParseType, Decodable, CustomDebugStringConvertible { + /** + The name of the function or job. + */ + var functionJobName: String { get set } + +} + +// MARK: Functions +extension ParseCloud { + + /** + Calls *synchronously* a Cloud Code function and returns a result of it's execution. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: Returns a JSON response of `AnyCodable` type. + */ + public func callFunction(options: API.Options = []) throws -> AnyCodable { + try callFunctionCommand().execute(options: options) + } + + /** + Calls *asynchronously* a Cloud Code function and returns a result of it's execution. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: A block that will be called when logging out, completes or fails. + It should have the following argument signature: `(Result)`. + */ + public func callFunction(options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + callFunctionCommand() + .executeAsync(options: options, + callbackQueue: callbackQueue, completion: completion) + } + + internal func callFunctionCommand() -> API.Command { + + return API.Command(method: .POST, + path: .functions(name: functionJobName), + body: self) { (data) -> AnyCodable in + let response = try ParseCoding.jsonDecoder().decode(AnyResultResponse.self, from: data) + guard let result = response.result else { + if let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) { + throw error + } + return AnyCodable() + } + return result + } + } +} + +// MARK: Jobs +extension ParseCloud { + /** + Calls *synchronously* a Cloud Code job and returns a result of it's execution. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: Returns a JSON response of `AnyCodable` type. + */ + public func callJob(options: API.Options = []) throws -> AnyCodable { + try callJobCommand().execute(options: options) + } + + /** + Calls *asynchronously* a Cloud Code job and returns a result of it's execution. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: A block that will be called when logging out, completes or fails. + It should have the following argument signature: `(Result)`. + */ + public func callJob(options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + callJobCommand() + .executeAsync(options: options, + callbackQueue: callbackQueue, completion: completion) + } + + internal func callJobCommand() -> API.Command { + return API.Command(method: .POST, + path: .jobs(name: functionJobName), + body: self) { (data) -> AnyCodable in + let response = try ParseCoding.jsonDecoder().decode(AnyResultResponse.self, from: data) + guard let result = response.result else { + if let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) { + throw error + } + return AnyCodable() + } + return result + } + } +} + +// MARK: CustomDebugStringConvertible +extension ParseCloud { + public var debugDescription: String { + guard let descriptionData = try? ParseCoding.parseEncoder().encode(self, skipKeys: .none), + let descriptionString = String(data: descriptionData, encoding: .utf8) else { + return "\(functionJobName)" + } + + return "\(descriptionString)" + } +} diff --git a/Sources/ParseSwift/Parse Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift similarity index 99% rename from Sources/ParseSwift/Parse Types/ParseError.swift rename to Sources/ParseSwift/Types/ParseError.swift index 9f4653d3b..c1b45aee9 100644 --- a/Sources/ParseSwift/Parse Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -8,7 +8,7 @@ import Foundation -public struct ParseError: Swift.Error, Codable { +public struct ParseError: ParseType, Decodable, Swift.Error { public let code: Code public let message: String diff --git a/Sources/ParseSwift/Parse Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift similarity index 90% rename from Sources/ParseSwift/Parse Types/ParseFile.swift rename to Sources/ParseSwift/Types/ParseFile.swift index d0ed7264d..47a112e95 100644 --- a/Sources/ParseSwift/Parse Types/ParseFile.swift +++ b/Sources/ParseSwift/Types/ParseFile.swift @@ -4,7 +4,7 @@ import Foundation A `ParseFile` object representes a file of binary data stored on the Parse server. This can be a image, video, or anything else that an application needs to reference in a non-relational way. */ -public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { +public struct ParseFile: Fileable, Savable, Fetchable, Deletable { internal let __type: String = "File" // swiftlint:disable:this identifier_name @@ -15,15 +15,7 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { && data == nil } - internal var _localUUID: UUID? // swiftlint:disable:this identifier_name - internal var localUUID: UUID { - mutating get { - if self._localUUID == nil { - self._localUUID = UUID() - } - return _localUUID! - } - } + public var localId: UUID /** The name of the file. @@ -43,7 +35,7 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { public var localURL: URL? /** - The link to the file online that should be downloaded. + The link to the file online that should be fetched before uploading to the Parse Server. */ public var cloudURL: URL? @@ -55,13 +47,13 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { /// The Content-Type header to use for the file. public var mimeType: String? - /// Key value pairs to be stored with file object + /// Key value pairs to be stored with the file object. public var metadata: [String: String]? - /// Key value pairs to be stored with file object + /// Key value pairs to be stored with the file object. public var tags: [String: String]? - /// A set of options used to delete files. + /// A set of header options sent to the server. public var options: API.Options = [] /** @@ -75,8 +67,11 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { extention of `name`. - parameter metadata: Optional key value pairs to be stored with file object - parameter tags: Optional key value pairs to be stored with file object + - note: `metadata` and `tags` is file adapter specific and not supported by all file adapters. + For more, see details on the + [S3 adapter](https://github.com/parse-community/parse-server-s3-adapter#adding-metadata-and-tags) */ - public init(name: String = "file", data: Data? = nil, mimeType: String? = nil, + public init(name: String = "file", data: Data, mimeType: String? = nil, metadata: [String: String]? = nil, tags: [String: String]? = nil, options: API.Options = []) { self.name = name @@ -85,7 +80,7 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { self.metadata = metadata self.tags = tags self.options = options - _ = self.localUUID //Need to ensure this creates a uuid + self.localId = UUID() } /** @@ -97,8 +92,11 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { - parameter mimeType: Specify the Content-Type header to use for the file, for example "application/pdf". The default is nil. If no value is specified the file type will be inferred from the file extention of `name`. - - parameter metadata: Optional key value pairs to be stored with file object - - parameter tags: Optional key value pairs to be stored with file object + - parameter metadata: Optional key value pairs to be stored with file object. + - parameter tags: Optional key value pairs to be stored with file object. + - note: `metadata` and `tags` is file adapter specific and not supported by all file adapters. + For more, see details on the + [S3 adapter](https://github.com/parse-community/parse-server-s3-adapter#adding-metadata-and-tags). */ public init(name: String = "file", localURL: URL, metadata: [String: String]? = nil, tags: [String: String]? = nil, @@ -108,7 +106,7 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { self.metadata = metadata self.tags = tags self.options = options - _ = self.localUUID //Need to ensure this creates a uuid + self.localId = UUID() } /** @@ -120,8 +118,11 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { - parameter mimeType: Specify the Content-Type header to use for the file, for example "application/pdf". The default is nil. If no value is specified the file type will be inferred from the file extention of `name`. - - parameter metadata: Optional key value pairs to be stored with file object - - parameter tags: Optional key value pairs to be stored with file object + - parameter metadata: Optional key value pairs to be stored with file object. + - parameter tags: Optional key value pairs to be stored with file object. + - note: `metadata` and `tags` is file adapter specific and not supported by all file adapters. + For more, see details on the + [S3 adapter](https://github.com/parse-community/parse-server-s3-adapter#adding-metadata-and-tags). */ public init(name: String = "file", cloudURL: URL, metadata: [String: String]? = nil, tags: [String: String]? = nil, @@ -131,7 +132,7 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { self.metadata = metadata self.tags = tags self.options = options - _ = self.localUUID //Need to ensure this creates a uuid + self.localId = UUID() } enum CodingKeys: String, CodingKey { @@ -141,12 +142,21 @@ public struct ParseFile: Fileable, Savable, Fetchable, Deletable, Hashable { } } +extension ParseFile { + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + url = try values.decode(URL.self, forKey: .url) + name = try values.decode(String.self, forKey: .name) + localId = UUID() + } +} + // MARK: Deleting extension ParseFile { /** Deletes the file from the Parse cloud. - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. - - parameter options: A set of options used to delete files. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: A `ParseError` if there was an issue deleting the file. Otherwise it was successful. */ public func delete(options: API.Options) throws { @@ -163,7 +173,7 @@ extension ParseFile { /** Deletes the file from the Parse cloud. Completes with `nil` if successful. - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. - - parameter options: A set of options used to delete files. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: A block that will be called when file deletes or fails. It should have the following argument signature: `(ParseError?)` @@ -233,7 +243,7 @@ extension ParseFile { print(currentProgess) } - - parameter options: A set of options used to save files. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter progress: A block that will be called when file updates it's progress. It should have the following argument signature: `(task: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. @@ -262,7 +272,7 @@ extension ParseFile { /** Creates a file with given data *synchronously*. A name will be assigned to it by the server. If the file hasn't been downloaded, it will automatically be downloaded before saved. - - parameter options: A set of options used to save files. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A saved `ParseFile`. */ public func save(options: API.Options = []) throws -> ParseFile { @@ -319,7 +329,7 @@ extension ParseFile { print(currentProgess) } - - parameter options: A set of options used to save files. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter progress: A block that will be called when file updates it's progress. It should have the following argument signature: `(task: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. @@ -383,7 +393,7 @@ extension ParseFile { ... }) - - parameter options: A set of options used to save files. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter progress: A block that will be called when file updates it's progress. It should have the following argument signature: `(task: URLSessionDownloadTask, @@ -437,11 +447,11 @@ extension ParseFile { } } -// MARK: Downloading +// MARK: Fetching extension ParseFile { /** Fetches a file with given url *synchronously*. - - parameter options: A set of options used to fetch the file. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter stream: An input file stream. - returns: A saved `ParseFile`. */ @@ -465,7 +475,7 @@ extension ParseFile { /** Fetches a file with given url *synchronously*. - - parameter options: A set of options used to fetch the file. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A saved `ParseFile`. */ public func fetch(options: API.Options = []) throws -> ParseFile { @@ -517,7 +527,7 @@ extension ParseFile { print(currentProgess) } - - parameter options: A set of options used to fetch the file. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter progress: A block that will be called when file updates it's progress. It should have the following argument signature: `(task: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. @@ -575,7 +585,7 @@ extension ParseFile { ... } - - parameter options: A set of options used to fetch the file. Defaults to an empty set. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter progress: A block that will be called when file updates it's progress. It should have the following argument signature: `(task: URLSessionDownloadTask, diff --git a/Sources/ParseSwift/Parse Types/ParseGeoPoint.swift b/Sources/ParseSwift/Types/ParseGeoPoint.swift similarity index 97% rename from Sources/ParseSwift/Parse Types/ParseGeoPoint.swift rename to Sources/ParseSwift/Types/ParseGeoPoint.swift index 7171f3852..10b0a4672 100644 --- a/Sources/ParseSwift/Parse Types/ParseGeoPoint.swift +++ b/Sources/ParseSwift/Types/ParseGeoPoint.swift @@ -8,7 +8,7 @@ import CoreLocation It could be used to perform queries in a geospatial manner using `ParseQuery.-whereKey:nearGeoPoint:`. Currently, instances of `ParseObject` may only have one key associated with a `ParseGeoPoint` type. */ -public struct ParseGeoPoint: Codable, Hashable, Equatable { +public struct ParseGeoPoint: Codable, Hashable { private let __type: String = "GeoPoint" // swiftlint:disable:this identifier_name static let earthRadiusMiles = 3958.8 static let earthRadiusKilometers = 6371.0 @@ -141,7 +141,7 @@ extension ParseGeoPoint { extension ParseGeoPoint: CustomDebugStringConvertible { public var debugDescription: String { - guard let descriptionData = try? JSONEncoder().encode(self), + guard let descriptionData = try? ParseCoding.jsonEncoder().encode(self), let descriptionString = String(data: descriptionData, encoding: .utf8) else { return "GeoPoint ()" } diff --git a/Sources/ParseSwift/Parse Types/Pointer.swift b/Sources/ParseSwift/Types/Pointer.swift similarity index 82% rename from Sources/ParseSwift/Parse Types/Pointer.swift rename to Sources/ParseSwift/Types/Pointer.swift index 24f0e095d..417280dcb 100644 --- a/Sources/ParseSwift/Parse Types/Pointer.swift +++ b/Sources/ParseSwift/Types/Pointer.swift @@ -14,7 +14,7 @@ private func getObjectId(target: Objectable) -> String { return objectId } -public struct Pointer: Fetchable, Codable { +public struct Pointer: Fetchable, Encodable { public typealias FetchingType = T private let __type: String = "Pointer" // swiftlint:disable:this identifier_name @@ -34,12 +34,18 @@ public struct Pointer: Fetchable, Codable { private enum CodingKeys: String, CodingKey { case __type, objectId, className // swiftlint:disable:this identifier_name } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + objectId = try values.decode(String.self, forKey: .objectId) + className = try values.decode(String.self, forKey: .className) + } } extension Pointer { public func fetch(options: API.Options = []) throws -> T { let path = API.Endpoint.object(className: className, objectId: objectId) - return try API.Command(method: .GET, + return try API.NonParseBodyCommand(method: .GET, path: path) { (data) -> T in try ParseCoding.jsonDecoder().decode(T.self, from: data) }.execute(options: options) @@ -48,14 +54,14 @@ extension Pointer { public func fetch(options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { let path = API.Endpoint.object(className: className, objectId: objectId) - API.Command(method: .GET, + API.NonParseBodyCommand(method: .GET, path: path) { (data) -> T in try ParseCoding.jsonDecoder().decode(T.self, from: data) }.executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } } -internal struct PointerType: Codable { +internal struct PointerType: Encodable { var __type: String = "Pointer" // swiftlint:disable:this identifier_name public var objectId: String diff --git a/Sources/ParseSwift/Parse Types/Query.swift b/Sources/ParseSwift/Types/Query.swift similarity index 91% rename from Sources/ParseSwift/Parse Types/Query.swift rename to Sources/ParseSwift/Types/Query.swift index 1d5962a4f..42eb7020f 100644 --- a/Sources/ParseSwift/Parse Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -477,8 +477,9 @@ internal struct QueryWhere: Encodable, Equatable { } } +// MARK: Query /** - The `Query` struct defines a query that is used to query for `ParseObject`s. + The `Query` class defines a query that is used to query for `ParseObject`s. */ public class Query: Encodable, Equatable where T: ParseObject { // interpolate as GET @@ -699,15 +700,15 @@ public class Query: Encodable, Equatable where T: ParseObject { } } +// MARK: Queryable extension Query: Queryable { public typealias ResultType = T - public typealias AnyResultType = [String: AnyCodable] /** Finds objects *synchronously* based on the constructed query and sets an error if there was one. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - returns: Returns an array of `ParseObject`s that were found. @@ -721,19 +722,19 @@ extension Query: Queryable { - parameter explain: Used to toggle the information on the query plan. - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - returns: Returns a dictionary of `AnyResultType` that is the JSON response of the query. */ - public func find(explain: Bool, hint: String? = nil, options: API.Options = []) throws -> AnyResultType { + public func find(explain: Bool, hint: String? = nil, options: API.Options = []) throws -> AnyCodable { try findCommand(explain: explain, hint: hint).execute(options: options) } /** Finds objects *asynchronously* and calls the given block with the results. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[ResultType], ParseError>)` @@ -748,14 +749,14 @@ extension Query: Queryable { - parameter explain: Used to toggle the information on the query plan. - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result<[AnyResultType], ParseError>)` */ public func find(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { findCommand(explain: explain, hint: hint).executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } @@ -764,7 +765,7 @@ extension Query: Queryable { 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`. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - returns: Returns a `ParseObject`, or `nil` if none was found. @@ -779,12 +780,12 @@ extension Query: Queryable { - warning: This method mutates the query. It will reset the limit to `1`. - parameter explain: Used to toggle the information on the query plan. - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - returns: Returns a dictionary of `AnyResultType` that is the JSON response of the query. */ - public func first(explain: Bool, hint: String? = nil, options: API.Options = []) throws -> AnyResultType { + public func first(explain: Bool, hint: String? = nil, options: API.Options = []) throws -> AnyCodable { try firstCommand(explain: explain, hint: hint).execute(options: options) } @@ -792,7 +793,7 @@ extension Query: Queryable { 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`. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. @@ -820,14 +821,14 @@ extension Query: Queryable { - warning: This method mutates the query. It will reset the limit to `1`. - parameter explain: Used to toggle the information on the query plan. - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ public func first(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { firstCommand(explain: explain, hint: hint).executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } @@ -835,7 +836,7 @@ extension Query: Queryable { /** Counts objects *synchronously* based on the constructed query and sets an error if there was one. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - returns: Returns the number of `ParseObject`s that match the query, or `-1` if there is an error. @@ -849,19 +850,19 @@ extension Query: Queryable { - parameter explain: Used to toggle the information on the query plan. - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - returns: Returns a dictionary of `AnyResultType` that is the JSON response of the query. */ - public func count(explain: Bool, hint: String? = nil, options: API.Options = []) throws -> AnyResultType { + public func count(explain: Bool, hint: String? = nil, options: API.Options = []) throws -> AnyCodable { try countCommand(explain: explain, hint: hint).execute(options: options) } /** Counts objects *asynchronously* and calls the given block with the counts. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result)` @@ -875,74 +876,97 @@ extension Query: Queryable { Counts objects *asynchronously* and calls the given block with the counts. - parameter explain: Used to toggle the information on the query plan. - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of options used to save objects. + - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - parameter completion: The block to execute. It should have the following argument signature: `(Result)` */ public func count(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { countCommand(explain: explain, hint: hint).executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } } private extension Query { - private func findCommand() -> API.Command, [ResultType]> { - return API.Command(method: .POST, path: endpoint, body: self) { + private func findCommand() -> API.NonParseBodyCommand, [ResultType]> { + return API.NonParseBodyCommand(method: .POST, path: endpoint, body: self) { try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).results } } - private func firstCommand() -> API.Command, ResultType?> { + private func firstCommand() -> API.NonParseBodyCommand, ResultType?> { let query = self query.limit = 1 - return API.Command(method: .POST, path: endpoint, body: query) { + return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).results.first } } - private func countCommand() -> API.Command, Int> { + private func countCommand() -> API.NonParseBodyCommand, Int> { let query = self query.limit = 1 query.isCount = true - return API.Command(method: .POST, path: endpoint, body: query) { + return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).count ?? 0 } } - private func findCommand(explain: Bool, hint: String?) -> API.Command, AnyResultType> { + private func findCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { let query = self query.explain = explain query.hint = hint - return API.Command(method: .POST, path: endpoint, body: query) { - try JSONDecoder().decode(AnyResultType.self, from: $0) + return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { + if let results = try JSONDecoder().decode(AnyResultsResponse.self, from: $0).results { + return results + } + return AnyCodable() } } - private func firstCommand(explain: Bool, hint: String?) -> API.Command, AnyResultType> { + private func firstCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { let query = self query.limit = 1 query.explain = explain query.hint = hint - return API.Command(method: .POST, path: endpoint, body: query) { - try JSONDecoder().decode(AnyResultType.self, from: $0) + return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { + if let results = try JSONDecoder().decode(AnyResultsResponse.self, from: $0).results { + return results + } + return AnyCodable() } } - private func countCommand(explain: Bool, hint: String?) -> API.Command, AnyResultType> { + private func countCommand(explain: Bool, hint: String?) -> API.NonParseBodyCommand, AnyCodable> { let query = self query.limit = 1 query.isCount = true query.explain = explain query.hint = hint - return API.Command(method: .POST, path: endpoint, body: query) { - try JSONDecoder().decode(AnyResultType.self, from: $0) + return API.NonParseBodyCommand(method: .POST, path: endpoint, body: query) { + if let results = try JSONDecoder().decode(AnyResultsResponse.self, from: $0).results { + return results + } + return AnyCodable() } } } +// MARK: ParseUser +extension Query where T: ParseUser { + var endpoint: API.Endpoint { + return .users + } +} + +// MARK: ParseInstallation +extension Query where T: ParseInstallation { + var endpoint: API.Endpoint { + return .installations + } +} + enum RawCodingKey: CodingKey { case key(String) var stringValue: String { diff --git a/Tests/ParseSwiftTests/ACLTests.swift b/Tests/ParseSwiftTests/ACLTests.swift index 9776c9950..19186e8ab 100644 --- a/Tests/ParseSwiftTests/ACLTests.swift +++ b/Tests/ParseSwiftTests/ACLTests.swift @@ -130,7 +130,7 @@ class ACLTests: XCTestCase { var encoded: Data? do { - encoded = try ParseCoding.parseEncoder().encode(acl) + encoded = try ParseCoding.jsonEncoder().encode(acl) } catch { XCTFail(error.localizedDescription) } @@ -184,7 +184,7 @@ class ACLTests: XCTestCase { MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -227,7 +227,7 @@ class ACLTests: XCTestCase { MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index e8cab6681..945a362e9 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -42,7 +42,10 @@ class APICommandTests: XCTestCase { } do { let returnedObject = - try API.Command(method: .GET, path: .login, params: nil, mapper: { (data) -> String in + try API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (data) -> String in return try JSONDecoder().decode(String.self, from: data) }).execute(options: []) XCTAssertEqual(originalObject, returnedObject) @@ -66,7 +69,10 @@ class APICommandTests: XCTestCase { } do { - _ = try API.Command(method: .GET, path: .login, params: nil, mapper: { (_) -> NoBody in + _ = try API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in throw originalError }).execute(options: []) XCTFail("Should have thrown an error") @@ -81,6 +87,7 @@ class APICommandTests: XCTestCase { //This is how errors HTTP errors should typically come in func testErrorHTTPJSON() { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" let codeKey = "code" @@ -89,6 +96,7 @@ class APICommandTests: XCTestCase { errorKey: errorValue, codeKey: codeValue ] + MockURLProtocol.mockRequests { _ in do { let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) @@ -98,29 +106,36 @@ class APICommandTests: XCTestCase { return nil } } + do { - _ = try API.Command(method: .GET, path: .login, params: nil, mapper: { (_) -> NoBody in - throw ParseError(code: .connectionFailed, message: "Connection failed") + _ = try API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError }).execute(options: []) + XCTFail("Should have thrown an error") } catch { guard let error = error as? ParseError else { XCTFail("should be able unwrap final error to ParseError") return } - let unknownError = ParseError(code: .unknownError, message: "") - XCTAssertEqual(unknownError.code, error.code) + XCTAssertEqual(error.code, parseError.code) } } //This is less common as the HTTP won't be able to produce ParseErrors directly, but used for testing func testErrorHTTPReturnsParseError1() { - let originalError = ParseError(code: .unknownError, message: "Couldn't decode") + let originalError = ParseError(code: .invalidServerResponse, message: "Couldn't decode") MockURLProtocol.mockRequests { _ in return MockURLResponse(error: originalError) } do { - _ = try API.Command(method: .GET, path: .login, params: nil, mapper: { (_) -> NoBody in + _ = try API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in throw originalError }).execute(options: []) XCTFail("Should have thrown an error") diff --git a/Tests/ParseSwiftTests/BatchUtilsTests.swift b/Tests/ParseSwiftTests/BatchUtilsTests.swift new file mode 100644 index 000000000..3a8e36bf1 --- /dev/null +++ b/Tests/ParseSwiftTests/BatchUtilsTests.swift @@ -0,0 +1,82 @@ +// +// BatchUtilsTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/2/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class BatchUtilsTests: XCTestCase { + override func setUpWithError() throws { + super.setUp() + } + + override func tearDownWithError() throws { + super.tearDown() + } + + func testSplitArrayLessSegments() throws { + let array = [1, 2] + let splitArray = BatchUtils.splitArray(array, valuesPerSegment: 3) + guard let firstSplit = splitArray.first else { + XCTFail("Should have a first item in the array") + return + } + XCTAssertEqual(splitArray.count, 1) + XCTAssertEqual(firstSplit, array) + } + + func testSplitArrayExactSegments() throws { + let array = [1, 2] + let splitArray = BatchUtils.splitArray(array, valuesPerSegment: 2) + guard let firstSplit = splitArray.first else { + XCTFail("Should have a first item in the array") + return + } + XCTAssertEqual(splitArray.count, 1) + XCTAssertEqual(firstSplit, array) + } + + func testSplitArrayMoreSegments() throws { + let array = [1, 2] + let splitArray = BatchUtils.splitArray(array, valuesPerSegment: 1) + guard let firstSplit = splitArray.first, + let lastSplit = splitArray.last else { + XCTFail("Should have a first item in the array") + return + } + XCTAssertEqual(splitArray.count, 2) + XCTAssertEqual(firstSplit, [1]) + XCTAssertEqual(lastSplit, [2]) + } + + func testSplitArrayEvenMoreSegments() throws { + let array = [1, 2, 3, 4, 5] + let splitArray = BatchUtils.splitArray(array, valuesPerSegment: 1) + guard let firstSplit = splitArray.first, + let lastSplit = splitArray.last else { + XCTFail("Should have a first item in the array") + return + } + XCTAssertEqual(splitArray.count, 5) + XCTAssertEqual(firstSplit, [1]) + XCTAssertEqual(lastSplit, [5]) + } + + func testSplitArrayComplexSegments() throws { + let array = [1, 2, 3, 4, 5, 6, 7] + let splitArray = BatchUtils.splitArray(array, valuesPerSegment: 2) + guard let firstSplit = splitArray.first, + let lastSplit = splitArray.last else { + XCTFail("Should have a first item in the array") + return + } + XCTAssertEqual(splitArray.count, 4) + XCTAssertEqual(firstSplit, [1, 2]) + XCTAssertEqual(lastSplit, [7]) + } +} diff --git a/Tests/ParseSwiftTests/ParseCloudTests.swift b/Tests/ParseSwiftTests/ParseCloudTests.swift new file mode 100644 index 000000000..b9d3c3f83 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseCloudTests.swift @@ -0,0 +1,459 @@ +// +// ParseCloudTests.swift +// ParseSwift +// +// Created by Corey Baker on 12/29/20. +// Copyright © 2020 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseCloudTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct Cloud: ParseCloud { + // Those are required for Object + var functionJobName: String + } + + struct Cloud2: ParseCloud { + // Those are required for Object + var functionJobName: String + + // Your custom keys + var customKey: String? + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "https://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + try KeychainStore.shared.deleteAll() + try ParseStorage.shared.deleteAll() + } + + func testJSONEncoding() throws { + let expected = ["functionJobName": "test"] + let cloud = Cloud(functionJobName: "test") + let encoded = try JSONEncoder().encode(cloud) + let decoded = try JSONDecoder().decode([String: String].self, from: encoded) + XCTAssertEqual(decoded, expected, "all keys should show up in JSONEncoder") + } + + func testJSONEncoding2() throws { + let expected = [ + "functionJobName": "test", + "customKey": "parse" + ] + let cloud = Cloud2(functionJobName: "test", customKey: "parse") + let encoded = try JSONEncoder().encode(cloud) + let decoded = try JSONDecoder().decode([String: String].self, from: encoded) + XCTAssertEqual(decoded, expected, "all keys should show up in JSONEncoder") + } + + func testParseEncoding() throws { + let expected = [String: String]() + let cloud = Cloud(functionJobName: "test") + let encoded = try ParseCoding.parseEncoder().encode(cloud, skipKeys: .cloud) + let decoded = try JSONDecoder().decode([String: String].self, from: encoded) + XCTAssertEqual(decoded, expected, "\"functionJobName\" key should be skipped by ParseEncoder") + } + + func testParseEncoding2() throws { + let expected = [ + "customKey": "parse" + ] + let cloud = Cloud2(functionJobName: "test", customKey: "parse") + let encoded = try ParseCoding.parseEncoder().encode(cloud, skipKeys: .cloud) + let decoded = try JSONDecoder().decode([String: String].self, from: encoded) + XCTAssertEqual(decoded, expected, "\"functionJobName\" key should be skipped by ParseEncoder") + } + + func testCallFunctionCommand() throws { + let cloud = Cloud(functionJobName: "test") + let command = cloud.callFunctionCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/functions/test") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertEqual(command.body?.functionJobName, "test") + } + + func testCallFunctionWithArgsCommand() throws { + let cloud = Cloud2(functionJobName: "test", customKey: "parse") + let command = cloud.callFunctionCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/functions/test") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertEqual(command.body?.functionJobName, "test") + XCTAssertEqual(command.body?.customKey, "parse") + } + + func testFunction() { + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + let cloud = Cloud(functionJobName: "test") + let functionResponse = try cloud.callFunction() + XCTAssertEqual(functionResponse, AnyCodable()) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testFunction2() { + var result: AnyCodable = ["hello": "world"] + let response = AnyResultResponse(result: result) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + let encodedResult = try ParseCoding.jsonEncoder().encode(result) + result = try ParseCoding.jsonDecoder().decode(AnyCodable.self, from: encodedResult) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + let cloud = Cloud(functionJobName: "test") + let functionResponse = try cloud.callFunction() + guard let resultAsDictionary = functionResponse.value as? [String: String] else { + XCTFail("Should have casted result to dictionary") + return + } + XCTAssertEqual(resultAsDictionary, ["hello": "world"]) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testFunctionError() { + + let parseError = ParseError(code: .scriptError, message: "Error: Invalid function") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(parseError) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let cloud = Cloud(functionJobName: "test") + _ = try cloud.callFunction() + XCTFail("Should have thrown ParseError") + } catch { + if let error = error as? ParseError { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + } + } + + func functionAsync(serverResponse: AnyCodable, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + let cloud = Cloud(functionJobName: "test") + cloud.callFunction(callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let response): + if serverResponse == AnyCodable() { + XCTAssertEqual(response, serverResponse) + } else { + guard let resultAsDictionary = serverResponse.value as? [String: String] else { + XCTFail("Should have casted result to dictionary") + expectation1.fulfill() + return + } + XCTAssertEqual(resultAsDictionary, ["hello": "world"]) + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testFunctionMainQueue() { + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.functionAsync(serverResponse: AnyCodable(), callbackQueue: .main) + } + + func testFunctionMainQueue2() { + let result: AnyCodable = ["hello": "world"] + let response = AnyResultResponse(result: result) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.functionAsync(serverResponse: result, callbackQueue: .main) + } + + func functionAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + let cloud = Cloud(functionJobName: "test") + cloud.callFunction(callbackQueue: callbackQueue) { result in + + switch result { + + case .success: + XCTFail("Should have thrown ParseError") + expectation1.fulfill() + + case .failure(let error): + XCTAssertEqual(error.code, parseError.code) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testFunctionMainQueueError() { + let parseError = ParseError(code: .scriptError, message: "Error: Invalid function") + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.functionAsyncError(parseError: parseError, callbackQueue: .main) + } + + func testCallJobCommand() throws { + let cloud = Cloud(functionJobName: "test") + let command = cloud.callJobCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/jobs/test") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertEqual(command.body?.functionJobName, "test") + } + + func testCallJobWithArgsCommand() throws { + let cloud = Cloud2(functionJobName: "test", customKey: "parse") + let command = cloud.callJobCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/jobs/test") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertEqual(command.body?.functionJobName, "test") + XCTAssertEqual(command.body?.customKey, "parse") + } + + func testJob() { + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + let cloud = Cloud(functionJobName: "test") + let functionResponse = try cloud.callJob() + XCTAssertEqual(functionResponse, AnyCodable()) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testJob2() { + let result: AnyCodable = ["hello": "world"] + let response = AnyResultResponse(result: result) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + let cloud = Cloud(functionJobName: "test") + let functionResponse = try cloud.callJob() + guard let resultAsDictionary = functionResponse.value as? [String: String] else { + XCTFail("Should have casted result to dictionary") + return + } + XCTAssertEqual(resultAsDictionary, ["hello": "world"]) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testJobError() { + + let parseError = ParseError(code: .scriptError, message: "Error: Invalid function") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(parseError) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let cloud = Cloud(functionJobName: "test") + _ = try cloud.callJob() + XCTFail("Should have thrown ParseError") + } catch { + if let error = error as? ParseError { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + } + } + + func jobAsync(serverResponse: AnyCodable, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + let cloud = Cloud(functionJobName: "test") + cloud.callJob(callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let response): + if serverResponse == AnyCodable() { + XCTAssertEqual(response, serverResponse) + } else { + guard let resultAsDictionary = serverResponse.value as? [String: String] else { + XCTFail("Should have casted result to dictionary") + expectation1.fulfill() + return + } + XCTAssertEqual(resultAsDictionary, ["hello": "world"]) + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testJobMainQueue() { + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.jobAsync(serverResponse: AnyCodable(), callbackQueue: .main) + } + + func testJobMainQueue2() { + let result: AnyCodable = ["hello": "world"] + let response = AnyResultResponse(result: result) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.jobAsync(serverResponse: result, callbackQueue: .main) + } + + func jobAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + let cloud = Cloud(functionJobName: "test") + cloud.callJob(callbackQueue: callbackQueue) { result in + + switch result { + + case .success: + XCTFail("Should have thrown ParseError") + expectation1.fulfill() + + case .failure(let error): + XCTAssertEqual(error.code, parseError.code) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testJobMainQueueError() { + let parseError = ParseError(code: .scriptError, message: "Error: Invalid function") + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.jobAsyncError(parseError: parseError, callbackQueue: .main) + } +} // swiftlint:disable:this file_length diff --git a/Tests/ParseSwiftTests/ParseEncoderTests.swift b/Tests/ParseSwiftTests/ParseEncoderTests.swift index 95e835dd0..28d45b636 100644 --- a/Tests/ParseSwiftTests/ParseEncoderTests.swift +++ b/Tests/ParseSwiftTests/ParseEncoderTests.swift @@ -44,18 +44,6 @@ class ParseEncoderTests: XCTestCase { let phoneNumbers: [String] } - func parseEncoding(for object: T) -> Data { - let encoder = ParseEncoder() - encoder.jsonEncoder.outputFormatting = .sortedKeys - - guard let encoding = try? encoder.encode(object) else { - XCTFail("Couldn't get a Parse encoding.") - return Data() - } - - return encoding - } - func referenceEncoding(for object: T) -> Data { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys @@ -68,35 +56,6 @@ class ParseEncoderTests: XCTestCase { return encoding } - func test_encodingScalarValue() { - let encoded = parseEncoding(for: ["": 5]) - let reference = referenceEncoding(for: ["": 5]) - XCTAssertEqual(encoded, reference) - } - - func test_encodingComplexValue() { - let value = Person( - addresses: [ - "home": Address(street: "Parse St.", city: "San Francisco"), - "work": Address(street: "Server Ave.", city: "Seattle") - ], - age: 21, - name: Name(first: "Parse", last: "User"), - nicknames: [ - Name(first: "Swift", last: "Developer"), - Name(first: "iOS", last: "Engineer") - ], - phoneNumbers: [ - "1-800-PARSE", - "1-999-SWIFT" - ] - ) - - let encoded = parseEncoding(for: value) - let reference = referenceEncoding(for: value) - XCTAssertEqual(encoded.count, reference.count) - } - func testNestedContatiner() throws { var newACL = ParseACL() newACL.publicRead = true @@ -104,7 +63,7 @@ class ParseEncoderTests: XCTestCase { let jsonEncoded = try JSONEncoder().encode(newACL) let jsonDecoded = try ParseCoding.jsonDecoder().decode([String: [String: Bool]].self, from: jsonEncoded) - let parseEncoded = try ParseCoding.parseEncoder().encode(newACL) + let parseEncoded = try ParseCoding.parseEncoder().encode(newACL, skipKeys: .object) let parseDecoded = try ParseCoding.jsonDecoder().decode([String: [String: Bool]].self, from: parseEncoded) XCTAssertEqual(jsonDecoded.keys.count, parseDecoded.keys.count) @@ -126,7 +85,7 @@ class ParseEncoderTests: XCTestCase { XCTAssertNotNil(decodedJSON["updatedAt"]) //ParseEncoder - let encoded = try ParseCoding.parseEncoder().encode(score) + let encoded = try ParseCoding.parseEncoder().encode(score, skipKeys: .object) let decoded = try ParseCoding.jsonDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(decoded["score"]?.value as? Int, score.score) XCTAssertNil(decoded["createdAt"]) diff --git a/Tests/ParseSwiftTests/ParseFileTests.swift b/Tests/ParseSwiftTests/ParseFileTests.swift index 6c0812ea0..64a2180bf 100644 --- a/Tests/ParseSwiftTests/ParseFileTests.swift +++ b/Tests/ParseSwiftTests/ParseFileTests.swift @@ -79,7 +79,6 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(command.method, API.Method.POST) XCTAssertNil(command.params) XCTAssertNil(command.body) - XCTAssertNil(command.data) let file2 = ParseFile(cloudURL: url) @@ -89,7 +88,6 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(command2.method, API.Method.POST) XCTAssertNil(command2.params) XCTAssertNil(command2.body) - XCTAssertNil(command2.data) } func testDeleteCommand() { @@ -105,7 +103,6 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(command.method, API.Method.DELETE) XCTAssertNil(command.params) XCTAssertNil(command.body) - XCTAssertNil(command.data) var file2 = ParseFile(cloudURL: url) file2.url = url @@ -115,7 +112,6 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(command2.method, API.Method.DELETE) XCTAssertNil(command2.params) XCTAssertNil(command2.body) - XCTAssertNil(command2.data) } func testDownloadCommand() { @@ -131,7 +127,6 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(command.method, API.Method.GET) XCTAssertNil(command.params) XCTAssertNil(command.body) - XCTAssertNil(command.data) let file2 = ParseFile(cloudURL: url) let command2 = file2.downloadFileCommand() @@ -140,7 +135,6 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(command2.method, API.Method.GET) XCTAssertNil(command2.params) XCTAssertNil(command2.body) - XCTAssertNil(command2.data) } func testLocalUUID() throws { @@ -148,11 +142,11 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length throw ParseError(code: .unknownError, message: "Should have converted to data") } let parseFile = ParseFile(name: "sampleData.txt", data: sampleData) - let localUUID = parseFile._localUUID - XCTAssertNotNil(localUUID) - XCTAssertEqual(localUUID, - parseFile._localUUID, - "localUUID should remain the same no matter how many times the getter is called") + let localId = parseFile.localId + XCTAssertNotNil(localId) + XCTAssertEqual(localId, + parseFile.localId, + "localId should remain the same no matter how many times the getter is called") } func testFileEquality() throws { @@ -171,15 +165,15 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length parseFile2.url = url2 var parseFile3 = ParseFile(name: "sampleData3.txt", data: sampleData) parseFile3.url = url1 - XCTAssertNotEqual(parseFile1, parseFile2, "different urls, url takes precedence over localUUID") + XCTAssertNotEqual(parseFile1, parseFile2, "different urls, url takes precedence over localId") XCTAssertEqual(parseFile1, parseFile3, "same urls") parseFile1.url = nil parseFile2.url = nil - XCTAssertNotEqual(parseFile1, parseFile2, "no urls, but localUUIDs shoud be different") + XCTAssertNotEqual(parseFile1, parseFile2, "no urls, but localIds shoud be different") let uuid = UUID() - parseFile1._localUUID = uuid - parseFile2._localUUID = uuid - XCTAssertEqual(parseFile1, parseFile2, "no urls, but localUUIDs shoud be the same") + parseFile1.localId = uuid + parseFile2.localId = uuid + XCTAssertEqual(parseFile1, parseFile2, "no urls, but localIds shoud be the same") } func testSave() throws { @@ -384,7 +378,7 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } try sampleData.write(to: tempFilePath) - let parseFile = ParseFile(name: "sampleData.data") + let parseFile = ParseFile(name: "sampleData.data", localURL: tempFilePath) // swiftlint:disable:next line_length guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { @@ -416,7 +410,7 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } try sampleData.write(to: tempFilePath) - let parseFile = ParseFile(name: "sampleData.data") + let parseFile = ParseFile(name: "sampleData.data", localURL: tempFilePath) // swiftlint:disable:next line_length guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { @@ -452,7 +446,7 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } try sampleData.write(to: tempFilePath) - let parseFile = ParseFile(name: "sampleData.data") + let parseFile = ParseFile(name: "sampleData.data", localURL: tempFilePath) // swiftlint:disable:next line_length guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { @@ -549,7 +543,7 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } try sampleData.write(to: tempFilePath) - let parseFile = ParseFile(name: "sampleData.data") + let parseFile = ParseFile(name: "sampleData.data", localURL: tempFilePath) // swiftlint:disable:next line_length guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { diff --git a/Tests/ParseSwiftTests/ParseGeoPointTests.swift b/Tests/ParseSwiftTests/ParseGeoPointTests.swift index 59ec25b9e..1df32312c 100644 --- a/Tests/ParseSwiftTests/ParseGeoPointTests.swift +++ b/Tests/ParseSwiftTests/ParseGeoPointTests.swift @@ -52,7 +52,7 @@ class ParseGeoPointTests: XCTestCase { let point = ParseGeoPoint(latitude: 10, longitude: 20) do { - let encoded = try ParseEncoder().encode(point) + let encoded = try ParseCoding.jsonEncoder().encode(point) let decoded = try JSONDecoder().decode(ParseGeoPoint.self, from: encoded) XCTAssertEqual(point, decoded) } catch { diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index 6db521970..6bfafa4f8 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -108,7 +108,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -304,7 +304,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installationOnServer.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) } catch { @@ -429,11 +429,11 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l installationOnServer.updatedAt = Date() let encoded: Data! do { - let encodedOriginal = try installation.getEncoder(skipKeys: false).encode(installation) + let encodedOriginal = try ParseCoding.jsonEncoder().encode(installation) //Get dates in correct format from ParseDecoding strategy installation = try installation.getDecoder().decode(Installation.self, from: encodedOriginal) - encoded = try installationOnServer.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) } catch { @@ -447,6 +447,25 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l self.updateAsync(installation: installation, installationOnServer: installationOnServer, callbackQueue: .main) } + func testFetchCommand() { + var installation = Installation() + let objectId = "yarr" + installation.objectId = objectId + do { + let command = try installation.fetchCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") + XCTAssertEqual(command.method, API.Method.GET) + XCTAssertNil(command.params) + XCTAssertNil(command.body) + } catch { + XCTFail(error.localizedDescription) + } + + let installation2 = Installation() + XCTAssertThrowsError(try installation2.fetchCommand()) + } + func testFetchUpdatedCurrentInstallation() { // swiftlint:disable:this function_body_length testUpdate() MockURLProtocol.removeAll() @@ -467,7 +486,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installationOnServer.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) } catch { @@ -545,7 +564,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installationOnServer.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) } catch { @@ -606,6 +625,25 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l wait(for: [expectation1], timeout: 20.0) } + func testDeleteCommand() { + var installation = Installation() + let objectId = "yarr" + installation.objectId = objectId + do { + let command = try installation.deleteCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") + XCTAssertEqual(command.method, API.Method.DELETE) + XCTAssertNil(command.params) + XCTAssertNil(command.body) + } catch { + XCTFail(error.localizedDescription) + } + + let installation2 = Installation() + XCTAssertThrowsError(try installation2.deleteCommand()) + } + func testDelete() { testUpdate() let expectation1 = XCTestExpectation(description: "Delete installation1") @@ -650,7 +688,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installationOnServer.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) } catch { @@ -690,9 +728,9 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + let encoded1 = try ParseCoding.jsonEncoder().encode(installation) installation = try installation.getDecoder().decode(Installation.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -776,9 +814,9 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + let encoded1 = try ParseCoding.jsonEncoder().encode(installation) installation = try installation.getDecoder().decode(Installation.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -846,6 +884,29 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l wait(for: [expectation1], timeout: 20.0) } + func testSaveCommand() { + let installation = Installation() + let command = installation.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/installations") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertNotNil(command.body) + } + + func testUpdateCommand() { + var installation = Installation() + let objectId = "yarr" + installation.objectId = objectId + + let command = installation.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.body) + } + // swiftlint:disable:next function_body_length func testSaveAll() { testUpdate() @@ -866,9 +927,9 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + let encoded1 = try ParseCoding.jsonEncoder().encode(installation) installation = try installation.getDecoder().decode(Installation.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -952,9 +1013,9 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + let encoded1 = try ParseCoding.jsonEncoder().encode(installation) installation = try installation.getDecoder().decode(Installation.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -1040,7 +1101,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() @@ -1083,7 +1144,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l let encoded: Data! do { - encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift old mode 100755 new mode 100644 index cf8dd177d..6af7be17d --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -71,11 +71,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le BatchResponseItem(success: scoreOnServer2, error: nil)] let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try scoreOnServer.getJSONEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -87,6 +87,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { + let saved = try [score, score2].saveAll() XCTAssertEqual(saved.count, 2) @@ -203,7 +204,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode([scoreOnServer, scoreOnServer2]) + let encoded = try ParseCoding.jsonEncoder().encode([scoreOnServer, scoreOnServer2]) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -255,11 +256,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le BatchResponseItem(success: scoreOnServer2, error: nil)] let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -380,7 +381,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode([scoreOnServer, scoreOnServer2]) + let encoded = try ParseCoding.jsonEncoder().encode([scoreOnServer, scoreOnServer2]) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -430,11 +431,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -685,11 +686,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le BatchResponseItem(success: scoreOnServer2, error: nil)] let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -726,11 +727,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le BatchResponseItem(success: scoreOnServer2, error: nil)] let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -896,11 +897,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -941,11 +942,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -980,11 +981,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2) let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -1155,11 +1156,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2) let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -1195,11 +1196,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2) let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) - let encoded2 = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer2) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) } catch { @@ -1214,13 +1215,12 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAll() { - let score = GameScore(score: 10) let error: ParseError? = nil let response = [error] let encoded: Data! do { - encoded = try score.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) } catch { XCTFail("Should have encoded/decoded. Error \(error)") return @@ -1248,12 +1248,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAllError() { - let score = GameScore(score: 10) let parseError = ParseError(code: .objectNotFound, message: "Object not found") let response = [parseError] let encoded: Data! do { - encoded = try score.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) } catch { XCTFail("Should have encoded/decoded. Error \(error)") return @@ -1313,12 +1312,11 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAllAsyncMainQueue() { - let score = GameScore(score: 10) let error: ParseError? = nil let response = [error] do { - let encoded = try score.getEncoder(skipKeys: false).encode(response) + let encoded = try ParseCoding.jsonEncoder().encode(response) MockURLProtocol.mockRequests { _ in return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } @@ -1363,13 +1361,12 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAllAsyncMainQueueError() { - let score = GameScore(score: 10) let parseError = ParseError(code: .objectNotFound, message: "Object not found") let response = [parseError] do { - let encoded = try score.getEncoder(skipKeys: false).encode(response) + let encoded = try ParseCoding.jsonEncoder().encode(response) MockURLProtocol.mockRequests { _ in return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index bc1afa50b..849c53ced 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -248,7 +248,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -311,7 +311,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -418,7 +418,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -446,7 +446,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -498,7 +498,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -562,7 +562,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -678,7 +678,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -704,7 +704,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -782,7 +782,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.updatedAt = Date() let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -809,7 +809,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.updatedAt = Date() let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -850,7 +850,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -883,7 +883,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try score.getEncoder(skipKeys: false).encode(parseError) + encoded = try ParseCoding.jsonEncoder().encode(parseError) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -953,7 +953,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -981,7 +981,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -1031,7 +1031,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let parseError = ParseError(code: .objectNotFound, message: "Object not found") let encoded: Data! do { - encoded = try score.getEncoder(skipKeys: false).encode(parseError) + encoded = try ParseCoding.jsonEncoder().encode(parseError) } catch { XCTFail("Should have encoded/decoded: Error: \(error)") return @@ -1055,7 +1055,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.objectId = "yarr" let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -1083,10 +1083,15 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertNil(parseError) + guard let savedChild = savedChildObject else { + XCTFail("Should have unwrapped child object") + return + } + //Saved updated info for game let encodedScore: Data do { - encodedScore = try game.getEncoder(skipKeys: false).encode(savedChildObject) + encodedScore = try ParseCoding.jsonEncoder().encode(savedChild) //Decode Pointer as GameScore game.score = try game.getDecoder().decode(GameScore.self, from: encodedScore) } catch { @@ -1104,7 +1109,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encodedGamed: Data do { - encodedGamed = try game.getEncoder(skipKeys: false).encode(gameOnServer) + encodedGamed = try game.getEncoder().encode(gameOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy gameOnServer = try game.getDecoder().decode(Game.self, from: encodedGamed) } catch { @@ -1154,15 +1159,15 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var game = Game(score: score) game.objectId = "nice" - var scoreOnServer = score - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - scoreOnServer.objectId = "yarr" - let pointer = scoreOnServer.toPointer() + var levelOnServer = score + levelOnServer.createdAt = Date() + levelOnServer.updatedAt = Date() + levelOnServer.ACL = nil + levelOnServer.objectId = "yarr" + let pointer = levelOnServer.toPointer() let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(pointer) + encoded = try ParseCoding.jsonEncoder().encode(pointer) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -1214,7 +1219,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length scoreOnServer.objectId = "yarr" let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) XCTFail("Should have thrown encode/decode error because child objects can't have the same objectId") @@ -1233,7 +1238,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length score.levels = [level1, level2] do { - let encoded = try score.getEncoder(skipKeys: false).encode(score) + let encoded = try score.getEncoder().encode(score, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy guard let scoreOnServer = try (score.getDecoder() .decode([String: AnyCodable].self, @@ -1274,7 +1279,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try game.getEncoder(skipKeys: false).encode(response) + encoded = try ParseCoding.jsonEncoder().encode(response) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -1314,7 +1319,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let encodedGamed: Data do { - encodedGamed = try game.getEncoder(skipKeys: false).encode(gameOnServer) + encodedGamed = try game.getEncoder().encode(gameOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy gameOnServer = try game.getDecoder().decode(Game2.self, from: encodedGamed) } catch { diff --git a/Tests/ParseSwiftTests/ParsePointerTests.swift b/Tests/ParseSwiftTests/ParsePointerTests.swift index f9f8afbe2..ba006e666 100644 --- a/Tests/ParseSwiftTests/ParsePointerTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerTests.swift @@ -71,7 +71,7 @@ class ParsePointerTests: XCTestCase { scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -195,7 +195,7 @@ class ParsePointerTests: XCTestCase { let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { @@ -224,7 +224,7 @@ class ParsePointerTests: XCTestCase { scoreOnServer.ACL = nil let encoded: Data! do { - encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer) + encoded = try scoreOnServer.getEncoder().encode(scoreOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) } catch { diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift old mode 100755 new mode 100644 index 6031248ed..153dad73b --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -77,6 +77,15 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(query4.`where`.constraints.values.count, 2) } + func testEndPoints() { + let query = Query() + let userQuery = Query() + let installationQuery = Query() + XCTAssertEqual(query.endpoint.urlComponent, "/classes/GameScore") + XCTAssertEqual(userQuery.endpoint.urlComponent, "/users") + XCTAssertEqual(installationQuery.endpoint.urlComponent, "/installations") + } + func testStaticProperties() { XCTAssertEqual(Query.className, GameScore.className) } @@ -180,7 +189,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -207,7 +216,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query("createdAt" > afterDate) let encodedJSON = try ParseCoding.jsonEncoder().encode(query) let decodedJSON = try ParseCoding.jsonDecoder().decode([String: AnyCodable].self, from: encodedJSON) - let encodedParse = try ParseCoding.parseEncoder().encode(query) + let encodedParse = try ParseCoding.jsonEncoder().encode(query) let decodedParse = try ParseCoding.jsonDecoder().decode([String: AnyCodable].self, from: encodedParse) guard let jsonSkipAny = decodedJSON["skip"], @@ -272,7 +281,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -294,7 +303,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -313,7 +322,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -335,11 +344,11 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } func testFirstNoObjectFound() { - let scoreOnServer = GameScore(score: 10) + let results = QueryResponse(results: [GameScore](), count: 0) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -409,7 +418,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -431,7 +440,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -445,7 +454,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [GameScore](), count: 0) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -462,7 +471,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [GameScore](), count: 0) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -481,7 +490,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -526,7 +535,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -548,7 +557,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let results = QueryResponse(results: [scoreOnServer], count: 1) MockURLProtocol.mockRequests { _ in do { - let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results) + let encoded = try ParseCoding.jsonEncoder().encode(results) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -567,7 +576,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -593,7 +602,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -618,7 +627,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -643,7 +652,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -668,7 +677,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -693,7 +702,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -718,7 +727,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -743,7 +752,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -769,7 +778,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -795,7 +804,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -824,7 +833,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -850,7 +859,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -876,7 +885,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -902,7 +911,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -933,7 +942,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -964,7 +973,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -996,7 +1005,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1042,7 +1051,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1086,7 +1095,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1125,7 +1134,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1160,7 +1169,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1186,7 +1195,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1212,7 +1221,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1242,7 +1251,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1280,7 +1289,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1321,7 +1330,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1367,7 +1376,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1413,7 +1422,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1463,7 +1472,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1521,7 +1530,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1583,7 +1592,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let queryWhere = query.`where` do { - let encoded = try ParseCoding.parseEncoder().encode(queryWhere) + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) @@ -1617,7 +1626,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length // MARK: JSON Responses func testExplainFindSynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1634,19 +1643,19 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query() do { let queryResult = try query.find(explain: true) - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) } catch { XCTFail("Error: \(error)") } } func testExplainFindAsynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1666,13 +1675,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length switch result { case .success(let queryResult): - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") expectation.fulfill() return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) case .failure(let error): XCTFail("Error: \(error)") } @@ -1682,7 +1691,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } func testExplainFirstSynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1699,19 +1708,19 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query() do { let queryResult = try query.first(explain: true) - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) } catch { XCTFail("Error: \(error)") } } func testExplainFirstAsynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1731,13 +1740,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length switch result { case .success(let queryResult): - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") expectation.fulfill() return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) case .failure(let error): XCTFail("Error: \(error)") } @@ -1747,7 +1756,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } func testExplainCountSynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1764,19 +1773,19 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query() do { let queryResult = try query.count(explain: true) - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) } catch { XCTFail("Error: \(error)") } } func testExplainCountAsynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1796,13 +1805,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length switch result { case .success(let queryResult): - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") expectation.fulfill() return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) case .failure(let error): XCTFail("Error: \(error)") } @@ -1812,7 +1821,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } func testHintFindSynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1829,19 +1838,19 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query() do { let queryResult = try query.find(explain: false, hint: "_id_") - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) } catch { XCTFail("Error: \(error)") } } func testHintFindAsynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1861,13 +1870,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length switch result { case .success(let queryResult): - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") expectation.fulfill() return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) case .failure(let error): XCTFail("Error: \(error)") } @@ -1877,7 +1886,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } func testHintFirstSynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1894,19 +1903,19 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query() do { let queryResult = try query.first(explain: false, hint: "_id_") - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) } catch { XCTFail("Error: \(error)") } } func testHintFirstAsynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1926,13 +1935,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length switch result { case .success(let queryResult): - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") expectation.fulfill() return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) case .failure(let error): XCTFail("Error: \(error)") } @@ -1942,7 +1951,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } func testHintCountSynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1959,19 +1968,19 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query() do { let queryResult = try query.count(explain: false, hint: "_id_") - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) } catch { XCTFail("Error: \(error)") } } func testHintCountAsynchronous() { - let json = ["yolo": "yarr"] + let json = AnyResultsResponse(results: ["yolo": "yarr"]) let encoded: Data! do { @@ -1991,13 +2000,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length switch result { case .success(let queryResult): - XCTAssertEqual(queryResult.keys.first, json.keys.first) - guard let valueString = queryResult.values.first?.value as? String else { + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { XCTFail("Error: Should cast to string") expectation.fulfill() return } - XCTAssertEqual(valueString, json.values.first) + XCTAssertEqual(response, expected) case .failure(let error): XCTFail("Error: \(error)") } diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index fae9a92cc..bbd2d50e5 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -79,13 +79,12 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testFetchCommand() { var user = User() - let className = user.className let objectId = "yarr" user.objectId = objectId do { let command = try user.fetchCommand() XCTAssertNotNil(command) - XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") XCTAssertEqual(command.method, API.Method.GET) XCTAssertNil(command.params) XCTAssertNil(command.body) @@ -93,6 +92,9 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } catch { XCTFail(error.localizedDescription) } + + let user2 = User() + XCTAssertThrowsError(try user2.fetchCommand()) } func testFetch() { // swiftlint:disable:this function_body_length @@ -106,7 +108,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length userOnServer.ACL = nil let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -159,7 +161,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } func testFetchAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length - testUserLogin() + testLogin() MockURLProtocol.removeAll() XCTAssertNotNil(User.current?.objectId) @@ -175,7 +177,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -223,7 +225,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testFetchAsyncAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) - testUserLogin() + testLogin() MockURLProtocol.removeAll() XCTAssertNotNil(User.current?.objectId) @@ -239,7 +241,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -361,7 +363,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length userOnServer.ACL = nil let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -379,11 +381,10 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testSaveCommand() { let user = User() - let className = user.className let command = user.saveCommand() XCTAssertNotNil(command) - XCTAssertEqual(command.path.urlComponent, "/classes/\(className)") + XCTAssertEqual(command.path.urlComponent, "/users") XCTAssertEqual(command.method, API.Method.POST) XCTAssertNil(command.params) XCTAssertNotNil(command.body) @@ -392,13 +393,12 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testUpdateCommand() { var user = User() - let className = user.className let objectId = "yarr" user.objectId = objectId let command = user.saveCommand() XCTAssertNotNil(command) - XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) XCTAssertNil(command.params) XCTAssertNotNil(command.body) @@ -407,7 +407,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testSaveAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) - testUserLogin() + testLogin() MockURLProtocol.removeAll() XCTAssertNotNil(User.current?.objectId) @@ -422,7 +422,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -468,7 +468,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testSaveAsyncAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) - testUserLogin() + testLogin() MockURLProtocol.removeAll() XCTAssertNotNil(User.current?.objectId) @@ -483,7 +483,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -545,7 +545,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -653,7 +653,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length userOnServer.updatedAt = Date() let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -681,7 +681,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length userOnServer.updatedAt = Date() let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -695,12 +695,23 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length self.updateAsync(user: user, userOnServer: userOnServer, callbackQueue: .main) } + func testSignupCommandWithBody() { + let body = SignupBody(username: "test", password: "user") + let command = User.signupCommand(username: "test", password: "user") + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertEqual(command.body?.username, body.username) + XCTAssertEqual(command.body?.password, body.password) + } + func testUserSignUp() { let loginResponse = LoginSignupResponse() MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -782,7 +793,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -792,12 +803,25 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length self.signUpAsync(loginResponse: loginResponse, callbackQueue: .main) } - func testUserLogin() { + func testLoginCommand() { + let params = [ + "username": "test", + "password": "user" + ] + let command = User.loginCommand(username: "test", password: "user") + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/login") + XCTAssertEqual(command.method, API.Method.GET) + XCTAssertEqual(command.params, params) + XCTAssertNil(command.body) + } + + func testLogin() { let loginResponse = LoginSignupResponse() MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -835,7 +859,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func userLoginAsync(loginResponse: LoginSignupResponse, callbackQueue: DispatchQueue) { + func loginAsync(loginResponse: LoginSignupResponse, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Login user") User.login(username: loginUserName, password: loginPassword, @@ -880,22 +904,31 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil } } - self.userLoginAsync(loginResponse: loginResponse, callbackQueue: .main) + self.loginAsync(loginResponse: loginResponse, callbackQueue: .main) } - func testUserLogout() { - let loginResponse = LoginSignupResponse() + func testLogutCommand() { + let command = User.logoutCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/logout") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertNil(command.body) + } + + func testLogout() { + let logoutResponse = NoBody() MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try ParseCoding.jsonEncoder().encode(logoutResponse) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -916,31 +949,27 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func logoutAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.logout(callbackQueue: callbackQueue) { result in - - switch result { + User.logout(callbackQueue: callbackQueue) { error in - case .success(let success): - XCTAssertTrue(success) + guard let error = error else { if let userFromKeychain = BaseParseUser.current { XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") - expectation1.fulfill() - return } - case .failure(let error): - XCTFail(error.localizedDescription) + expectation1.fulfill() + return } + XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 20.0) } func testLogoutAsyncMainQueue() { - let loginResponse = LoginSignupResponse() + let logoutResponse = NoBody() MockURLProtocol.mockRequests { _ in do { - let encoded = try loginResponse.getEncoder(skipKeys: false).encode(loginResponse) + let encoded = try ParseCoding.jsonEncoder().encode(logoutResponse) return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } catch { return nil @@ -950,8 +979,240 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length self.logoutAsync(callbackQueue: .main) } + func testPasswordResetCommand() throws { + let body = EmailBody(email: "hello@parse.org") + let command = User.passwordResetCommand(email: body.email) + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/requestPasswordReset") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertEqual(command.body?.email, body.email) + } + + func testPasswordReset() { + let response = NoBody() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + try User.passwordReset(email: "hello@parse.org") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testPasswordResetError() { + + let parseError = ParseError(code: .internalServer, message: "Object not found") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(parseError) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + try User.passwordReset(email: "hello@parse.org") + XCTFail("Should have thrown ParseError") + } catch { + if let error = error as? ParseError { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + } + } + + func passwordResetAsync(callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + + guard let error = error else { + expectation1.fulfill() + return + } + XCTFail(error.localizedDescription) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testPasswordResetMainQueue() { + let response = NoBody() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.passwordResetAsync(callbackQueue: .main) + } + + func passwordResetAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + + guard let error = error else { + XCTFail("Should have thrown ParseError") + expectation1.fulfill() + return + } + XCTAssertEqual(error.code, parseError.code) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testPasswordResetMainQueueError() { + let parseError = ParseError(code: .internalServer, message: "Object not found") + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.passwordResetAsyncError(parseError: parseError, callbackQueue: .main) + } + + func testVerificationEmailRequestCommand() throws { + let body = EmailBody(email: "hello@parse.org") + let command = User.verificationEmailRequestCommand(email: body.email) + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/verificationEmailRequest") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertEqual(command.body?.email, body.email) + } + + func testVerificationEmailRequestReset() { + let response = NoBody() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + try User.verificationEmailRequest(email: "hello@parse.org") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testVerificationEmailRequestError() { + + let parseError = ParseError(code: .internalServer, message: "Object not found") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(parseError) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + try User.verificationEmailRequest(email: "hello@parse.org") + XCTFail("Should have thrown ParseError") + } catch { + if let error = error as? ParseError { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + } + } + + func verificationEmailRequestAsync(callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + User.verificationEmailRequest(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + + guard let error = error else { + expectation1.fulfill() + return + } + XCTFail(error.localizedDescription) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testVerificationEmailRequestMainQueue() { + let response = NoBody() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.verificationEmailRequestAsync(callbackQueue: .main) + } + + func verificationEmailRequestAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + User.verificationEmailRequest(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + + guard let error = error else { + XCTFail("Should have thrown ParseError") + expectation1.fulfill() + return + } + XCTAssertEqual(error.code, parseError.code) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testVerificationEmailRequestMainQueueError() { + let parseError = ParseError(code: .internalServer, message: "Object not found") + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.verificationEmailRequestAsyncError(parseError: parseError, callbackQueue: .main) + } + func testUserCustomValuesNotSavedToKeychain() { - testUserLogin() + testLogin() User.current?.customKey = "Changed" User.saveCurrentContainerToKeychain() guard let keychainUser: CurrentUserContainer @@ -962,8 +1223,27 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNil(keychainUser.currentUser?.customKey) } + func testDeleteCommand() { + var user = User() + let objectId = "yarr" + user.objectId = objectId + do { + let command = try user.deleteCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.method, API.Method.DELETE) + XCTAssertNil(command.params) + XCTAssertNil(command.body) + } catch { + XCTFail(error.localizedDescription) + } + + let user2 = User() + XCTAssertThrowsError(try user2.deleteCommand()) + } + func testDelete() { - testUserLogin() + testLogin() let expectation1 = XCTestExpectation(description: "Delete installation1") DispatchQueue.main.async { guard let user = User.current else { @@ -990,7 +1270,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } func testDeleteAsyncMainQueue() { - testUserLogin() + testLogin() MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Delete installation1") @@ -1006,7 +1286,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) //Get dates in correct format from ParseDecoding strategy userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) } catch { @@ -1028,7 +1308,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length // swiftlint:disable:next function_body_length func testFetchAll() { - testUserLogin() + testLogin() MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Fetch user1") @@ -1046,9 +1326,9 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + let encoded1 = try ParseCoding.jsonEncoder().encode(user) user = try user.getDecoder().decode(User.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -1115,7 +1395,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length // swiftlint:disable:next function_body_length func testFetchAllAsyncMainQueue() { - testUserLogin() + testLogin() MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Fetch user1") @@ -1132,9 +1412,9 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + let encoded1 = try ParseCoding.jsonEncoder().encode(user) user = try user.getDecoder().decode(User.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -1203,7 +1483,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length // swiftlint:disable:next function_body_length func testSaveAll() { - testUserLogin() + testLogin() MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Fetch user1") @@ -1221,9 +1501,9 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + let encoded1 = try ParseCoding.jsonEncoder().encode(user) user = try user.getDecoder().decode(User.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -1290,7 +1570,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length // swiftlint:disable:next function_body_length func testSaveAllAsyncMainQueue() { - testUserLogin() + testLogin() MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Fetch user1") @@ -1307,9 +1587,9 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) //Get dates in correct format from ParseDecoding strategy - let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + let encoded1 = try ParseCoding.jsonEncoder().encode(user) user = try user.getDecoder().decode(User.self, from: encoded1) } catch { XCTFail("Should encode/decode. Error \(error)") @@ -1377,7 +1657,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } func testDeleteAll() { - testUserLogin() + testLogin() MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Delete user1") @@ -1394,7 +1674,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill() @@ -1421,7 +1701,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } func testDeleteAllAsyncMainQueue() { - testUserLogin() + testLogin() MockURLProtocol.removeAll() let expectation1 = XCTestExpectation(description: "Delete user1") @@ -1437,7 +1717,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length let encoded: Data! do { - encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) } catch { XCTFail("Should encode/decode. Error \(error)") expectation1.fulfill()