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