diff --git a/.gitignore b/.gitignore
index f4d4ee2ba..8abe4fa3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@ docs/
xcuserdata/
## Other
+.DS_Store
*.moved-aside
*.xccheckout
*.xcscmblueprint
diff --git a/.spi.yml b/.spi.yml
index dcd7b5431..c47517008 100644
--- a/.spi.yml
+++ b/.spi.yml
@@ -3,6 +3,10 @@ builder:
configs:
- platform: ios
scheme: "ParseSwift (iOS)"
+ - platform: macos-xcodebuild
+ scheme: "ParseSwift (macOS)"
+ - platform: macos-xcodebuild-arm
+ scheme: "ParseSwift (macOS)"
- platform: tvos
scheme: "ParseSwift (tvOS)"
- platform: watchos
diff --git a/Package.swift b/Package.swift
index 1db075989..186d28bb5 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.1
+// swift-tools-version:5.0
import PackageDescription
diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift
index 35cb51231..84fab153a 100644
--- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift
+++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift
@@ -235,14 +235,12 @@ do {
[scoreToFetch, score2ToFetch].deleteAll { result in
switch result {
case .success(let deletedScores):
-
- deletedScores.forEach { result in
- switch result {
- case .success(let deleted):
- print("Successfully deleted: \(deleted)")
- case .failure(let error):
- print("Error deleting: \(error)")
+ deletedScores.forEach { error in
+ guard let error = error else {
+ print("Successfully deleted scores")
+ return
}
+ print("Error deleting: \(error)")
}
case .failure(let error):
assertionFailure("Error deleting: \(error)")
diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift
index 0aa83bae3..7feef8b30 100644
--- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift
+++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift
@@ -34,6 +34,7 @@ query.limit(2).find(callbackQueue: .main) { results in
scores.forEach { (score) in
guard let createdAt = score.createdAt else { fatalError() }
assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok")
+ print("Found score: \(score)")
}
case .failure(let error):
@@ -47,6 +48,7 @@ assert(results.count >= 1)
results.forEach { (score) in
guard let createdAt = score.createdAt else { fatalError() }
assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok")
+ print("Found score: \(score)")
}
// Query first asynchronously (preferred way) - Performs work on background
@@ -59,7 +61,7 @@ query.first { results in
guard let objectId = score.objectId,
let createdAt = score.createdAt else { fatalError() }
assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok")
- print(objectId)
+ print("Found score: \(score)")
case .failure(let error):
assertionFailure("Error querying: \(error)")
diff --git a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift
index 92e99f5a7..269a78b20 100644
--- a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift
+++ b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift
@@ -30,12 +30,14 @@ struct GameScore: ParseObject {
//: a custom initializer
init(score: Int) {
self.score = score
- self.ACL = try? ParseACL.defaultACL()
}
}
//: Define initial GameScores
-let score = GameScore(score: 40)
+var score = GameScore(score: 40)
+
+//: Set the ACL to default for your GameScore
+score.ACL = try? ParseACL.defaultACL()
/*: Save asynchronously (preferred way) - Performs work on background
queue and returns to designated on designated callbackQueue.
@@ -47,8 +49,10 @@ score.save { result in
assert(savedScore.objectId != nil)
assert(savedScore.createdAt != nil)
assert(savedScore.updatedAt != nil)
- assert(savedScore.ACL != nil)
assert(savedScore.score == 40)
+ assert(savedScore.ACL != nil)
+
+ print("Saved score with ACL: \(savedScore)")
case .failure(let error):
assertionFailure("Error saving: \(error)")
diff --git a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift
new file mode 100644
index 000000000..8a4cde3a3
--- /dev/null
+++ b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift
@@ -0,0 +1,154 @@
+//: [Previous](@previous)
+
+import PlaygroundSupport
+import Foundation
+import ParseSwift
+PlaygroundPage.current.needsIndefiniteExecution = true
+
+initializeParse()
+
+//: Create your own ValueTyped ParseObject
+struct GameScore: ParseObject {
+ //: Those are required for Object
+ var objectId: String?
+ var createdAt: Date?
+ var updatedAt: Date?
+ var ACL: ParseACL?
+
+ //: Your own properties
+ var score: Int = 0
+ var profilePicture: ParseFile?
+ var myData: ParseFile?
+
+ //custom initializer
+ init(score: Int) {
+ self.score = score
+ }
+
+ init(objectId: String?) {
+ self.objectId = objectId
+ }
+}
+
+//: Define initial GameScore
+var score = GameScore(score: 52)
+
+//: Set the link online for the file
+let linkToFile = URL(string: "https://parseplatform.org/img/logo.svg")!
+
+//: Create a new ParseFile for your picture
+let profilePic = ParseFile(name: "profile.svg", cloudURL: linkToFile)
+
+//: Set the picture as part of your ParseObject
+score.profilePicture = profilePic
+
+/*: Save asynchronously (preferred way) - Performs work on background
+ queue and returns to designated on designated callbackQueue.
+ If no callbackQueue is specified it returns to main queue.
+*/
+score.save { result in
+ switch result {
+ case .success(let savedScore):
+ assert(savedScore.objectId != nil)
+ assert(savedScore.createdAt != nil)
+ assert(savedScore.updatedAt != nil)
+ assert(savedScore.ACL == nil)
+ assert(savedScore.score == 52)
+ assert(savedScore.profilePicture != nil)
+
+ print("Your profile picture has been successfully saved")
+
+ //: To get the contents updated ParseFile, you need to fetch your GameScore
+ savedScore.fetch { result in
+ switch result {
+ case .success(let fetchedScore):
+ guard let picture = fetchedScore.profilePicture,
+ let url = fetchedScore.profilePicture?.url else {
+ return
+ }
+ print("The new name of your saved profilePicture is: \(picture.name)")
+ print("The profilePicture is saved to your Parse Server at: \(url)")
+ print("The full details of your profilePicture ParseFile are: \(picture)")
+
+ //: If you need to download your profilePicture
+ picture.fetch { result in
+ switch result {
+ case .success(let fetchedFile):
+ print("The file is now saved on your device at: \(fetchedFile.localURL)")
+ print("The full details of your profilePicture ParseFile are: \(fetchedFile)")
+ case .failure(let error):
+ assertionFailure("Error fetching: \(error)")
+ }
+ }
+
+ case .failure(let error):
+ assertionFailure("Error fetching: \(error)")
+ }
+ }
+ case .failure(let error):
+ assertionFailure("Error saving: \(error)")
+ }
+}
+
+/*: Files can also be saved from data. Below is how to do it synchrously, but async is similar to above
+ Create a new ParseFile for your data
+ */
+let sampleData = "Hello World".data(using: .utf8)!
+let helloFile = ParseFile(name: "hello.txt", data: sampleData)
+
+//: Define another GameScore
+var score2 = GameScore(score: 105)
+score2.myData = helloFile
+
+//: Save synchronously (not preferred - all operations on main queue)
+do {
+ let savedScore = try score2.save()
+ print("Your hello file has been successfully saved")
+
+ //: To get the contents updated ParseFile, you need to fetch your GameScore
+ let fetchedScore = try savedScore.fetch()
+ if var myData = fetchedScore.myData {
+
+ guard let url = myData.url else {
+ fatalError("Error: file should have url.")
+ }
+ print("The new name of your saved data is: \(myData.name)")
+ 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
+ let fetchedFile = try myData.fetch()
+ if fetchedFile.localURL != nil {
+ print("The file is now saved at: \(fetchedFile.localURL!)")
+ print("The full details of your data ParseFile are: \(fetchedFile)")
+
+ /*: If you want to use the data from the file to display the text file or image, you need to retreive
+ the data from the file.
+ */
+ guard let dataFromParseFile = try? Data(contentsOf: fetchedFile.localURL!) else {
+ fatalError("Error: couldn't get data from file.")
+ }
+
+ //: Checking to make sure the data saved on the Parse Server is the same as the original
+ if dataFromParseFile != sampleData {
+ assertionFailure("Data isn't the same. Something went wrong.")
+ }
+
+ guard let parseFileString = String(data: dataFromParseFile, encoding: .utf8) else {
+ fatalError("Error: couldn't create String from data.")
+ }
+ print("The data saved on parse is: \"\(parseFileString)\"")
+ } else {
+ assertionFailure("Error fetching: there should be a localURL")
+ }
+ } else {
+ assertionFailure("Error fetching: there should be a localURL")
+ }
+} catch {
+ fatalError("Error saving: \(error)")
+}
+
+/*: Files can also be saved from files located on your device by using:
+ let localFile = ParseFile(name: "hello.txt", localURL: URL)
+*/
+//: [Next](@next)
diff --git a/ParseSwift.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground
index bf7611d1f..dba126537 100644
--- a/ParseSwift.playground/contents.xcplayground
+++ b/ParseSwift.playground/contents.xcplayground
@@ -9,5 +9,6 @@
+
\ No newline at end of file
diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj
index b8d7ced8c..02b0435ad 100644
--- a/ParseSwift.xcodeproj/project.pbxproj
+++ b/ParseSwift.xcodeproj/project.pbxproj
@@ -31,6 +31,27 @@
7033ECB825584A83009770F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB625584A83009770F3 /* Main.storyboard */; };
7033ECBA25584A85009770F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB925584A85009770F3 /* Assets.xcassets */; };
7033ECBD25584A85009770F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECBB25584A85009770F3 /* LaunchScreen.storyboard */; };
+ 70572671259033A700F0ADD5 /* ParseFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70572670259033A700F0ADD5 /* ParseFileManager.swift */; };
+ 70572672259033A700F0ADD5 /* ParseFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70572670259033A700F0ADD5 /* ParseFileManager.swift */; };
+ 70572673259033A700F0ADD5 /* ParseFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70572670259033A700F0ADD5 /* ParseFileManager.swift */; };
+ 70572674259033A700F0ADD5 /* ParseFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70572670259033A700F0ADD5 /* ParseFileManager.swift */; };
+ 705726E02592C2A800F0ADD5 /* ParseHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705726DF2592C2A800F0ADD5 /* ParseHash.swift */; };
+ 705726E12592C2A800F0ADD5 /* ParseHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705726DF2592C2A800F0ADD5 /* ParseHash.swift */; };
+ 705726E22592C2A800F0ADD5 /* ParseHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705726DF2592C2A800F0ADD5 /* ParseHash.swift */; };
+ 705726E32592C2A800F0ADD5 /* ParseHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705726DF2592C2A800F0ADD5 /* ParseHash.swift */; };
+ 705727262592CBAF00F0ADD5 /* HashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705726ED2592C91C00F0ADD5 /* HashTests.swift */; };
+ 705727302592CBB000F0ADD5 /* HashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705726ED2592C91C00F0ADD5 /* HashTests.swift */; };
+ 7057273A2592CBB100F0ADD5 /* HashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705726ED2592C91C00F0ADD5 /* HashTests.swift */; };
+ 705727B12593FF8800F0ADD5 /* ParseFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705727882593FF8000F0ADD5 /* ParseFileTests.swift */; };
+ 705727BB2593FF8B00F0ADD5 /* ParseFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705727882593FF8000F0ADD5 /* ParseFileTests.swift */; };
+ 705727BC2593FF8C00F0ADD5 /* ParseFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705727882593FF8000F0ADD5 /* ParseFileTests.swift */; };
+ 705A99F9259807F900B3547F /* ParseFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */; };
+ 705A99FA259807F900B3547F /* ParseFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */; };
+ 705A99FB259807F900B3547F /* ParseFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */; };
+ 705A9A2F25991C1400B3547F /* Fileable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705A9A2E25991C1400B3547F /* Fileable.swift */; };
+ 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 */; };
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 */; };
@@ -143,10 +164,10 @@
F97B45FB24D9C6F200F4A88B /* ParseACL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C024D9C6F200F4A88B /* ParseACL.swift */; };
F97B45FC24D9C6F200F4A88B /* ParseACL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C024D9C6F200F4A88B /* ParseACL.swift */; };
F97B45FD24D9C6F200F4A88B /* ParseACL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C024D9C6F200F4A88B /* ParseACL.swift */; };
- F97B45FE24D9C6F200F4A88B /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* File.swift */; };
- F97B45FF24D9C6F200F4A88B /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* File.swift */; };
- F97B460024D9C6F200F4A88B /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* File.swift */; };
- F97B460124D9C6F200F4A88B /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* File.swift */; };
+ F97B45FE24D9C6F200F4A88B /* ParseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* ParseFile.swift */; };
+ F97B45FF24D9C6F200F4A88B /* ParseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* ParseFile.swift */; };
+ F97B460024D9C6F200F4A88B /* ParseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* ParseFile.swift */; };
+ F97B460124D9C6F200F4A88B /* ParseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C124D9C6F200F4A88B /* ParseFile.swift */; };
F97B460224D9C6F200F4A88B /* NoBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C224D9C6F200F4A88B /* NoBody.swift */; };
F97B460324D9C6F200F4A88B /* NoBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C224D9C6F200F4A88B /* NoBody.swift */; };
F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C224D9C6F200F4A88B /* NoBody.swift */; };
@@ -163,10 +184,10 @@
F97B460F24D9C6F200F4A88B /* ParseObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C624D9C6F200F4A88B /* ParseObject.swift */; };
F97B461024D9C6F200F4A88B /* ParseObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C624D9C6F200F4A88B /* ParseObject.swift */; };
F97B461124D9C6F200F4A88B /* ParseObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C624D9C6F200F4A88B /* ParseObject.swift */; };
- F97B461224D9C6F200F4A88B /* Saveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Saveable.swift */; };
- F97B461324D9C6F200F4A88B /* Saveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Saveable.swift */; };
- F97B461424D9C6F200F4A88B /* Saveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Saveable.swift */; };
- F97B461524D9C6F200F4A88B /* Saveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Saveable.swift */; };
+ F97B461224D9C6F200F4A88B /* Savable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Savable.swift */; };
+ F97B461324D9C6F200F4A88B /* Savable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Savable.swift */; };
+ F97B461424D9C6F200F4A88B /* Savable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Savable.swift */; };
+ F97B461524D9C6F200F4A88B /* Savable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C724D9C6F200F4A88B /* Savable.swift */; };
F97B461624D9C6F200F4A88B /* Queryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C824D9C6F200F4A88B /* Queryable.swift */; };
F97B461724D9C6F200F4A88B /* Queryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C824D9C6F200F4A88B /* Queryable.swift */; };
F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45C824D9C6F200F4A88B /* Queryable.swift */; };
@@ -231,10 +252,6 @@
F97B466524D9C88600F4A88B /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B466324D9C88600F4A88B /* SecureStorage.swift */; };
F97B466624D9C88600F4A88B /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B466324D9C88600F4A88B /* SecureStorage.swift */; };
F97B466724D9C88600F4A88B /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B466324D9C88600F4A88B /* SecureStorage.swift */; };
- F97B466924D9C8C600F4A88B /* FindResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B466824D9C8C600F4A88B /* FindResult.swift */; };
- F97B466A24D9C8C600F4A88B /* FindResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B466824D9C8C600F4A88B /* FindResult.swift */; };
- F97B466B24D9C8C600F4A88B /* FindResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B466824D9C8C600F4A88B /* FindResult.swift */; };
- F97B466C24D9C8C600F4A88B /* FindResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B466824D9C8C600F4A88B /* FindResult.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -303,6 +320,12 @@
7033ECB925584A85009770F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
7033ECBC25584A85009770F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
7033ECBE25584A85009770F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 70572670259033A700F0ADD5 /* ParseFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileManager.swift; sourceTree = ""; };
+ 705726DF2592C2A800F0ADD5 /* ParseHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseHash.swift; sourceTree = ""; };
+ 705726ED2592C91C00F0ADD5 /* HashTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashTests.swift; sourceTree = ""; };
+ 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 = ""; };
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 = ""; };
@@ -344,12 +367,12 @@
F97B45BE24D9C6F200F4A88B /* Pointer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pointer.swift; sourceTree = ""; };
F97B45BF24D9C6F200F4A88B /* ParseError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseError.swift; sourceTree = ""; };
F97B45C024D9C6F200F4A88B /* ParseACL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseACL.swift; sourceTree = ""; };
- F97B45C124D9C6F200F4A88B /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; };
+ F97B45C124D9C6F200F4A88B /* ParseFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseFile.swift; sourceTree = ""; };
F97B45C224D9C6F200F4A88B /* NoBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoBody.swift; sourceTree = ""; };
F97B45C424D9C6F200F4A88B /* ParseUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseUser.swift; sourceTree = ""; };
F97B45C524D9C6F200F4A88B /* Fetchable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = ""; };
F97B45C624D9C6F200F4A88B /* ParseObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseObject.swift; sourceTree = ""; };
- F97B45C724D9C6F200F4A88B /* Saveable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Saveable.swift; sourceTree = ""; };
+ F97B45C724D9C6F200F4A88B /* Savable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Savable.swift; sourceTree = ""; };
F97B45C824D9C6F200F4A88B /* Queryable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queryable.swift; sourceTree = ""; };
F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseStorage.swift; sourceTree = ""; };
F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrimitiveObjectStore.swift; sourceTree = ""; };
@@ -366,7 +389,6 @@
F97B464524D9C78B00F4A88B /* IncrementOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncrementOperation.swift; sourceTree = ""; };
F97B465E24D9C7B500F4A88B /* KeychainStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = ""; };
F97B466324D9C88600F4A88B /* SecureStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureStorage.swift; sourceTree = ""; };
- F97B466824D9C8C600F4A88B /* FindResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindResult.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -481,6 +503,7 @@
children = (
4AA8076D1F794C1C008CD551 /* Info.plist */,
9194657724F16E330070296B /* ACLTests.swift */,
+ 705726ED2592C91C00F0ADD5 /* HashTests.swift */,
911DB12D24C4837E0027F3C7 /* APICommandTests.swift */,
4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */,
F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */,
@@ -490,6 +513,8 @@
911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */,
70CE1D882545BF730018D572 /* ParsePointerTests.swift */,
70C7DC1F24D20F180050419B /* ParseQueryTests.swift */,
+ 705727882593FF8000F0ADD5 /* ParseFileTests.swift */,
+ 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */,
70C7DC1D24D20E530050419B /* ParseUserTests.swift */,
7FFF552A2217E729007C3B4E /* AnyCodableTests */,
911DB12A24C3F7260027F3C7 /* NetworkMocking */,
@@ -560,9 +585,9 @@
70110D5D250849B30091CC1D /* Internal */ = {
isa = PBXGroup;
children = (
+ 705726DF2592C2A800F0ADD5 /* ParseHash.swift */,
F97B45BD24D9C6F200F4A88B /* BaseParseUser.swift */,
70110D562506CE890091CC1D /* BaseParseInstallation.swift */,
- F97B466824D9C8C600F4A88B /* FindResult.swift */,
F97B45C224D9C6F200F4A88B /* NoBody.swift */,
);
path = Internal;
@@ -571,11 +596,12 @@
70110D5E25084AF80091CC1D /* Protocols */ = {
isa = PBXGroup;
children = (
- F97B45C524D9C6F200F4A88B /* Fetchable.swift */,
- F97B45C824D9C6F200F4A88B /* Queryable.swift */,
- F97B45C724D9C6F200F4A88B /* Saveable.swift */,
708D035125215F9B00646C70 /* Deletable.swift */,
+ F97B45C524D9C6F200F4A88B /* Fetchable.swift */,
70BC988F252A5B5C00FF3074 /* Objectable.swift */,
+ F97B45C824D9C6F200F4A88B /* Queryable.swift */,
+ F97B45C724D9C6F200F4A88B /* Savable.swift */,
+ 705A9A2E25991C1400B3547F /* Fileable.swift */,
);
path = Protocols;
sourceTree = "";
@@ -657,12 +683,12 @@
F97B45B324D9C6F200F4A88B /* Coding */ = {
isa = PBXGroup;
children = (
- F97B45B424D9C6F200F4A88B /* ParseCoding.swift */,
- F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */,
- F97B45B624D9C6F200F4A88B /* ParseEncoder.swift */,
- F97B45B724D9C6F200F4A88B /* Extensions.swift */,
F97B45B824D9C6F200F4A88B /* AnyCodable.swift */,
+ F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */,
F97B45B924D9C6F200F4A88B /* AnyEncodable.swift */,
+ F97B45B724D9C6F200F4A88B /* Extensions.swift */,
+ F97B45B424D9C6F200F4A88B /* ParseCoding.swift */,
+ F97B45B624D9C6F200F4A88B /* ParseEncoder.swift */,
);
path = Coding;
sourceTree = "";
@@ -671,9 +697,9 @@
isa = PBXGroup;
children = (
F97B45C024D9C6F200F4A88B /* ParseACL.swift */,
- F97B45C124D9C6F200F4A88B /* File.swift */,
- F97B45BC24D9C6F200F4A88B /* ParseGeoPoint.swift */,
F97B45BF24D9C6F200F4A88B /* ParseError.swift */,
+ F97B45C124D9C6F200F4A88B /* ParseFile.swift */,
+ F97B45BC24D9C6F200F4A88B /* ParseGeoPoint.swift */,
F97B45BE24D9C6F200F4A88B /* Pointer.swift */,
F97B45BB24D9C6F200F4A88B /* Query.swift */,
70110D5D250849B30091CC1D /* Internal */,
@@ -708,6 +734,7 @@
isa = PBXGroup;
children = (
F97B465E24D9C7B500F4A88B /* KeychainStore.swift */,
+ 70572670259033A700F0ADD5 /* ParseFileManager.swift */,
F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */,
F97B45CD24D9C6F200F4A88B /* PrimitiveObjectStore.swift */,
F97B466324D9C88600F4A88B /* SecureStorage.swift */,
@@ -718,12 +745,12 @@
F97B463F24D9C78B00F4A88B /* Mutation Operations */ = {
isa = PBXGroup;
children = (
- F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */,
- F97B464124D9C78B00F4A88B /* DeleteOperation.swift */,
F97B464224D9C78B00F4A88B /* AddOperation.swift */,
F97B464324D9C78B00F4A88B /* AddUniqueOperation.swift */,
- F97B464424D9C78B00F4A88B /* RemoveOperation.swift */,
+ F97B464124D9C78B00F4A88B /* DeleteOperation.swift */,
F97B464524D9C78B00F4A88B /* IncrementOperation.swift */,
+ F97B464024D9C78B00F4A88B /* ParseMutationContainer.swift */,
+ F97B464424D9C78B00F4A88B /* RemoveOperation.swift */,
);
path = "Mutation Operations";
sourceTree = "";
@@ -1093,7 +1120,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
+ shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
4A6511551F49D544005237DF /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
@@ -1128,6 +1155,7 @@
F97B461624D9C6F200F4A88B /* Queryable.swift in Sources */,
F97B45DA24D9C6F200F4A88B /* Extensions.swift in Sources */,
F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */,
+ 705726E02592C2A800F0ADD5 /* ParseHash.swift in Sources */,
70110D52250680140091CC1D /* ParseConstants.swift in Sources */,
F97B465224D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */,
F97B45D624D9C6F200F4A88B /* ParseEncoder.swift in Sources */,
@@ -1136,11 +1164,12 @@
F97B45D224D9C6F200F4A88B /* AnyDecodable.swift in Sources */,
F97B463B24D9C74400F4A88B /* API+Commands.swift in Sources */,
F97B464624D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */,
+ 705A9A2F25991C1400B3547F /* Fileable.swift in Sources */,
F97B464A24D9C78B00F4A88B /* DeleteOperation.swift in Sources */,
F97B460624D9C6F200F4A88B /* ParseUser.swift in Sources */,
F97B465A24D9C78C00F4A88B /* IncrementOperation.swift in Sources */,
F97B45E224D9C6F200F4A88B /* AnyEncodable.swift in Sources */,
- F97B466924D9C8C600F4A88B /* FindResult.swift in Sources */,
+ 70572671259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
F97B462224D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */,
F97B45E624D9C6F200F4A88B /* Query.swift in Sources */,
708D035225215F9B00646C70 /* Deletable.swift in Sources */,
@@ -1153,11 +1182,11 @@
F97B463324D9C74400F4A88B /* URLSession+extensions.swift in Sources */,
F97B464E24D9C78B00F4A88B /* AddOperation.swift in Sources */,
70BC9890252A5B5C00FF3074 /* Objectable.swift in Sources */,
- F97B45FE24D9C6F200F4A88B /* File.swift in Sources */,
+ F97B45FE24D9C6F200F4A88B /* ParseFile.swift in Sources */,
F97B45EE24D9C6F200F4A88B /* BaseParseUser.swift in Sources */,
F97B460A24D9C6F200F4A88B /* Fetchable.swift in Sources */,
F97B460E24D9C6F200F4A88B /* ParseObject.swift in Sources */,
- F97B461224D9C6F200F4A88B /* Saveable.swift in Sources */,
+ F97B461224D9C6F200F4A88B /* Savable.swift in Sources */,
F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */,
F97B465624D9C78C00F4A88B /* RemoveOperation.swift in Sources */,
F97B45FA24D9C6F200F4A88B /* ParseACL.swift in Sources */,
@@ -1177,7 +1206,9 @@
911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */,
911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */,
70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */,
+ 705727B12593FF8800F0ADD5 /* ParseFileTests.swift in Sources */,
70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */,
+ 705A99F9259807F900B3547F /* ParseFileManagerTests.swift in Sources */,
7FFF552E2217E72A007C3B4E /* AnyEncodableTests.swift in Sources */,
7FFF55302217E72A007C3B4E /* AnyDecodableTests.swift in Sources */,
70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */,
@@ -1188,6 +1219,7 @@
9194657824F16E330070296B /* ACLTests.swift in Sources */,
70C7DC1E24D20E530050419B /* ParseUserTests.swift in Sources */,
911DB13324C494390027F3C7 /* MockURLProtocol.swift in Sources */,
+ 7057273A2592CBB100F0ADD5 /* HashTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1199,6 +1231,7 @@
F97B461724D9C6F200F4A88B /* Queryable.swift in Sources */,
F97B45DB24D9C6F200F4A88B /* Extensions.swift in Sources */,
F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */,
+ 705726E12592C2A800F0ADD5 /* ParseHash.swift in Sources */,
70110D53250680140091CC1D /* ParseConstants.swift in Sources */,
F97B465324D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */,
F97B45D724D9C6F200F4A88B /* ParseEncoder.swift in Sources */,
@@ -1207,11 +1240,12 @@
F97B45D324D9C6F200F4A88B /* AnyDecodable.swift in Sources */,
F97B463C24D9C74400F4A88B /* API+Commands.swift in Sources */,
F97B464724D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */,
+ 705A9A3025991C1400B3547F /* Fileable.swift in Sources */,
F97B464B24D9C78B00F4A88B /* DeleteOperation.swift in Sources */,
F97B460724D9C6F200F4A88B /* ParseUser.swift in Sources */,
F97B465B24D9C78C00F4A88B /* IncrementOperation.swift in Sources */,
F97B45E324D9C6F200F4A88B /* AnyEncodable.swift in Sources */,
- F97B466A24D9C8C600F4A88B /* FindResult.swift in Sources */,
+ 70572672259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
F97B462324D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */,
F97B45E724D9C6F200F4A88B /* Query.swift in Sources */,
708D035325215F9B00646C70 /* Deletable.swift in Sources */,
@@ -1224,11 +1258,11 @@
F97B463424D9C74400F4A88B /* URLSession+extensions.swift in Sources */,
F97B464F24D9C78B00F4A88B /* AddOperation.swift in Sources */,
70BC9891252A5B5C00FF3074 /* Objectable.swift in Sources */,
- F97B45FF24D9C6F200F4A88B /* File.swift in Sources */,
+ F97B45FF24D9C6F200F4A88B /* ParseFile.swift in Sources */,
F97B45EF24D9C6F200F4A88B /* BaseParseUser.swift in Sources */,
F97B460B24D9C6F200F4A88B /* Fetchable.swift in Sources */,
F97B460F24D9C6F200F4A88B /* ParseObject.swift in Sources */,
- F97B461324D9C6F200F4A88B /* Saveable.swift in Sources */,
+ F97B461324D9C6F200F4A88B /* Savable.swift in Sources */,
F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */,
F97B465724D9C78C00F4A88B /* RemoveOperation.swift in Sources */,
F97B45FB24D9C6F200F4A88B /* ParseACL.swift in Sources */,
@@ -1257,7 +1291,9 @@
709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */,
709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */,
709B98572556ECAA00507778 /* ACLTests.swift in Sources */,
+ 705727BC2593FF8C00F0ADD5 /* ParseFileTests.swift in Sources */,
709B984F2556ECAA00507778 /* AnyCodableTests.swift in Sources */,
+ 705A99FB259807F900B3547F /* ParseFileManagerTests.swift in Sources */,
709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */,
709B98522556ECAA00507778 /* ParseUserTests.swift in Sources */,
709B984E2556ECAA00507778 /* ParseGeoPointTests.swift in Sources */,
@@ -1268,6 +1304,7 @@
709B985A2556ECAA00507778 /* ParseObjectBatchTests.swift in Sources */,
709B98582556ECAA00507778 /* AnyEncodableTests.swift in Sources */,
709B98542556ECAA00507778 /* ParseInstallationTests.swift in Sources */,
+ 705727262592CBAF00F0ADD5 /* HashTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1280,7 +1317,9 @@
70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */,
70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */,
70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */,
+ 705727BB2593FF8B00F0ADD5 /* ParseFileTests.swift in Sources */,
70F2E2BD254F283000B2EA5C /* AnyDecodableTests.swift in Sources */,
+ 705A99FA259807F900B3547F /* ParseFileManagerTests.swift in Sources */,
70F2E2C1254F283000B2EA5C /* AnyCodableTests.swift in Sources */,
70F2E2B3254F283000B2EA5C /* ParseUserTests.swift in Sources */,
70F2E2C0254F283000B2EA5C /* MockURLResponse.swift in Sources */,
@@ -1291,6 +1330,7 @@
70F2E2B4254F283000B2EA5C /* ParseQueryTests.swift in Sources */,
70F2E2BA254F283000B2EA5C /* ParseInstallationTests.swift in Sources */,
70F2E2B9254F283000B2EA5C /* KeychainStoreTests.swift in Sources */,
+ 705727302592CBB000F0ADD5 /* HashTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1302,6 +1342,7 @@
F97B45E924D9C6F200F4A88B /* Query.swift in Sources */,
F97B463624D9C74400F4A88B /* URLSession+extensions.swift in Sources */,
F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */,
+ 705726E32592C2A800F0ADD5 /* ParseHash.swift in Sources */,
F97B45E124D9C6F200F4A88B /* AnyCodable.swift in Sources */,
F97B45E524D9C6F200F4A88B /* AnyEncodable.swift in Sources */,
F97B465D24D9C78C00F4A88B /* IncrementOperation.swift in Sources */,
@@ -1310,11 +1351,12 @@
F97B461124D9C6F200F4A88B /* ParseObject.swift in Sources */,
F97B460D24D9C6F200F4A88B /* Fetchable.swift in Sources */,
F97B45ED24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */,
+ 705A9A3225991C1400B3547F /* Fileable.swift in Sources */,
F97B45F524D9C6F200F4A88B /* Pointer.swift in Sources */,
F97B460924D9C6F200F4A88B /* ParseUser.swift in Sources */,
F97B463A24D9C74400F4A88B /* Responses.swift in Sources */,
- F97B466C24D9C8C600F4A88B /* FindResult.swift in Sources */,
F97B45DD24D9C6F200F4A88B /* Extensions.swift in Sources */,
+ 70572674259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
F97B462124D9C6F200F4A88B /* ParseStorage.swift in Sources */,
F97B466724D9C88600F4A88B /* SecureStorage.swift in Sources */,
708D035525215F9B00646C70 /* Deletable.swift in Sources */,
@@ -1323,13 +1365,13 @@
F97B465924D9C78C00F4A88B /* RemoveOperation.swift in Sources */,
70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */,
F97B45F924D9C6F200F4A88B /* ParseError.swift in Sources */,
- F97B460124D9C6F200F4A88B /* File.swift in Sources */,
+ F97B460124D9C6F200F4A88B /* ParseFile.swift in Sources */,
F97B464924D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */,
F97B45D124D9C6F200F4A88B /* ParseCoding.swift in Sources */,
70BC9893252A5B5C00FF3074 /* Objectable.swift in Sources */,
F97B465524D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */,
F97B464D24D9C78B00F4A88B /* DeleteOperation.swift in Sources */,
- F97B461524D9C6F200F4A88B /* Saveable.swift in Sources */,
+ F97B461524D9C6F200F4A88B /* Savable.swift in Sources */,
F97B462524D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */,
F97B466224D9C7B500F4A88B /* KeychainStore.swift in Sources */,
F97B463E24D9C74400F4A88B /* API+Commands.swift in Sources */,
@@ -1350,6 +1392,7 @@
F97B45E824D9C6F200F4A88B /* Query.swift in Sources */,
F97B463524D9C74400F4A88B /* URLSession+extensions.swift in Sources */,
F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */,
+ 705726E22592C2A800F0ADD5 /* ParseHash.swift in Sources */,
F97B45E024D9C6F200F4A88B /* AnyCodable.swift in Sources */,
F97B45E424D9C6F200F4A88B /* AnyEncodable.swift in Sources */,
F97B465C24D9C78C00F4A88B /* IncrementOperation.swift in Sources */,
@@ -1358,11 +1401,12 @@
F97B461024D9C6F200F4A88B /* ParseObject.swift in Sources */,
F97B460C24D9C6F200F4A88B /* Fetchable.swift in Sources */,
F97B45EC24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */,
+ 705A9A3125991C1400B3547F /* Fileable.swift in Sources */,
F97B45F424D9C6F200F4A88B /* Pointer.swift in Sources */,
F97B460824D9C6F200F4A88B /* ParseUser.swift in Sources */,
F97B463924D9C74400F4A88B /* Responses.swift in Sources */,
- F97B466B24D9C8C600F4A88B /* FindResult.swift in Sources */,
F97B45DC24D9C6F200F4A88B /* Extensions.swift in Sources */,
+ 70572673259033A700F0ADD5 /* ParseFileManager.swift in Sources */,
F97B462024D9C6F200F4A88B /* ParseStorage.swift in Sources */,
F97B466624D9C88600F4A88B /* SecureStorage.swift in Sources */,
708D035425215F9B00646C70 /* Deletable.swift in Sources */,
@@ -1371,13 +1415,13 @@
F97B465824D9C78C00F4A88B /* RemoveOperation.swift in Sources */,
70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */,
F97B45F824D9C6F200F4A88B /* ParseError.swift in Sources */,
- F97B460024D9C6F200F4A88B /* File.swift in Sources */,
+ F97B460024D9C6F200F4A88B /* ParseFile.swift in Sources */,
F97B464824D9C78B00F4A88B /* ParseMutationContainer.swift in Sources */,
F97B45D024D9C6F200F4A88B /* ParseCoding.swift in Sources */,
70BC9892252A5B5C00FF3074 /* Objectable.swift in Sources */,
F97B465424D9C78C00F4A88B /* AddUniqueOperation.swift in Sources */,
F97B464C24D9C78B00F4A88B /* DeleteOperation.swift in Sources */,
- F97B461424D9C6F200F4A88B /* Saveable.swift in Sources */,
+ F97B461424D9C6F200F4A88B /* Savable.swift in Sources */,
F97B462424D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */,
F97B466124D9C7B500F4A88B /* KeychainStore.swift in Sources */,
F97B463D24D9C74400F4A88B /* API+Commands.swift in Sources */,
@@ -1539,6 +1583,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -1597,6 +1642,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
diff --git a/ParseSwift.xcodeproj/xcshareddata/xcschemes/ParseSwift (watchOS).xcscheme b/ParseSwift.xcodeproj/xcshareddata/xcschemes/ParseSwift (watchOS).xcscheme
new file mode 100644
index 000000000..8bcf1c200
--- /dev/null
+++ b/ParseSwift.xcodeproj/xcshareddata/xcschemes/ParseSwift (watchOS).xcscheme
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift
index 1d1c030fc..f88e97250 100644
--- a/Sources/ParseSwift/API/API+Commands.swift
+++ b/Sources/ParseSwift/API/API+Commands.swift
@@ -12,7 +12,7 @@ import FoundationNetworking
#endif
internal extension API {
-
+ // swiftlint:disable:next type_body_length
struct Command: Encodable where T: Encodable {
typealias ReturnType = U // swiftlint:disable:this nesting
let method: API.Method
@@ -20,25 +20,76 @@ internal extension API {
let body: T?
let mapper: ((Data) throws -> U)
let params: [String: String?]?
+ let uploadData: Data?
+ let uploadFile: URL?
+ let parseURL: URL?
+ let otherURL: URL?
+ let stream: InputStream?
init(method: API.Method,
path: API.Endpoint,
params: [String: String]? = nil,
body: T? = nil,
+ uploadData: Data? = nil,
+ uploadFile: URL? = nil,
+ parseURL: URL? = nil,
+ otherURL: URL? = nil,
+ stream: InputStream? = nil,
mapper: @escaping ((Data) throws -> U)) {
self.method = method
self.path = path
self.body = body
+ self.uploadData = uploadData
+ self.uploadFile = uploadFile
+ self.parseURL = parseURL
+ self.otherURL = otherURL
+ self.stream = stream
self.mapper = mapper
self.params = params
}
- func execute(options: API.Options, childObjects: [NSDictionary: PointerType]? = nil) throws -> U {
- var responseResult: Result?
+ // MARK: Synchronous Execution
+ func executeStream(options: API.Options,
+ childObjects: [NSDictionary: PointerType]? = nil,
+ childFiles: [UUID: ParseFile]? = nil,
+ uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil,
+ stream: InputStream) throws {
+ switch self.prepareURLRequest(options: options, childObjects: childObjects, childFiles: childFiles) {
+
+ case .success(let urlRequest):
+ if method == .POST || method == .PUT {
+ if !ParseConfiguration.isTestingSDK {
+ let delegate = ParseURLSessionDelegate(callbackQueue: nil,
+ uploadProgress: uploadProgress,
+ stream: stream)
+ let session = URLSession(configuration: .default,
+ delegate: delegate,
+ delegateQueue: nil)
+ session.uploadTask(withStreamedRequest: urlRequest).resume()
+ } else {
+ URLSession.testing.uploadTask(withStreamedRequest: urlRequest).resume()
+ }
+ return
+ }
+ case .failure(let error):
+ throw error
+ }
+ }
+ func execute(options: API.Options,
+ childObjects: [NSDictionary: PointerType]? = nil,
+ childFiles: [UUID: ParseFile]? = nil,
+ uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil,
+ downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil) throws -> U {
+ var responseResult: Result?
let group = DispatchGroup()
group.enter()
- self.executeAsync(options: options, callbackQueue: nil, childObjects: childObjects) { result in
+ self.executeAsync(options: options,
+ callbackQueue: nil,
+ childObjects: childObjects,
+ childFiles: childFiles,
+ uploadProgress: uploadProgress,
+ downloadProgress: downloadProgress) { result in
responseResult = result
group.leave()
}
@@ -51,25 +102,169 @@ internal extension API {
return try response.get()
}
- // swiftlint:disable:next function_body_length
+ // MARK: Asynchronous Execution
+ // swiftlint:disable:next function_body_length cyclomatic_complexity
func executeAsync(options: API.Options, callbackQueue: DispatchQueue?,
childObjects: [NSDictionary: PointerType]? = nil,
+ childFiles: [UUID: ParseFile]? = nil,
+ uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil,
+ downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil,
completion: @escaping(Result) -> Void) {
+
+ if !path.urlComponent.contains("/files/") {
+ //All ParseObjects use the shared URLSession
+ switch self.prepareURLRequest(options: options,
+ childObjects: childObjects,
+ childFiles: childFiles) {
+ 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))
+ }
+ } else {
+ //ParseFiles are handled with a dedicated URLSession
+ let session: URLSession!
+ let delegate: URLSessionDelegate!
+ if method == .POST || method == .PUT {
+ switch self.prepareURLRequest(options: options,
+ childObjects: childObjects,
+ childFiles: childFiles) {
+
+ case .success(let urlRequest):
+ if !ParseConfiguration.isTestingSDK {
+ delegate = ParseURLSessionDelegate(callbackQueue: callbackQueue,
+ uploadProgress: uploadProgress)
+ session = URLSession(configuration: .default,
+ delegate: delegate,
+ delegateQueue: nil)
+ } else {
+ session = URLSession.testing
+ }
+ session.uploadTask(with: urlRequest,
+ from: uploadData,
+ from: uploadFile,
+ 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))
+ }
+ } else {
+
+ if !ParseConfiguration.isTestingSDK {
+ delegate = ParseURLSessionDelegate(callbackQueue: callbackQueue,
+ downloadProgress: downloadProgress)
+ session = URLSession(configuration: .default,
+ delegate: delegate,
+ delegateQueue: nil)
+ } else {
+ session = URLSession.testing
+ }
+ if parseURL != nil {
+ switch self.prepareURLRequest(options: options,
+ childObjects: childObjects,
+ childFiles: childFiles) {
+
+ case .success(let urlRequest):
+ session.downloadTask(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))
+ }
+ } else if let otherURL = self.otherURL {
+ //Non-parse servers don't receive any parse dedicated request info
+ session.downloadTask(with: otherURL, 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))
+ }
+ }
+ }
+ } else {
+ completion(.failure(ParseError(code: .unknownError,
+ message: "Can't download the file without specifying the url")))
+ }
+ }
+ }
+ }
+
+ // MARK: URL Preperation
+ func prepareURLRequest(options: API.Options,
+ childObjects: [NSDictionary: PointerType]? = nil,
+ childFiles: [UUID: ParseFile]? = nil) -> Result {
let params = self.params?.getQueryItems()
let headers = API.getHeaders(options: options)
- let url = ParseConfiguration.serverURL.appendingPathComponent(path.urlComponent)
+ let url = parseURL == nil ?
+ ParseConfiguration.serverURL.appendingPathComponent(path.urlComponent) : parseURL!
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
- completion(.failure(ParseError(code: .unknownError,
- message: "couldn't unrwrap url components for \(url)")))
- return
+ return .failure(ParseError(code: .unknownError,
+ message: "couldn't unrwrap url components for \(url)"))
}
components.queryItems = params
guard let urlComponents = components.url else {
- completion(.failure(ParseError(code: .unknownError,
- message: "couldn't create url from components for \(components)")))
- return
+ return .failure(ParseError(code: .unknownError,
+ message: "couldn't create url from components for \(components)"))
}
var urlRequest = URLRequest(url: urlComponents)
@@ -79,41 +274,24 @@ internal extension API {
guard let bodyData = try? ParseCoding
.parseEncoder()
.encode(urlBody, collectChildren: false,
- objectsSavedBeforeThisOne: childObjects) else {
- completion(.failure(ParseError(code: .unknownError,
- message: "couldn't encode body \(urlBody)")))
- return
+ objectsSavedBeforeThisOne: childObjects, filesSavedBeforeThisOne: childFiles) else {
+ return .failure(ParseError(code: .unknownError,
+ message: "couldn't encode body \(urlBody)"))
}
urlRequest.httpBody = bodyData.encoded
} else {
- guard let bodyData = try? ParseCoding.parseEncoder().encode(urlBody) else {
- completion(.failure(ParseError(code: .unknownError,
- message: "couldn't encode body \(urlBody)")))
- return
+ guard let bodyData = try? ParseCoding
+ .parseEncoder()
+ .encode(urlBody) else {
+ return .failure(ParseError(code: .unknownError,
+ message: "couldn't encode body \(urlBody)"))
}
urlRequest.httpBody = bodyData
}
}
urlRequest.httpMethod = method.rawValue
- 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))
- }
- }
- }
+ return .success(urlRequest)
}
enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting
@@ -123,7 +301,67 @@ internal extension API {
}
internal extension API.Command {
- // MARK: Saving
+ // MARK: Uploading File
+ static func uploadFileCommand(_ object: ParseFile) -> API.Command {
+ if object.isSaved {
+ return updateFileCommand(object)
+ }
+ return createFileCommand(object)
+ }
+
+ // MARK: Uploading File - private
+ private static func createFileCommand(_ object: ParseFile) -> API.Command {
+ API.Command(method: .POST,
+ path: .file(fileName: object.name),
+ uploadData: object.data,
+ uploadFile: object.localURL) { (data) -> ParseFile in
+ try ParseCoding.jsonDecoder().decode(FileUploadResponse.self, from: data).apply(to: object)
+ }
+ }
+
+ private static func updateFileCommand(_ object: ParseFile) -> API.Command {
+ API.Command(method: .PUT,
+ path: .file(fileName: object.name),
+ uploadData: object.data,
+ uploadFile: object.localURL) { (data) -> ParseFile in
+ try ParseCoding.jsonDecoder().decode(FileUploadResponse.self, from: data).apply(to: object)
+ }
+ }
+
+ // MARK: Downloading File
+ static func downloadFileCommand(_ object: ParseFile) -> API.Command {
+ API.Command(method: .GET,
+ path: .file(fileName: object.name),
+ parseURL: object.url,
+ otherURL: object.cloudURL) { (data) -> ParseFile in
+ let tempFileLocation = try ParseCoding.jsonDecoder().decode(URL.self, from: data)
+ guard let fileManager = ParseFileManager(),
+ let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else {
+ throw ParseError(code: .unknownError, message: "Can't create fileManager")
+ }
+ let downloadDirectoryPath = defaultDirectoryPath
+ .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true)
+ try fileManager.createDirectoryIfNeeded(downloadDirectoryPath.relativePath)
+ let fileLocation = downloadDirectoryPath.appendingPathComponent(object.name)
+ try? FileManager.default.removeItem(at: fileLocation) //Remove file if it's already present
+ try FileManager.default.moveItem(at: tempFileLocation, to: fileLocation)
+ var object = object
+ object.localURL = fileLocation
+ _ = object.localUUID //Ensure downloaded file has a localUUID
+ return object
+ }
+ }
+
+ // MARK: Deleting File
+ static func deleteFileCommand(_ object: ParseFile) -> API.Command {
+ API.Command(method: .DELETE,
+ path: .file(fileName: object.name),
+ parseURL: object.url) { (data) -> NoBody in
+ try ParseCoding.jsonDecoder().decode(NoBody.self, from: data)
+ }
+ }
+
+ // MARK: Saving ParseObjects
static func saveCommand(_ object: T) -> API.Command where T: ParseObject {
if object.isSaved {
return updateCommand(object)
@@ -131,7 +369,7 @@ internal extension API.Command {
return createCommand(object)
}
- // MARK: Saving - private
+ // MARK: Saving ParseObjects - private
private static func createCommand(_ object: T) -> API.Command where T: ParseObject {
let mapper = { (data) -> T in
try ParseCoding.jsonDecoder().decode(SaveResponse.self, from: data).apply(to: object)
@@ -152,7 +390,7 @@ internal extension API.Command {
mapper: mapper)
}
- // MARK: Saving Encodable
+ // MARK: Saving ParseObjects - Encodable
static func saveCommand(_ object: T) throws -> API.Command where T: Encodable {
guard let objectable = object as? Objectable else {
throw ParseError(code: .unknownError, message: "Not able to cast to objectable. Not saving")
@@ -164,7 +402,7 @@ internal extension API.Command {
}
}
- // MARK: Saving Encodable - private
+ // MARK: Saving ParseObjects - Encodable - private
private static func createCommand(_ object: T) throws -> API.Command where T: Encodable {
guard var objectable = object as? Objectable else {
throw ParseError(code: .unknownError, message: "Not able to cast to objectable. Not saving")
@@ -210,20 +448,21 @@ internal extension API.Command {
}
// MARK: Deleting
- static func deleteCommand(_ object: T) throws -> API.Command where T: ParseObject {
+ 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(
+ return API.Command(
method: .DELETE,
path: object.endpoint
- ) { (data) -> NoBody in
- try ParseCoding.jsonDecoder().decode(NoBody.self, from: data)
+ ) { (data) -> ParseError? in
+ try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data)
}
}
}
+// MARK: Batch - Saving, Fetching
extension API.Command where T: ParseObject {
internal var data: Data? {
@@ -277,44 +516,42 @@ extension API.Command where T: ParseObject {
return RESTBatchCommandType(method: .POST, path: .batch, body: batchCommand, mapper: mapper)
}
- static func batch(commands: [API.Command]) -> RESTBatchCommandNoBodyType {
- let commands = commands.compactMap { (command) -> API.Command? in
+ // MARK: Batch - Deleting
+ static func batch(commands: [API.Command]) -> RESTBatchCommandNoBodyType {
+ let commands = commands.compactMap { (command) -> API.Command? in
let path = ParseConfiguration.mountPath + command.path.urlComponent
- return API.Command(
+ return API.Command(
method: command.method,
path: .any(path), mapper: command.mapper)
}
- let mapper = { (data: Data) -> [Result] in
+ let mapper = { (data: Data) -> [ParseError?] in
- let decodingType = [BatchResponseItem].self
+ let decodingType = [ParseError?].self
do {
let responses = try ParseCoding.jsonDecoder().decode(decodingType, from: data)
- return responses.enumerated().map({ (object) -> (Result) in
+ return responses.enumerated().map({ (object) -> ParseError? in
let response = responses[object.offset]
- if response.success != nil {
- return .success(true)
+ if let error = response {
+ return error
} else {
- guard let parseError = response.error else {
- return .failure(ParseError(code: .unknownError, message: "unknown error"))
- }
-
- return .failure(parseError)
+ return nil
}
})
} catch {
- guard let parseError = error as? ParseError else {
- return [(.failure(ParseError(code: .unknownError, message: "decoding error: \(error)")))]
+ guard (try? ParseCoding.jsonDecoder().decode(NoBody.self, from: data)) != nil else {
+ return [ParseError(code: .unknownError, message: "decoding error: \(error)")]
}
- return [(.failure(parseError))]
+ return [nil]
}
}
let batchCommand = BatchCommand(requests: commands)
- return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper)
+ return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper)
}
}
+// MARK: Batch - Child Objects
extension API.Command where T: Encodable {
internal var data: Data? {
@@ -366,4 +603,4 @@ extension API.Command where T: Encodable {
let batchCommand = BatchCommand(requests: commands)
return RESTBatchCommandTypeEncodable(method: .POST, path: .batch, body: batchCommand, mapper: mapper)
}
-}
+} // swiftlint:disable:this file_length
diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift
index ca3329e4f..618429a02 100644
--- a/Sources/ParseSwift/API/API.swift
+++ b/Sources/ParseSwift/API/API.swift
@@ -21,6 +21,7 @@ public struct API {
case login
case signup
case logout
+ case file(fileName: String)
case any(String)
var urlComponent: String {
@@ -37,6 +38,8 @@ public struct API {
return "/users"
case .logout:
return "/users/logout"
+ case .file(let fileName):
+ return "/files/\(fileName)"
case .any(let path):
return path
}
@@ -54,8 +57,12 @@ public struct API {
case useMasterKey // swiftlint:disable:this inclusive_language
case sessionToken(String)
case installationId(String)
+ case mimeType(String)
+ case fileSize(String)
+ case removeMimeType
+ case metadata([String: String])
+ case tags([String: String])
- // use HashValue so we can use in a sets
public func hash(into hasher: inout Hasher) {
switch self {
case .useMasterKey:
@@ -64,10 +71,21 @@ public struct API {
hasher.combine(2)
case .installationId:
hasher.combine(3)
+ case .mimeType:
+ hasher.combine(4)
+ case .fileSize:
+ hasher.combine(5)
+ case .removeMimeType:
+ hasher.combine(6)
+ case .metadata:
+ hasher.combine(7)
+ case .tags:
+ hasher.combine(8)
}
}
}
+ // swiftlint:disable:next cyclomatic_complexity
internal static func getHeaders(options: API.Options) -> [String: String] {
var headers: [String: String] = ["X-Parse-Application-Id": ParseConfiguration.applicationId,
"Content-Type": "application/json"]
@@ -88,9 +106,23 @@ public struct API {
case .useMasterKey:
headers["X-Parse-Master-Key"] = ParseConfiguration.masterKey
case .sessionToken(let sessionToken):
- headers["X-Parse-Session-Token"] = sessionToken
+ headers["X-Parse-Session-Token"] = sessionToken
case .installationId(let installationId):
headers["X-Parse-Installation-Id"] = installationId
+ case .mimeType(let mimeType):
+ headers["Content-Type"] = mimeType
+ case .fileSize(let fileSize):
+ headers["Content-Length"] = fileSize
+ case .removeMimeType:
+ headers.removeValue(forKey: "Content-Type")
+ case .metadata(let metadata):
+ metadata.forEach {(key, value) -> Void in
+ headers[key] = value
+ }
+ case .tags(let tags):
+ tags.forEach {(key, value) -> Void in
+ headers[key] = value
+ }
}
}
diff --git a/Sources/ParseSwift/API/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift
index 7e0a66bb0..1847caa51 100644
--- a/Sources/ParseSwift/API/BatchUtils.swift
+++ b/Sources/ParseSwift/API/BatchUtils.swift
@@ -13,8 +13,8 @@ typealias ParseObjectBatchResponse = [(Result)]
// swiftlint:disable line_length
typealias RESTBatchCommandType = API.Command, ParseObjectBatchResponse> where T: ParseObject
-typealias ParseObjectBatchCommandNoBody = BatchCommand
-typealias ParseObjectBatchResponseNoBody = [(Result)]
+typealias ParseObjectBatchCommandNoBody = BatchCommand
+typealias ParseObjectBatchResponseNoBody = [ParseError?]
typealias RESTBatchCommandNoBodyType = API.Command, ParseObjectBatchResponseNoBody> where T: Codable
typealias ParseObjectBatchCommandEncodable = BatchCommand where T: Encodable
@@ -27,42 +27,3 @@ typealias RESTBatchCommandTypeEncodable = API.Command: Encodable where T: Encodable {
let requests: [API.Command]
}
-
-internal struct BatchResponseItem: Codable where T: Codable {
- let success: T?
- let error: ParseError?
-}
-
-internal struct WriteResponse: Codable {
- var objectId: String?
- var createdAt: Date?
- var updatedAt: Date?
- var ACL: ParseACL?
-
- func asSaveResponse() -> SaveResponse {
- guard let objectId = objectId, let createdAt = createdAt else {
- fatalError("Cannot create a SaveResponse without objectId")
- }
- return SaveResponse(objectId: objectId, createdAt: createdAt, ACL: ACL)
- }
-
- func asUpdateResponse() -> UpdateResponse {
- guard let updatedAt = updatedAt else {
- fatalError("Cannot create an UpdateResponse without updatedAt")
- }
- return UpdateResponse(updatedAt: updatedAt)
- }
-
- func apply(to object: T, method: API.Method) -> T where T: ParseObject {
- switch method {
- case .POST:
- return asSaveResponse().apply(to: object)
- case .PUT:
- return asUpdateResponse().apply(to: object)
- case .GET:
- fatalError("Parse-server doesn't support batch fetching like this. Look at \"fetchAll\".")
- default:
- fatalError("There is no configured way to apply for method: \(method)")
- }
- }
-}
diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift
index 04c073d60..51bc177bb 100644
--- a/Sources/ParseSwift/API/Responses.swift
+++ b/Sources/ParseSwift/API/Responses.swift
@@ -13,6 +13,7 @@ protocol ChildResponse: Codable {
var className: String { get set }
}
+// MARK: ParseObject
internal struct PointerSaveResponse: ChildResponse {
private let __type: String = "Pointer" // swiftlint:disable:this identifier_name
@@ -47,14 +48,12 @@ internal struct SaveResponse: Decodable {
var updatedAt: Date {
return createdAt
}
- var ACL: ParseACL?
func apply(to object: T) -> T where T: ParseObject {
var object = object
object.objectId = objectId
object.createdAt = createdAt
object.updatedAt = updatedAt
- object.ACL = ACL
return object
}
}
@@ -69,10 +68,69 @@ internal struct UpdateResponse: Decodable {
}
}
-// MARK: LoginSignupResponse
+// MARK: ParseObject Batch
+internal struct BatchResponseItem: Codable where T: Codable {
+ let success: T?
+ let error: ParseError?
+}
+
+internal struct WriteResponse: Codable {
+ var objectId: String?
+ var createdAt: Date?
+ var updatedAt: Date?
+
+ func asSaveResponse() -> SaveResponse {
+ guard let objectId = objectId, let createdAt = createdAt else {
+ fatalError("Cannot create a SaveResponse without objectId")
+ }
+ return SaveResponse(objectId: objectId, createdAt: createdAt)
+ }
+
+ func asUpdateResponse() -> UpdateResponse {
+ guard let updatedAt = updatedAt else {
+ fatalError("Cannot create an UpdateResponse without updatedAt")
+ }
+ return UpdateResponse(updatedAt: updatedAt)
+ }
+
+ func apply(to object: T, method: API.Method) -> T where T: ParseObject {
+ switch method {
+ case .POST:
+ return asSaveResponse().apply(to: object)
+ case .PUT:
+ return asUpdateResponse().apply(to: object)
+ case .GET:
+ fatalError("Parse-server doesn't support batch fetching like this. Look at \"fetchAll\".")
+ default:
+ fatalError("There is no configured way to apply for method: \(method)")
+ }
+ }
+}
+
+// MARK: Query
+internal struct QueryResponse: Codable where T: ParseObject {
+ let results: [T]
+ let count: Int?
+}
+
+// MARK: ParseUser
internal struct LoginSignupResponse: Codable {
let createdAt: Date
let objectId: String
let sessionToken: String
var updatedAt: Date?
}
+
+// MARK: ParseFile
+internal struct FileUploadResponse: Decodable {
+ let name: String
+ let url: URL
+
+ func apply(to file: ParseFile) -> ParseFile {
+ var file = file
+ file.name = name
+ file.url = url
+ _ = file.localUUID //Ensure file has a localUUID
+ return file
+ }
+}
diff --git a/Sources/ParseSwift/API/URLSession+extensions.swift b/Sources/ParseSwift/API/URLSession+extensions.swift
index da5dc3826..bc58fbe48 100755
--- a/Sources/ParseSwift/API/URLSession+extensions.swift
+++ b/Sources/ParseSwift/API/URLSession+extensions.swift
@@ -12,37 +12,188 @@ import Foundation
import FoundationNetworking
#endif
+class ParseURLSessionDelegate: NSObject, URLSessionDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate
+{
+
+ var downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
+ var uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)?
+ var stream: InputStream?
+ var callbackQueue: DispatchQueue?
+
+ init (callbackQueue: DispatchQueue?, uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil,
+ downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil,
+ stream: InputStream? = nil) {
+ super.init()
+ self.callbackQueue = callbackQueue
+ self.uploadProgress = uploadProgress
+ self.downloadProgress = downloadProgress
+ self.stream = stream
+ }
+
+ func urlSession(_ session: URLSession,
+ task: URLSessionTask,
+ didSendBodyData bytesSent: Int64,
+ totalBytesSent: Int64,
+ totalBytesExpectedToSend: Int64) {
+ if let callbackQueue = callbackQueue {
+ callbackQueue.async {
+ self.uploadProgress?(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
+ }
+ } else {
+ uploadProgress?(task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
+ }
+ }
+
+ func urlSession(_ session: URLSession,
+ downloadTask: URLSessionDownloadTask,
+ didFinishDownloadingTo location: URL) {
+ downloadProgress = nil
+ }
+
+ func urlSession(_ session: URLSession,
+ downloadTask: URLSessionDownloadTask,
+ didWriteData bytesWritten: Int64,
+ totalBytesWritten: Int64,
+ totalBytesExpectedToWrite: Int64) {
+ if let callbackQueue = callbackQueue {
+ callbackQueue.async {
+ self.downloadProgress?(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
+ }
+ } else {
+ downloadProgress?(downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
+ }
+ }
+
+ func urlSession(_ session: URLSession,
+ task: URLSessionTask,
+ needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
+ completionHandler(stream)
+ }
+}
+
extension URLSession {
- internal func dataTask(
- with request: URLRequest,
- mapper: @escaping (Data) throws -> U,
- completion: @escaping(Result) -> Void
- ) {
- func makeResult(responseData: Data?, urlResponse: URLResponse?,
- responseError: Error?) -> Result {
- 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)"))
- }
- } else if let responseError = responseError {
+ internal func makeResult(responseData: Data?,
+ urlResponse: URLResponse?,
+ responseError: Error?,
+ mapper: @escaping (Data) throws -> U) -> Result {
+ if let responseError = responseError {
+ guard let parseError = responseError as? ParseError else {
+ return .failure(ParseError(code: .unknownError,
+ message: "Unable to sync with parse-server: \(responseError)"))
+ }
+ return .failure(parseError)
+ } else 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)"))
+ }
+ } else {
+ return .failure(ParseError(code: .unknownError,
+ // swiftlint:disable:next line_length
+ message: "Unable to sync with parse-server: \(String(describing: urlResponse))."))
+ }
+ }
+
+ internal func makeResult(location: URL?,
+ urlResponse: URLResponse?,
+ responseError: Error?,
+ mapper: @escaping (Data) throws -> U) -> Result {
+ if let responseError = responseError {
+ guard let parseError = responseError as? ParseError else {
return .failure(ParseError(code: .unknownError,
message: "Unable to sync with parse-server: \(responseError)"))
- } else {
+ }
+ return .failure(parseError)
+ } else 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: "Unable to sync with parse-server: \(String(describing: urlResponse))."))
+ message: "Error decoding parse-server response: \(error.localizedDescription)"))
}
+ } else {
+ return .failure(ParseError(code: .unknownError,
+ // swiftlint:disable:next line_length
+ message: "Unable to sync with parse-server: \(String(describing: urlResponse))."))
}
+ }
+
+ internal func dataTask(
+ with request: URLRequest,
+ mapper: @escaping (Data) throws -> U,
+ completion: @escaping(Result) -> Void
+ ) {
dataTask(with: request) { (responseData, urlResponse, responseError) in
- let result = makeResult(responseData: responseData, urlResponse: urlResponse, responseError: responseError)
- completion(result)
+ completion(self.makeResult(responseData: responseData,
+ urlResponse: urlResponse,
+ responseError: responseError, mapper: mapper))
+ }.resume()
+ }
+}
+
+extension URLSession {
+
+ internal func uploadTask(
+ with request: URLRequest,
+ from data: Data?,
+ from file: URL?,
+ mapper: @escaping (Data) throws -> U,
+ completion: @escaping(Result) -> Void
+ ) {
+
+ if let data = data {
+ uploadTask(with: request, from: data) { (responseData, urlResponse, responseError) in
+ completion(self.makeResult(responseData: responseData,
+ urlResponse: urlResponse,
+ responseError: responseError, mapper: mapper))
+ }.resume()
+ } else if let file = file {
+ uploadTask(with: request, fromFile: file) { (responseData, urlResponse, responseError) in
+ completion(self.makeResult(responseData: responseData,
+ urlResponse: urlResponse,
+ responseError: responseError, mapper: mapper))
+ }.resume()
+ } else {
+ completion(.failure(ParseError(code: .unknownError, message: "data and file both can't be nil")))
+ }
+ }
+
+ internal func downloadTask(
+ with request: URLRequest,
+ mapper: @escaping (Data) throws -> U,
+ completion: @escaping(Result) -> Void
+ ) {
+
+ downloadTask(with: request) { (location, urlResponse, responseError) in
+ completion(self.makeResult(location: location,
+ urlResponse: urlResponse,
+ responseError: responseError, mapper: mapper))
+ }.resume()
+ }
+
+ internal func downloadTask(
+ with url: URL,
+ mapper: @escaping (Data) throws -> U,
+ completion: @escaping(Result) -> Void
+ ) {
+
+ downloadTask(with: url) { (location, urlResponse, responseError) in
+ completion(self.makeResult(location: location,
+ urlResponse: urlResponse,
+ responseError: responseError,
+ mapper: mapper))
}.resume()
}
}
+
+internal extension URLSession {
+ static var testing = URLSession.shared
+}
diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift
index 827bc015b..4b6ee2546 100644
--- a/Sources/ParseSwift/Coding/ParseEncoder.swift
+++ b/Sources/ParseSwift/Coding/ParseEncoder.swift
@@ -76,17 +76,18 @@ public struct ParseEncoder {
if let dateEncodingStrategy = dateEncodingStrategy {
encoder.dateEncodingStrategy = .custom(dateEncodingStrategy)
}
- return try encoder.encodeObject(value, collectChildren: false, objectsSavedBeforeThisOne: nil).encoded
+ return try encoder.encodeObject(value, collectChildren: false, objectsSavedBeforeThisOne: nil, filesSavedBeforeThisOne: nil).encoded
}
// swiftlint:disable large_tuple
internal func encode(_ value: Encodable, collectChildren: Bool,
- objectsSavedBeforeThisOne: [NSDictionary: PointerType]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) {
+ objectsSavedBeforeThisOne: [NSDictionary: PointerType]?,
+ filesSavedBeforeThisOne: [UUID: ParseFile]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) {
let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: skippedKeys)
if let dateEncodingStrategy = dateEncodingStrategy {
encoder.dateEncodingStrategy = .custom(dateEncodingStrategy)
}
- return try encoder.encodeObject(value, collectChildren: collectChildren, objectsSavedBeforeThisOne: objectsSavedBeforeThisOne)
+ return try encoder.encodeObject(value, collectChildren: collectChildren, objectsSavedBeforeThisOne: objectsSavedBeforeThisOne, filesSavedBeforeThisOne: filesSavedBeforeThisOne)
}
}
@@ -96,9 +97,11 @@ private class _ParseEncoder: JSONEncoder, Encoder {
let dictionary: NSMutableDictionary
let skippedKeys: Set
var uniqueObjects = Set()
+ var uniqueFiles = Set()
var newObjects = [Encodable]()
var collectChildren = false
var objectsSavedBeforeThisOne: [NSDictionary: PointerType]?
+ var filesSavedBeforeThisOne: [UUID: ParseFile]?
/// The encoder's storage.
var storage: _ParseEncodingStorage
var ignoreSkipKeys = false
@@ -148,7 +151,9 @@ private class _ParseEncoder: JSONEncoder, Encoder {
}
// swiftlint:disable large_tuple
- func encodeObject(_ value: Encodable, collectChildren: Bool, objectsSavedBeforeThisOne: [NSDictionary: PointerType]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) {
+ func encodeObject(_ value: Encodable, collectChildren: Bool,
+ objectsSavedBeforeThisOne: [NSDictionary: PointerType]?,
+ filesSavedBeforeThisOne: [UUID: ParseFile]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) {
let encoder = _ParseEncoder(codingPath: codingPath, dictionary: dictionary, skippingKeys: skippedKeys)
encoder.collectChildren = collectChildren
@@ -159,6 +164,7 @@ private class _ParseEncoder: JSONEncoder, Encoder {
encoder.keyEncodingStrategy = keyEncodingStrategy
encoder.userInfo = userInfo
encoder.objectsSavedBeforeThisOne = objectsSavedBeforeThisOne
+ encoder.filesSavedBeforeThisOne = filesSavedBeforeThisOne
guard let topLevel = try encoder.box_(value) else {
throw EncodingError.invalidValue(value,
@@ -251,6 +257,36 @@ private class _ParseEncoder: JSONEncoder, Encoder {
}
return valueToEncode
}
+
+ func deepFindAndReplaceParseFiles(_ value: ParseFile) throws -> Encodable? {
+ var valueToEncode: Encodable?
+ if value.isSaved {
+ if self.uniqueFiles.contains(value) {
+ throw ParseError(code: .unknownError, message: "Found a circular dependency when encoding.")
+ }
+ self.uniqueFiles.insert(value)
+ if !self.collectChildren {
+ valueToEncode = value
+ }
+ } else {
+ var mutableValue = value
+ let uuid = mutableValue.localUUID
+ if self.collectChildren {
+ if let updatedFile = self.filesSavedBeforeThisOne?[uuid] {
+ 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] {
+ valueToEncode = currentFile
+ } else if codingPath.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.")
+ }
+ }
+ return valueToEncode
+ }
}
// MARK: _ParseEncoderKeyedEncodingContainer
@@ -337,6 +373,18 @@ private struct _ParseEncoderKeyedEncodingContainer: KeyedEncodin
self.container[key.stringValue] = try replacedObjects.map { try self.encoder.box($0) }
return
}
+ } else if let parseFile = value as? ParseFile {
+ if let replacedObject = try self.encoder.deepFindAndReplaceParseFiles(parseFile) {
+ valueToEncode = replacedObject
+ }
+ } else if let parseFiles = value as? [ParseFile] {
+ let replacedFiles = try parseFiles.compactMap { try self.encoder.deepFindAndReplaceParseFiles($0) }
+ if replacedFiles.count > 0 {
+ self.encoder.codingPath.append(key)
+ defer { self.encoder.codingPath.removeLast() }
+ self.container[key.stringValue] = try replacedFiles.map { try self.encoder.box($0) }
+ return
+ }
}
self.encoder.codingPath.append(key)
@@ -394,11 +442,11 @@ private struct _ParseEncoderKeyedEncodingContainer: KeyedEncodin
}
mutating func superEncoder() -> Encoder {
- _ParseReferencingEncoder(referencing: self.encoder, key: _JSONKey.super, wrapping: self.container, skippingKeys: self.encoder.skippedKeys, collectChildren: self.encoder.collectChildren, objectsSavedBeforeThisOne: self.encoder.objectsSavedBeforeThisOne)
+ _ParseReferencingEncoder(referencing: self.encoder, key: _JSONKey.super, wrapping: self.container, skippingKeys: self.encoder.skippedKeys, collectChildren: self.encoder.collectChildren, objectsSavedBeforeThisOne: self.encoder.objectsSavedBeforeThisOne, filesSavedBeforeThisOne: self.encoder.filesSavedBeforeThisOne)
}
mutating func superEncoder(forKey key: Key) -> Encoder {
- _ParseReferencingEncoder(referencing: self.encoder, key: key, wrapping: self.container, skippingKeys: self.encoder.skippedKeys, collectChildren: self.encoder.collectChildren, objectsSavedBeforeThisOne: self.encoder.objectsSavedBeforeThisOne)
+ _ParseReferencingEncoder(referencing: self.encoder, key: key, wrapping: self.container, skippingKeys: self.encoder.skippedKeys, collectChildren: self.encoder.collectChildren, objectsSavedBeforeThisOne: self.encoder.objectsSavedBeforeThisOne, filesSavedBeforeThisOne: self.encoder.filesSavedBeforeThisOne)
}
}
@@ -477,7 +525,7 @@ private struct _ParseEncoderUnkeyedEncodingContainer: UnkeyedEncodingContainer {
}
public mutating func superEncoder() -> Encoder {
- return _ParseReferencingEncoder(referencing: self.encoder, at: self.container.count, wrapping: self.container, skippingKeys: self.encoder.skippedKeys, collectChildren: self.encoder.collectChildren, objectsSavedBeforeThisOne: self.encoder.objectsSavedBeforeThisOne)
+ return _ParseReferencingEncoder(referencing: self.encoder, at: self.container.count, wrapping: self.container, skippingKeys: self.encoder.skippedKeys, collectChildren: self.encoder.collectChildren, objectsSavedBeforeThisOne: self.encoder.objectsSavedBeforeThisOne, filesSavedBeforeThisOne: self.encoder.filesSavedBeforeThisOne)
}
}
@@ -774,6 +822,7 @@ extension _ParseEncoder {
// swiftlint:disable:next force_cast
return (value as! NSDecimalNumber)
} else if value is _JSONStringDictionaryEncodableMarker {
+ //COREY: DON'T remove the force unwrap, it will crash the app
// swiftlint:disable:next force_cast
return try self.box(value as! [String : Encodable])
} else if value is PointerType {
@@ -830,22 +879,24 @@ 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]?) {
+ init(referencing encoder: _ParseEncoder, at index: Int, wrapping array: NSMutableArray, skippingKeys: Set, collectChildren: Bool, objectsSavedBeforeThisOne: [NSDictionary: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) {
self.encoder = encoder
self.reference = .array(array, index)
super.init(codingPath: encoder.codingPath, dictionary: NSMutableDictionary(), skippingKeys: skippingKeys)
self.collectChildren = collectChildren
self.objectsSavedBeforeThisOne = objectsSavedBeforeThisOne
+ self.filesSavedBeforeThisOne = filesSavedBeforeThisOne
self.codingPath.append(_JSONKey(index: index))
}
/// 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]?) {
+ init(referencing encoder: _ParseEncoder, key: CodingKey, wrapping dictionary: NSMutableDictionary, skippingKeys: Set, collectChildren: Bool, objectsSavedBeforeThisOne: [NSDictionary: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) {
self.encoder = encoder
self.reference = .dictionary(dictionary, key.stringValue)
super.init(codingPath: encoder.codingPath, dictionary: dictionary, skippingKeys: skippingKeys)
self.collectChildren = collectChildren
self.objectsSavedBeforeThisOne = objectsSavedBeforeThisOne
+ self.filesSavedBeforeThisOne = filesSavedBeforeThisOne
self.codingPath.append(key)
}
diff --git a/Sources/ParseSwift/Object Protocols/ParseInstallation.swift b/Sources/ParseSwift/Object Protocols/ParseInstallation.swift
index b768fb709..20fd72c36 100644
--- a/Sources/ParseSwift/Object Protocols/ParseInstallation.swift
+++ b/Sources/ParseSwift/Object Protocols/ParseInstallation.swift
@@ -28,10 +28,10 @@ import AppKit
when the `ParseInstallation` is saved, thus these fields might not reflect the
latest device state if the installation has not recently been saved.
- `ParseInstallation` objects which have a valid `deviceToken` and are saved to
+ `ParseInstallation` installations which have a valid `deviceToken` and are saved to
the Parse cloud can be used to target push notifications.
- - warning: Only use `ParseInstallation` objects on the main thread as they
+ - warning: Only use `ParseInstallation` installations on the main thread as they
require UIApplication for `badge`
*/
public protocol ParseInstallation: ParseObject {
@@ -314,7 +314,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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to save installations. 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 +327,7 @@ extension ParseInstallation {
/**
Fetches the `ParseInstallation` *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 options used to save installations. 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.
@@ -354,30 +354,27 @@ extension ParseInstallation {
}
}
-// MARK: Saveable
+// MARK: Savable
extension ParseInstallation {
/**
Saves the `ParseInstallation` *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 options used to save installations. 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 childFiles: [UUID: ParseFile]?
var error: ParseError?
let group = DispatchGroup()
group.enter()
- self.ensureDeepSave(options: options) { result in
- switch result {
-
- case .success(let savedChildObjects):
- childObjects = savedChildObjects
- case .failure(let parseError):
- error = parseError
- }
+ self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) in
+ childObjects = savedChildObjects
+ childFiles = savedChildFiles
+ error = parseError
group.leave()
}
group.wait()
@@ -386,7 +383,10 @@ extension ParseInstallation {
throw error
}
- let result: Self = try saveCommand().execute(options: options, childObjects: childObjects)
+ let result: Self = try saveCommand()
+ .execute(options: options,
+ childObjects: childObjects,
+ childFiles: childFiles)
try? Self.updateKeychainIfNeeded([result])
return result
}
@@ -394,7 +394,7 @@ extension ParseInstallation {
/**
Saves the `ParseInstallation` *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 options used to save installations. 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)`.
@@ -405,20 +405,20 @@ extension ParseInstallation {
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result) -> Void
) {
- self.ensureDeepSave(options: options) { result in
- switch result {
-
- case .success(let savedChildObjects):
- self.saveCommand().executeAsync(options: options, callbackQueue: callbackQueue,
- childObjects: savedChildObjects) { result in
+ self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in
+ guard let parseError = error else {
+ self.saveCommand().executeAsync(options: options,
+ callbackQueue: callbackQueue,
+ childObjects: savedChildObjects,
+ childFiles: savedChildFiles) { result in
if case .success(let foundResults) = result {
try? Self.updateKeychainIfNeeded([foundResults])
}
completion(result)
}
- case .failure(let parseError):
- completion(.failure(parseError))
+ return
}
+ completion(.failure(parseError))
}
}
}
@@ -429,7 +429,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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to save installations. 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 +441,7 @@ extension ParseInstallation {
/**
Deletes the `ParseInstallation` *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 options used to save installations. 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.
@@ -476,9 +476,9 @@ extension ParseInstallation {
public extension Sequence where Element: ParseInstallation {
/**
- Saves a collection of objects *synchronously* all at once and throws an error if necessary.
+ Saves a collection of installations *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 options used to save installations. 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`
@@ -494,9 +494,9 @@ public extension Sequence where Element: ParseInstallation {
}
/**
- Saves a collection of objects all at once *asynchronously* and executes the completion block when done.
+ 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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to save installations. 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>)`.
@@ -514,7 +514,7 @@ public extension Sequence where Element: ParseInstallation {
switch results {
case .success(let saved):
- try? Self.Element.updateKeychainIfNeeded(compactMap {$0})
+ try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0})
completion(.success(saved))
case .failure(let error):
completion(.failure(error))
@@ -523,14 +523,14 @@ public extension Sequence where Element: ParseInstallation {
}
/**
- Fetches a collection of objects *synchronously* all at once and throws an error if necessary.
+ Fetches a collection of installations *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 options used to fetch installations. 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`
- important: If an object fetched has the same objectId as current, it will automatically update the current.
- - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in
+ - warning: The order in which installations are returned are not guarenteed. You shouldn't expect results in
any particular order.
*/
func fetchAll(options: API.Options = []) throws -> [(Result)] {
@@ -559,14 +559,14 @@ public extension Sequence where Element: ParseInstallation {
}
/**
- Fetches a collection of objects all at once *asynchronously* and executes the completion block when done.
+ 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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to fetch installations. 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 fetched has the same objectId as current, it will automatically update the current.
- - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in
+ - warning: The order in which installations are returned are not guarenteed. You shouldn't expect results in
any particular order.
*/
func fetchAll(
@@ -606,11 +606,11 @@ public extension Sequence where Element: ParseInstallation {
}
/**
- Deletes a collection of objects *synchronously* all at once and throws an error if necessary.
+ Deletes a collection of installations *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 options: A set of options used to delete installations. Defaults to an empty set.
- - returns: Returns a Result enum with `true` if the delete successful or a `ParseError` if it failed.
+ - 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
array of other Parse.Error objects. Each error object in this array
has an "object" property that references the object that could not be
@@ -621,9 +621,9 @@ 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 -> [(Result)] {
+ func deleteAll(options: API.Options = []) throws -> [ParseError?] {
let commands = try map { try $0.deleteCommand() }
- let returnResults = try API.Command
+ let returnResults = try API.Command
.batch(commands: commands)
.execute(options: options)
@@ -632,13 +632,13 @@ public extension Sequence where Element: ParseInstallation {
}
/**
- Deletes a collection of objects all at once *asynchronously* and executes the completion block when done.
+ 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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to delete installations. 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>)`.
- Each element in the array is a Result enum with `true` if the delete successful or a `ParseError` if it failed.
+ It should have the following argument signature: `(Result<[ParseError?], ParseError>)`.
+ Each element in the array is either `nil` if the delete successful or a `ParseError` if it failed.
1. A `ParseError.Code.aggregateError`. This object's "errors" property is an
array of other Parse.Error objects. Each error object in this array
has an "object" property that references the object that could not be
@@ -651,18 +651,18 @@ public extension Sequence where Element: ParseInstallation {
func deleteAll(
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
- completion: @escaping (Result<[(Result)], ParseError>) -> Void
+ completion: @escaping (Result<[ParseError?], ParseError>) -> Void
) {
do {
let commands = try map({ try $0.deleteCommand() })
- API.Command
+ API.Command
.batch(commands: commands)
.executeAsync(options: options,
callbackQueue: callbackQueue) { results in
switch results {
case .success(let deleted):
- try? Self.Element.updateKeychainIfNeeded(compactMap {$0})
+ try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0})
completion(.success(deleted))
case .failure(let error):
completion(.failure(error))
diff --git a/Sources/ParseSwift/Object Protocols/ParseObject.swift b/Sources/ParseSwift/Object Protocols/ParseObject.swift
index 46c5e161a..18be7dfd8 100644
--- a/Sources/ParseSwift/Object Protocols/ParseObject.swift
+++ b/Sources/ParseSwift/Object Protocols/ParseObject.swift
@@ -24,7 +24,7 @@ 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, Saveable, Deletable, Hashable, CustomDebugStringConvertible {}
+public protocol ParseObject: Objectable, Fetchable, Savable, Deletable, Hashable, CustomDebugStringConvertible {}
// MARK: Default Implementations
extension ParseObject {
@@ -170,7 +170,7 @@ public extension Sequence where Element: ParseObject {
- parameter options: A set of options used to delete objects. Defaults to an empty set.
- - returns: Returns a Result enum with `true` if the delete successful or a `ParseError` if it failed.
+ - 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
array of other Parse.Error objects. Each error object in this array
has an "object" property that references the object that could not be
@@ -180,9 +180,9 @@ public extension Sequence where Element: ParseObject {
instance, a connection failure in the middle of the delete).
- throws: `ParseError`
*/
- func deleteAll(options: API.Options = []) throws -> [(Result)] {
+ func deleteAll(options: API.Options = []) throws -> [ParseError?] {
let commands = try map { try $0.deleteCommand() }
- return try API.Command
+ return try API.Command
.batch(commands: commands)
.execute(options: options)
}
@@ -193,8 +193,8 @@ public extension Sequence where Element: ParseObject {
- parameter options: A set of options used to delete objects. 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>)`.
- Each element in the array is a Result enum with `true` if the delete successful or a `ParseError` if it failed.
+ It should have the following argument signature: `(Result<[ParseError?], ParseError>)`.
+ Each element in the array is `nil` if the delete successful or a `ParseError` if it failed.
1. A `ParseError.Code.aggregateError`. This object's "errors" property is an
array of other Parse.Error objects. Each error object in this array
has an "object" property that references the object that could not be
@@ -206,11 +206,11 @@ public extension Sequence where Element: ParseObject {
func deleteAll(
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
- completion: @escaping (Result<[(Result)], ParseError>) -> Void
+ completion: @escaping (Result<[ParseError?], ParseError>) -> Void
) {
do {
let commands = try map({ try $0.deleteCommand() })
- API.Command
+ API.Command
.batch(commands: commands)
.executeAsync(options: options,
callbackQueue: callbackQueue,
@@ -321,7 +321,7 @@ public extension ParseObject {
}
}
-// MARK: Saveable
+// MARK: Savable
extension ParseObject {
/**
@@ -334,17 +334,14 @@ extension ParseObject {
*/
public func save(options: API.Options = []) throws -> Self {
var childObjects: [NSDictionary: PointerType]?
+ var childFiles: [UUID: ParseFile]?
var error: ParseError?
let group = DispatchGroup()
group.enter()
- self.ensureDeepSave(options: options) { result in
- switch result {
-
- case .success(let savedChildObjects):
- childObjects = savedChildObjects
- case .failure(let parseError):
- error = parseError
- }
+ self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) in
+ childObjects = savedChildObjects
+ childFiles = savedChildFiles
+ error = parseError
group.leave()
}
group.wait()
@@ -353,7 +350,7 @@ extension ParseObject {
throw error
}
- return try saveCommand().execute(options: options, childObjects: childObjects)
+ return try saveCommand().execute(options: options, childObjects: childObjects, childFiles: childFiles)
}
/**
@@ -369,15 +366,16 @@ extension ParseObject {
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result) -> Void
) {
- self.ensureDeepSave(options: options) { result in
- switch result {
-
- case .success(let savedChildObjects):
- self.saveCommand().executeAsync(options: options, callbackQueue: callbackQueue,
- childObjects: savedChildObjects, completion: completion)
- case .failure(let parseError):
- completion(.failure(parseError))
+ self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in
+ guard let parseError = error else {
+ self.saveCommand().executeAsync(options: options,
+ callbackQueue: callbackQueue,
+ childObjects: savedChildObjects,
+ childFiles: savedChildFiles,
+ completion: completion)
+ return
}
+ completion(.failure(parseError))
}
}
@@ -385,56 +383,83 @@ extension ParseObject {
return API.Command.saveCommand(self)
}
+ // swiftlint:disable:next function_body_length
internal func ensureDeepSave(options: API.Options = [],
- completion: @escaping (Result<[NSDictionary: PointerType], ParseError>) -> Void) {
+ completion: @escaping ([NSDictionary: 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 filesFinishedSaving = [UUID: ParseFile]()
+
do {
- let object = try ParseCoding.parseEncoder().encode(self, collectChildren: true,
- objectsSavedBeforeThisOne: nil)
+ let object = try ParseCoding.parseEncoder()
+ .encode(self, collectChildren: true,
+ objectsSavedBeforeThisOne: nil, filesSavedBeforeThisOne: nil)
var waitingToBeSaved = object.unsavedChildren
- var finishedSaving = [NSDictionary: PointerType]()
+
while waitingToBeSaved.count > 0 {
- var savable = [Encodable]()
+ var savableObjects = [Encodable]()
+ var savableFiles = [ParseFile]()
var nextBatch = [Encodable]()
- try waitingToBeSaved.forEach { parseObject in
+ try waitingToBeSaved.forEach { parseType in
- let waitingObjectInfo = try ParseCoding.parseEncoder().encode(parseObject,
- collectChildren: true,
- objectsSavedBeforeThisOne: finishedSaving)
-
- if waitingObjectInfo.unsavedChildren.count == 0 {
- savable.append(parseObject)
+ if let parseFile = parseType as? ParseFile {
+ //ParseFiles can be saved now
+ savableFiles.append(parseFile)
} else {
- nextBatch.append(parseObject)
+ //This is a ParseObject
+ let waitingObjectInfo = try ParseCoding
+ .parseEncoder()
+ .encode(parseType,
+ 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)
+ } else {
+ //Else this ParseObject needs to wait until it's children are saved
+ nextBatch.append(parseType)
+ }
}
}
waitingToBeSaved = nextBatch
- if savable.count == 0 {
- completion(.failure(ParseError(code: .unknownError,
- message: "Found a circular dependency in ParseObject.")))
+ if savableObjects.count == 0 && savableFiles.count == 0 {
+ completion(objectsFinishedSaving,
+ filesFinishedSaving,
+ ParseError(code: .unknownError,
+ message: "Found a circular dependency in ParseObject."))
return
}
//Currently, batch isn't working for Encodable
- //savable.saveAll(encodableObjects: savable)
- try savable.forEach {
+ //savableObjects.saveAll(encodableObjects: savable)
+ try savableObjects.forEach {
let hash = BaseObjectable.createHash($0)
- finishedSaving[hash] = try $0.save(options: options)
+ objectsFinishedSaving[hash] = try $0.save(options: options)
+ }
+
+ try savableFiles.forEach {
+ var file = $0
+ filesFinishedSaving[file.localUUID] = try $0.save(options: options)
}
}
- completion(.success(finishedSaving))
+ completion(objectsFinishedSaving, filesFinishedSaving, nil)
} catch {
guard let parseError = error as? ParseError else {
- completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription)))
+ completion(objectsFinishedSaving, filesFinishedSaving,
+ ParseError(code: .unknownError,
+ message: error.localizedDescription))
return
}
- completion(.failure(parseError))
+ completion(objectsFinishedSaving, filesFinishedSaving, parseError)
}
}
}
@@ -468,8 +493,9 @@ extension ParseObject {
- throws: An Error of `ParseError` type.
*/
public func delete(options: API.Options = []) throws {
- _ = try deleteCommand().execute(options: options)
- return
+ if let error = try deleteCommand().execute(options: options) {
+ throw error
+ }
}
/**
@@ -490,8 +516,8 @@ extension ParseObject {
try deleteCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in
switch result {
- case .success:
- completion(nil)
+ case .success(let error):
+ completion(error)
case .failure(let error):
completion(error)
}
@@ -503,7 +529,7 @@ extension ParseObject {
}
}
- internal func deleteCommand() throws -> API.Command {
- try API.Command.deleteCommand(self)
+ internal func deleteCommand() throws -> API.Command {
+ try API.Command.deleteCommand(self)
}
}// swiftlint:disable:this file_length
diff --git a/Sources/ParseSwift/Object Protocols/ParseUser.swift b/Sources/ParseSwift/Object Protocols/ParseUser.swift
index 9fc916478..89690b64b 100644
--- a/Sources/ParseSwift/Object Protocols/ParseUser.swift
+++ b/Sources/ParseSwift/Object Protocols/ParseUser.swift
@@ -69,7 +69,7 @@ extension ParseUser {
Gets the currently logged in user from the Keychain and returns an instance of it.
- returns: Returns a `ParseUser` that is the currently logged in user. If there is none, returns `nil`.
- - warning: Only use `current` objects on the main thread as as modifications to `current` have to be unique.
+ - warning: Only use `current` users on the main thread as as modifications to `current` have to be unique.
*/
public static var current: Self? {
get { Self.currentUserContainer?.currentUser }
@@ -125,7 +125,7 @@ extension ParseUser {
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result) -> Void
) {
- return loginCommand(username: username, password: password)
+ loginCommand(username: username, password: password)
.executeAsync(options: [], callbackQueue: callbackQueue, completion: completion)
}
@@ -160,8 +160,8 @@ extension ParseUser {
/**
Logs out the currently logged in user in Keychain *synchronously*.
*/
- public static func logout() throws {
- _ = try logoutCommand().execute(options: [])
+ public static func logout(options: API.Options = []) throws {
+ _ = try logoutCommand().execute(options: options)
}
/**
@@ -174,9 +174,9 @@ extension ParseUser {
- 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.
*/
- public static func logout(callbackQueue: DispatchQueue = .main,
+ public static func logout(options: API.Options = [], callbackQueue: DispatchQueue = .main,
completion: @escaping (Result) -> Void) {
- logoutCommand().executeAsync(options: [], callbackQueue: callbackQueue) { result in
+ logoutCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in
completion(result.map { true })
}
}
@@ -197,12 +197,14 @@ 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 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.
- returns: Returns whether the sign up was successful.
*/
public static func signup(username: String,
- password: String) throws -> Self {
- try signupCommand(username: username, password: password).execute(options: [])
+ password: String, options: API.Options = []) throws -> Self {
+ try signupCommand(username: username, password: password).execute(options: options)
}
/**
@@ -211,11 +213,11 @@ 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.
- returns: Returns whether the sign up was successful.
*/
- public func signup() throws -> Self {
- try signupCommand().execute(options: [])
+ public func signup(options: API.Options = []) throws -> Self {
+ try signupCommand().execute(options: options)
}
/**
@@ -224,13 +226,14 @@ 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 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 signup(callbackQueue: DispatchQueue = .main,
+ public func signup(options: API.Options = [], callbackQueue: DispatchQueue = .main,
completion: @escaping (Result) -> Void) {
- return signupCommand().executeAsync(options: [], callbackQueue: callbackQueue, completion: completion)
+ signupCommand().executeAsync(options: options, callbackQueue: callbackQueue, completion: completion)
}
/**
@@ -239,9 +242,9 @@ 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 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 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)`.
@@ -249,10 +252,11 @@ extension ParseUser {
public static func signup(
username: String,
password: String,
+ options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result) -> Void
) {
- return signupCommand(username: username, password: password)
+ signupCommand(username: username, password: password)
.executeAsync(options: [], callbackQueue: callbackQueue, completion: completion)
}
@@ -328,7 +332,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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to save users. 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.
*/
@@ -341,7 +345,7 @@ extension ParseUser {
/**
Fetches the `ParseUser` *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 options used to save users. 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.
@@ -368,30 +372,27 @@ extension ParseUser {
}
}
-// MARK: Saveable
+// MARK: Savable
extension ParseUser {
/**
Saves the `ParseUser` *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 options used to save users. 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 childFiles: [UUID: ParseFile]?
var error: ParseError?
let group = DispatchGroup()
group.enter()
- self.ensureDeepSave(options: options) { result in
- switch result {
-
- case .success(let savedChildObjects):
- childObjects = savedChildObjects
- case .failure(let parseError):
- error = parseError
- }
+ self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, parseError) in
+ childObjects = savedChildObjects
+ childFiles = savedChildFiles
+ error = parseError
group.leave()
}
group.wait()
@@ -400,7 +401,10 @@ extension ParseUser {
throw error
}
- let result: Self = try saveCommand().execute(options: options, childObjects: childObjects)
+ let result: Self = try saveCommand()
+ .execute(options: options,
+ childObjects: childObjects,
+ childFiles: childFiles)
try? Self.updateKeychainIfNeeded([result])
return result
}
@@ -408,7 +412,7 @@ extension ParseUser {
/**
Saves the `ParseUser` *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 options used to save users. 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)`.
@@ -419,20 +423,20 @@ extension ParseUser {
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result) -> Void
) {
- self.ensureDeepSave(options: options) { result in
- switch result {
-
- case .success(let savedChildObjects):
- self.saveCommand().executeAsync(options: options, callbackQueue: callbackQueue,
- childObjects: savedChildObjects) { result in
+ self.ensureDeepSave(options: options) { (savedChildObjects, savedChildFiles, error) in
+ guard let parseError = error else {
+ self.saveCommand().executeAsync(options: options,
+ callbackQueue: callbackQueue,
+ childObjects: savedChildObjects,
+ childFiles: savedChildFiles) { result in
if case .success(let foundResults) = result {
try? Self.updateKeychainIfNeeded([foundResults])
}
completion(result)
}
- case .failure(let parseError):
- completion(.failure(parseError))
+ return
}
+ completion(.failure(parseError))
}
}
}
@@ -442,7 +446,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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to save users. 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.
*/
@@ -454,7 +458,7 @@ extension ParseUser {
/**
Deletes the `ParseUser` *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 options used to save users. 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.
@@ -489,9 +493,9 @@ extension ParseUser {
public extension Sequence where Element: ParseUser {
/**
- Saves a collection of objects *synchronously* all at once and throws an error if necessary.
+ Saves a collection of users *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 options used to save users. 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`
@@ -507,9 +511,9 @@ public extension Sequence where Element: ParseUser {
}
/**
- Saves a collection of objects all at once *asynchronously* and executes the completion block when done.
+ 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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to save users. 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>)`.
@@ -527,7 +531,7 @@ public extension Sequence where Element: ParseUser {
switch results {
case .success(let saved):
- try? Self.Element.updateKeychainIfNeeded(compactMap {$0})
+ try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0})
completion(.success(saved))
case .failure(let error):
completion(.failure(error))
@@ -536,20 +540,20 @@ public extension Sequence where Element: ParseUser {
}
/**
- Fetches a collection of objects *synchronously* all at once and throws an error if necessary.
+ Fetches a collection of users *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 options used to fetch users. 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`
- important: If an object fetched has the same objectId as current, it will automatically update the current.
- - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in
+ - warning: The order in which users are returned are not guarenteed. You shouldn't expect results in
any particular order.
*/
func fetchAll(options: API.Options = []) throws -> [(Result)] {
if (allSatisfy { $0.className == Self.Element.className}) {
- let uniqueObjectIds = Set(compactMap { $0.objectId })
+ let uniqueObjectIds = Set(self.compactMap { $0.objectId })
let query = Self.Element.query(containedIn(key: "objectId", array: [uniqueObjectIds]))
let fetchedObjects = try query.find(options: options)
var fetchedObjectsToReturn = [(Result)]()
@@ -572,14 +576,14 @@ public extension Sequence where Element: ParseUser {
}
/**
- Fetches a collection of objects all at once *asynchronously* and executes the completion block when done.
+ 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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to fetch users. 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 fetched has the same objectId as current, it will automatically update the current.
- - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in
+ - warning: The order in which users are returned are not guarenteed. You shouldn't expect results in
any particular order.
*/
func fetchAll(
@@ -619,11 +623,11 @@ public extension Sequence where Element: ParseUser {
}
/**
- Deletes a collection of objects *synchronously* all at once and throws an error if necessary.
+ Deletes a collection of users *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 options: A set of options used to delete users. Defaults to an empty set.
- - returns: Returns a Result enum with `true` if the delete successful or a `ParseError` if it failed.
+ - 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
array of other Parse.Error objects. Each error object in this array
has an "object" property that references the object that could not be
@@ -634,9 +638,9 @@ 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 -> [(Result)] {
+ func deleteAll(options: API.Options = []) throws -> [ParseError?] {
let commands = try map { try $0.deleteCommand() }
- let returnResults = try API.Command
+ let returnResults = try API.Command
.batch(commands: commands)
.execute(options: options)
@@ -645,13 +649,13 @@ public extension Sequence where Element: ParseUser {
}
/**
- Deletes a collection of objects all at once *asynchronously* and executes the completion block when done.
+ 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 objects. Defaults to an empty set.
+ - parameter options: A set of options used to delete users. 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>)`.
- Each element in the array is a Result enum with `true` if the delete successful or a `ParseError` if it failed.
+ It should have the following argument signature: `(Result<[ParseError?], ParseError>)`.
+ Each element in the array is `nil` if the delete successful or a `ParseError` if it failed.
1. A `ParseError.Code.aggregateError`. This object's "errors" property is an
array of other Parse.Error objects. Each error object in this array
has an "object" property that references the object that could not be
@@ -664,18 +668,18 @@ public extension Sequence where Element: ParseUser {
func deleteAll(
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
- completion: @escaping (Result<[(Result)], ParseError>) -> Void
+ completion: @escaping (Result<[ParseError?], ParseError>) -> Void
) {
do {
let commands = try map({ try $0.deleteCommand() })
- API.Command
+ API.Command
.batch(commands: commands)
.executeAsync(options: options,
callbackQueue: callbackQueue) { results in
switch results {
case .success(let deleted):
- try? Self.Element.updateKeychainIfNeeded(compactMap {$0})
+ try? Self.Element.updateKeychainIfNeeded(self.compactMap {$0})
completion(.success(deleted))
case .failure(let error):
completion(.failure(error))
diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift b/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift
index 4fe31fe87..809239626 100644
--- a/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift
+++ b/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift
@@ -6,7 +6,7 @@
// Copyright © 2020 Parse. All rights reserved.
//
-public protocol Fetchable: Codable {
+public protocol Fetchable: Decodable {
associatedtype FetchingType
func fetch(options: API.Options) throws -> FetchingType
diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Fileable.swift b/Sources/ParseSwift/Object Protocols/Protocols/Fileable.swift
new file mode 100644
index 000000000..f0467a074
--- /dev/null
+++ b/Sources/ParseSwift/Object Protocols/Protocols/Fileable.swift
@@ -0,0 +1,39 @@
+//
+// Fileable.swift
+// ParseSwift
+//
+// Created by Corey Baker on 12/27/20.
+// Copyright © 2020 Parse Community. All rights reserved.
+//
+
+import Foundation
+
+protocol Fileable: Encodable {
+ 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 {
+ var isSaved: Bool {
+ return url != nil
+ }
+
+ // Equatable
+ 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 lURL == rURL
+ }
+
+ //Hashable
+ public func hash(into hasher: inout Hasher) {
+ var fileable = self
+ hasher.combine(fileable.localUUID)
+ }
+}
diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Saveable.swift b/Sources/ParseSwift/Object Protocols/Protocols/Savable.swift
similarity index 81%
rename from Sources/ParseSwift/Object Protocols/Protocols/Saveable.swift
rename to Sources/ParseSwift/Object Protocols/Protocols/Savable.swift
index c3a36efe5..77d1a73cc 100644
--- a/Sources/ParseSwift/Object Protocols/Protocols/Saveable.swift
+++ b/Sources/ParseSwift/Object Protocols/Protocols/Savable.swift
@@ -1,19 +1,19 @@
//
-// Saveable.swift
+// Savable.swift
// ParseSwift
//
// Created by Florent Vilmart on 17-07-24.
// Copyright © 2020 Parse. All rights reserved.
//
-public protocol Saveable: Codable {
+public protocol Savable: Encodable {
associatedtype SavingType
func save(options: API.Options) throws -> SavingType
func save() throws -> SavingType
}
-extension Saveable {
+extension Savable {
public func save() throws -> SavingType {
try save(options: [])
}
diff --git a/Sources/ParseSwift/Parse Types/File.swift b/Sources/ParseSwift/Parse Types/File.swift
deleted file mode 100644
index 0574916d2..000000000
--- a/Sources/ParseSwift/Parse Types/File.swift
+++ /dev/null
@@ -1,65 +0,0 @@
-import Foundation
-
-/**
- A `File` 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 File: Saveable, Fetchable {
-
- private let __type: String = "File" // swiftlint:disable:this identifier_name
-
- /**
- The name of the file.
- Before the file is saved, this is the filename given by the user.
- After the file is saved, that name gets prefixed with a unique identifier.
- */
- public var name: String?
-
- /**
- The contents of the file.
- */
- public var data: Data?
-
- /**
- The url of the file.
- */
- public var url: URL?
-
- internal init(data: Data?, url: URL?) {
- self.data = data
- self.url = url
- }
-
- public func save(options: API.Options) throws -> File {
- // upload file
- // store in server
- // callback with the data
- fatalError()
- }
-
- public func encode(to encoder: Encoder) throws {
- if data == nil && url == nil {
- throw ParseError(code: .unknownError, message: "cannot encode file")
- }
- var container = encoder.container(keyedBy: CodingKeys.self)
- if let url = url {
- try container.encode(__type, forKey: .__type)
- try container.encode(url.absoluteString, forKey: .url)
- }
- if let data = data {
- try container.encode(__type, forKey: .__type)
- try container.encode(data, forKey: .data)
- }
- }
-
- public func fetch(options: API.Options) -> File {
- fatalError()
- }
-
- enum CodingKeys: String, CodingKey {
- case url
- case data
- case name
- case __type // swiftlint:disable:this identifier_name
- }
-}
diff --git a/Sources/ParseSwift/Parse Types/Internal/FindResult.swift b/Sources/ParseSwift/Parse Types/Internal/FindResult.swift
deleted file mode 100644
index 31b115ee7..000000000
--- a/Sources/ParseSwift/Parse Types/Internal/FindResult.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-//
-// FindResult.swift
-// ParseSwift
-//
-// Created by Florent Vilmart on 17-07-24.
-// Copyright © 2020 Parse Community. All rights reserved.
-//
-
-internal struct FindResult: Codable where T: ParseObject {
- let results: [T]
- let count: Int?
-}
diff --git a/Sources/ParseSwift/Parse Types/Internal/ParseHash.swift b/Sources/ParseSwift/Parse Types/Internal/ParseHash.swift
new file mode 100644
index 000000000..3752010cb
--- /dev/null
+++ b/Sources/ParseSwift/Parse Types/Internal/ParseHash.swift
@@ -0,0 +1,36 @@
+//
+// ParseHash.swift
+// ParseSwift
+//
+// Created by Corey Baker on 12/22/20.
+// Copyright © 2020 Parse Community. All rights reserved.
+//
+
+import Foundation
+import CommonCrypto
+
+struct ParseHash {
+ static func md5HashFromData(_ data: Data) -> String {
+ var dataBytes = [UInt8](repeating: 0, count: data.count)
+ data.copyBytes(to: &dataBytes, count: data.count)
+
+ var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
+ var md5 = CC_MD5_CTX()
+ CC_MD5_Init(&md5)
+ CC_MD5_Update(&md5, dataBytes, CC_LONG(data.count))
+ CC_MD5_Final(&digest, &md5)
+
+ return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
+ digest[0], digest[1], digest[2], digest[3],
+ digest[4], digest[5], digest[6], digest[7],
+ digest[8], digest[9], digest[10], digest[11],
+ digest[12], digest[13], digest[14], digest[15])
+ }
+
+ static func md5HashFromString(_ string: String) -> String? {
+ guard let data = string.data(using: .utf8) else {
+ return nil
+ }
+ return md5HashFromData(data)
+ }
+}
diff --git a/Sources/ParseSwift/Parse Types/ParseFile.swift b/Sources/ParseSwift/Parse Types/ParseFile.swift
new file mode 100644
index 000000000..d0ed7264d
--- /dev/null
+++ b/Sources/ParseSwift/Parse Types/ParseFile.swift
@@ -0,0 +1,611 @@
+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 {
+
+ internal let __type: String = "File" // swiftlint:disable:this identifier_name
+
+ internal var isDownloadNeeded: Bool {
+ return cloudURL != nil
+ && url == nil
+ && localURL == nil
+ && 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!
+ }
+ }
+
+ /**
+ The name of the file.
+ Before the file is saved, this is the filename given by the user.
+ After the file is saved, that name gets prefixed with a unique identifier.
+ */
+ public internal(set) var name: String
+
+ /**
+ The Parse Server url of the file.
+ */
+ public internal(set) var url: URL?
+
+ /**
+ The local file path.
+ */
+ public var localURL: URL?
+
+ /**
+ The link to the file online that should be downloaded.
+ */
+ public var cloudURL: URL?
+
+ /**
+ The contents of the file.
+ */
+ public var data: Data?
+
+ /// The Content-Type header to use for the file.
+ public var mimeType: String?
+
+ /// Key value pairs to be stored with file object
+ public var metadata: [String: String]?
+
+ /// Key value pairs to be stored with file object
+ public var tags: [String: String]?
+
+ /// A set of options used to delete files.
+ public var options: API.Options = []
+
+ /**
+ Creates a file with given data and name.
+ - parameter name: The name of the new `ParseFile`. The file name must begin with and
+ alphanumeric character, and consist of alphanumeric characters, periods, spaces, underscores,
+ or dashes. The default value is "file".
+ - parameter data: The contents of the new `ParseFile`.
+ - 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
+ */
+ public init(name: String = "file", data: Data? = nil, mimeType: String? = nil,
+ metadata: [String: String]? = nil, tags: [String: String]? = nil,
+ options: API.Options = []) {
+ self.name = name
+ self.data = data
+ self.mimeType = mimeType
+ self.metadata = metadata
+ self.tags = tags
+ self.options = options
+ _ = self.localUUID //Need to ensure this creates a uuid
+ }
+
+ /**
+ Creates a file from a local file path and name.
+ - parameter name: The name of the new `ParseFile`. The file name must begin with and
+ alphanumeric character, and consist of alphanumeric characters, periods, spaces, underscores,
+ or dashes. The default value is "file".
+ - parameter localURL: The local file path of the`ParseFile`.
+ - 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
+ */
+ public init(name: String = "file", localURL: URL,
+ metadata: [String: String]? = nil, tags: [String: String]? = nil,
+ options: API.Options = []) {
+ self.name = name
+ self.localURL = localURL
+ self.metadata = metadata
+ self.tags = tags
+ self.options = options
+ _ = self.localUUID //Need to ensure this creates a uuid
+ }
+
+ /**
+ Creates a file from a link online and name.
+ - parameter name: The name of the new `ParseFile`. The file name must begin with and
+ alphanumeric character, and consist of alphanumeric characters, periods, spaces, underscores,
+ or dashes. The default value is "file".
+ - parameter cloudURL: The online link of the`ParseFile`.
+ - 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
+ */
+ public init(name: String = "file", cloudURL: URL,
+ metadata: [String: String]? = nil, tags: [String: String]? = nil,
+ options: API.Options = []) {
+ self.name = name
+ self.cloudURL = cloudURL
+ self.metadata = metadata
+ self.tags = tags
+ self.options = options
+ _ = self.localUUID //Need to ensure this creates a uuid
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case url
+ case name
+ case __type // swiftlint:disable:this identifier_name
+ }
+}
+
+// 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.
+ - throws: A `ParseError` if there was an issue deleting the file. Otherwise it was successful.
+ */
+ public func delete(options: API.Options) throws {
+ var options = options
+ options = options.union(self.options)
+
+ if !options.contains(.useMasterKey) {
+ throw ParseError(code: .unknownError,
+ message: "You must specify \"useMasterKey\" in \"options\" in order to delete a file.")
+ }
+ _ = try deleteFileCommand().execute(options: options)
+ }
+
+ /**
+ 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 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?)`
+ */
+ public func delete(options: API.Options,
+ callbackQueue: DispatchQueue = .main,
+ completion: @escaping (ParseError?) -> Void) {
+ var options = options
+ options = options.union(self.options)
+
+ if !options.contains(.useMasterKey) {
+ completion(ParseError(code: .unknownError,
+ // swiftlint:disable:next line_length
+ message: "You must specify \"useMasterKey\" in \"options\" in order to delete a file."))
+ return
+ }
+ deleteFileCommand().executeAsync(options: options,
+ callbackQueue: callbackQueue) { result in
+ switch result {
+
+ case .success:
+ completion(nil)
+ case .failure(let error):
+ completion(error)
+ }
+ }
+ }
+
+ internal func deleteFileCommand() -> API.Command {
+ return API.Command.deleteFileCommand(self)
+ }
+}
+
+// MARK: Saving
+extension ParseFile {
+ /**
+ Creates a file with given stream *synchronously*. A name will be assigned to it by the server.
+
+ **Checking progress**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.fetch(stream: InputStream(fileAtPath: URL("parse.org")!) {
+ (_, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ print(currentProgess)
+ }
+
+ **Cancelling**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.fetch(stream: InputStream(fileAtPath: URL("parse.org")!){
+ (task, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ //Cancel when data exceeds 10%
+ if currentProgess > 10 {
+ task.cancel()
+ print("task has been cancelled")
+ }
+ print(currentProgess)
+ }
+
+ - parameter options: A set of options used to save files. 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)`.
+ - parameter stream: An input file stream.
+ - returns: A saved `ParseFile`.
+ */
+ public func save(options: API.Options = [],
+ stream: InputStream,
+ progress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil) throws {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ return try uploadFileCommand().executeStream(options: options, uploadProgress: progress, stream: stream)
+ }
+
+ /**
+ 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.
+ - returns: A saved `ParseFile`.
+ */
+ public func save(options: API.Options = []) throws -> ParseFile {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ if isDownloadNeeded {
+ let fetched = try fetch(options: options)
+ return try fetched.uploadFileCommand().execute(options: options)
+ }
+ return try uploadFileCommand().execute(options: options)
+ }
+
+ /**
+ 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.
+
+ **Checking progress**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.save { (_, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ print(currentProgess)
+ }
+
+ **Cancelling**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.save { (task, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ //Cancel when data exceeds 10%
+ if currentProgess > 10 {
+ task.cancel()
+ print("task has been cancelled")
+ }
+ print(currentProgess)
+ }
+
+ - parameter options: A set of options used to save files. 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)`.
+ - returns: A saved `ParseFile`.
+ */
+ public func save(options: API.Options = [],
+ progress: ((URLSessionTask, Int64, Int64, Int64) -> Void)?) throws -> ParseFile {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ if isDownloadNeeded {
+ let fetched = try fetch(options: options)
+ return try fetched.uploadFileCommand().execute(options: options, uploadProgress: progress)
+ }
+ return try uploadFileCommand().execute(options: options, uploadProgress: progress)
+ }
+
+ /**
+ Creates a file with given data *asynchronously* and executes the given callback block.
+ A name will be assigned to it by the server. If the file hasn't been downloaded, it will automatically
+ be downloaded before saved.
+
+ **Checking progress**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.save { (_, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ print(currentProgess)
+ }
+
+ **Cancelling**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.save(progress: {(task, _, totalWritten, totalExpected)-> Void in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ //Cancel when data exceeds 10%
+ if currentProgess > 10 {
+ task.cancel()
+ print("task has been cancelled")
+ }
+ print(currentProgess)
+ }) { result in
+ ...
+ })
+
+ - parameter options: A set of options used to save files. 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,
+ bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`.
+ - parameter completion: A block that will be called when file saves or fails.
+ It should have the following argument signature: `(Result)`
+ */
+ public func save(options: API.Options = [],
+ callbackQueue: DispatchQueue = .main,
+ progress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil,
+ completion: @escaping (Result) -> Void) {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ if isDownloadNeeded {
+ fetch(options: options) { result in
+ switch result {
+
+ case .success(let fetched):
+ fetched.uploadFileCommand()
+ .executeAsync(options: options,
+ callbackQueue: callbackQueue,
+ uploadProgress: progress,
+ completion: completion)
+ case .failure(let error):
+ completion(.failure(error))
+ }
+ }
+ } else {
+ uploadFileCommand()
+ .executeAsync(options: options,
+ callbackQueue: callbackQueue,
+ uploadProgress: progress,
+ completion: completion)
+ }
+
+ }
+
+ internal func uploadFileCommand() -> API.Command {
+ return API.Command.uploadFileCommand(self)
+ }
+}
+
+// MARK: Downloading
+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 stream: An input file stream.
+ - returns: A saved `ParseFile`.
+ */
+ public func fetch(options: API.Options = [],
+ stream: InputStream) throws {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ return try downloadFileCommand().executeStream(options: options, stream: stream)
+ }
+
+ /**
+ Fetches a file with given url *synchronously*.
+ - parameter options: A set of options used to fetch the file. Defaults to an empty set.
+ - returns: A saved `ParseFile`.
+ */
+ public func fetch(options: API.Options = []) throws -> ParseFile {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ return try downloadFileCommand().execute(options: options)
+ }
+
+ /**
+ Fetches a file with given url *synchronously*.
+
+ **Checking progress**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.fetch { (_, _, totalDownloaded, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ print(currentProgess)
+ }
+
+ **Cancelling**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.fetch { (task, _, totalDownloaded, totalExpected) in
+ let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100
+ //Cancel when data exceeds 10%
+ if currentProgess > 10 {
+ task.cancel()
+ print("task has been cancelled")
+ }
+ print(currentProgess)
+ }
+
+ - parameter options: A set of options used to fetch the file. 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)`.
+ - returns: A saved `ParseFile`.
+ */
+ public func fetch(options: API.Options = [],
+ progress: @escaping ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)) throws -> ParseFile {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ return try downloadFileCommand().execute(options: options, downloadProgress: progress)
+ }
+
+ /**
+ Fetches a file with given url *asynchronously*.
+
+ **Checking progress**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.fetch { (_, _, totalDownloaded, totalExpected) in
+ let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100
+ print(currentProgess)
+ }
+
+ **Cancelling**
+
+ guard let parseFileURL = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: parseFileURL)
+ let fetchedFile = try parseFile.fetch(progress: {(task, _, totalDownloaded, totalExpected)-> Void in
+ let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100
+ //Cancel when data exceeds 10%
+ if currentProgess > 10 {
+ task.cancel()
+ print("task has been cancelled")
+ }
+ print(currentProgess)
+ }) { result in
+ ...
+ }
+
+ - parameter options: A set of options used to fetch the file. 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,
+ bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`.
+ - parameter completion: A block that will be called when file fetches or fails.
+ It should have the following argument signature: `(Result)`
+ */
+ public func fetch(options: API.Options = [],
+ callbackQueue: DispatchQueue = .main,
+ progress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil,
+ completion: @escaping (Result) -> Void) {
+ var options = options
+ if let mimeType = mimeType {
+ options.insert(.mimeType(mimeType))
+ } else {
+ options.insert(.removeMimeType)
+ }
+ if let metadata = metadata {
+ options.insert(.metadata(metadata))
+ }
+ if let tags = tags {
+ options.insert(.tags(tags))
+ }
+ options = options.union(self.options)
+ downloadFileCommand().executeAsync(options: options,
+ callbackQueue: callbackQueue,
+ downloadProgress: progress, completion: completion)
+ }
+
+ internal func downloadFileCommand() -> API.Command {
+ return API.Command.downloadFileCommand(self)
+ }
+} // swiftlint:disable:this file_length
diff --git a/Sources/ParseSwift/Parse Types/Query.swift b/Sources/ParseSwift/Parse Types/Query.swift
index ed4b0bec9..1d5962a4f 100644
--- a/Sources/ParseSwift/Parse Types/Query.swift
+++ b/Sources/ParseSwift/Parse Types/Query.swift
@@ -891,7 +891,7 @@ extension Query: Queryable {
private extension Query {
private func findCommand() -> API.Command, [ResultType]> {
return API.Command(method: .POST, path: endpoint, body: self) {
- try ParseCoding.jsonDecoder().decode(FindResult.self, from: $0).results
+ try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).results
}
}
@@ -899,7 +899,7 @@ private extension Query {
let query = self
query.limit = 1
return API.Command(method: .POST, path: endpoint, body: query) {
- try ParseCoding.jsonDecoder().decode(FindResult.self, from: $0).results.first
+ try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).results.first
}
}
@@ -908,7 +908,7 @@ private extension Query {
query.limit = 1
query.isCount = true
return API.Command(method: .POST, path: endpoint, body: query) {
- try ParseCoding.jsonDecoder().decode(FindResult.self, from: $0).count ?? 0
+ try ParseCoding.jsonDecoder().decode(QueryResponse.self, from: $0).count ?? 0
}
}
diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift
index 637e1f053..9a98ba4e4 100644
--- a/Sources/ParseSwift/Parse.swift
+++ b/Sources/ParseSwift/Parse.swift
@@ -6,6 +6,7 @@ internal struct ParseConfiguration {
static var clientKey: String?
static var serverURL: URL!
static var mountPath: String!
+ static var isTestingSDK = false //Enable this only for certain tests like ParseFile
}
// swiftlint:disable:next inclusive_language
@@ -29,3 +30,8 @@ public func initialize(
_ = BaseParseInstallation()
}
}
+
+internal func setupForTesting() {
+ ParseConfiguration.isTestingSDK = true
+ _ = URLSession.testing
+}
diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift
index 540b47033..8f3572aec 100644
--- a/Sources/ParseSwift/ParseConstants.swift
+++ b/Sources/ParseSwift/ParseConstants.swift
@@ -11,6 +11,10 @@ import Foundation
enum ParseConstants {
static let parseVersion = "0.0.1"
static let hashingKey = "parseSwift"
+ static let fileManagementDirectory = "parse/"
+ static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
+ static let fileManagementLibraryDirectory = "Library/"
+ static let fileDownloadsDirectory = "Downloads"
#if os(iOS)
static let deviceType = "ios"
#elseif os(macOS)
diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift
new file mode 100644
index 000000000..5432139df
--- /dev/null
+++ b/Sources/ParseSwift/Storage/ParseFileManager.swift
@@ -0,0 +1,194 @@
+//
+// ParseFileManager.swift
+// ParseSwift
+//
+// Created by Corey Baker on 12/20/20.
+// Copyright © 2020 Parse Community. All rights reserved.
+//
+
+import Foundation
+
+internal struct ParseFileManager {
+
+ private var defaultDirectoryAttributes: [FileAttributeKey: Any]? {
+ #if os(macOS) || os(Linux)
+ return nil
+ #else
+ return [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication]
+ #endif
+ }
+
+ private var defaultDataWritingOptions: Data.WritingOptions {
+ var options = Data.WritingOptions.atomic
+ #if !os(macOS) && !os(Linux)
+ options.insert(.completeFileProtectionUntilFirstUserAuthentication)
+ #endif
+ return options
+ }
+
+ private var localSandBoxDataDirectoryPath: URL? {
+ #if os(macOS) || os(Linux)
+ return self.defaultDataDirectoryPath
+ #else
+ // swiftlint:disable:next line_length
+ let directoryPath = "\(NSHomeDirectory())/\(ParseConstants.fileManagementLibraryDirectory)\(ParseConstants.fileManagementPrivateDocumentsDirectory)\(ParseConstants.fileManagementDirectory)"
+ guard (try? createDirectoryIfNeeded(directoryPath)) != nil else {
+ return nil
+ }
+ return URL(fileURLWithPath: directoryPath, isDirectory: true)
+ #endif
+ }
+
+ private let synchronizationQueue = DispatchQueue(label: "com.parse.file",
+ qos: .default,
+ attributes: .concurrent,
+ autoreleaseFrequency: .inherit,
+ target: nil)
+
+ private let applicationIdentifier: String
+ private let applicationGroupIdentifer: String?
+
+ public var defaultDataDirectoryPath: URL? {
+ #if os(macOS) || os(Linux)
+ var directoryPath: String!
+ let paths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)
+ guard let directory = paths.first else {
+ return nil
+ }
+ directoryPath = directory
+ directoryPath += "/\(ParseConstants.fileManagementDirectory)\(applicationIdentifier)"
+ return URL(fileURLWithPath: directoryPath, isDirectory: true)
+ #else
+ if let groupIdentifier = applicationGroupIdentifer {
+ guard var directory = FileManager
+ .default
+ .containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) else {
+ return nil
+ }
+ directory.appendPathComponent(ParseConstants.fileManagementDirectory)
+ directory.appendPathComponent(applicationIdentifier)
+ return directory
+ } else {
+ return self.localSandBoxDataDirectoryPath
+ }
+ #endif
+ }
+
+ public func dataItemPathForPathComponent(_ component: String) -> URL? {
+ guard var path = self.defaultDataDirectoryPath else {
+ return nil
+ }
+ path.appendPathComponent(component)
+ return path
+ }
+
+ init?() {
+ if let identifier = Bundle.main.bundleIdentifier {
+ applicationIdentifier = identifier
+ } else {
+ return nil
+ }
+ applicationGroupIdentifer = nil
+ }
+
+ func createDirectoryIfNeeded(_ path: String) throws {
+ if !FileManager.default.fileExists(atPath: path) {
+ try FileManager.default.createDirectory(atPath: path,
+ withIntermediateDirectories: true,
+ attributes: defaultDirectoryAttributes)
+ }
+ }
+
+ func writeString(_ string: String, filePath: URL, completion: @escaping(Error?) -> Void) {
+ synchronizationQueue.async {
+ do {
+ guard let data = string.data(using: .utf8) else {
+ completion(ParseError(code: .unknownError, message: "Couldn't convert string to utf8"))
+ return
+ }
+ try data.write(to: filePath, options: defaultDataWritingOptions)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ func writeData(_ data: Data, filePath: URL, completion: @escaping(Error?) -> Void) {
+ synchronizationQueue.async {
+ do {
+ try data.write(to: filePath, options: defaultDataWritingOptions)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ func copyItem(_ fromPath: URL, toPath: URL, completion: @escaping(Error?) -> Void) {
+ synchronizationQueue.async {
+ do {
+ try FileManager.default.copyItem(at: fromPath, to: toPath)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ func moveItem(_ fromPath: URL, toPath: URL, completion: @escaping(Error?) -> Void) {
+ synchronizationQueue.async {
+ do {
+ try FileManager.default.moveItem(at: fromPath, to: toPath)
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ func moveContentsOfDirectory(_ fromPath: URL, toPath: URL, completion: @escaping(Error?) -> Void) {
+ synchronizationQueue.async {
+ do {
+ if fromPath == toPath {
+ completion(nil)
+ return
+ }
+
+ try createDirectoryIfNeeded(toPath.path)
+ let contents = try FileManager.default.contentsOfDirectory(atPath: fromPath.path)
+ if contents.count == 0 {
+ completion(nil)
+ return
+ }
+ try contents.forEach {
+ let fromFilePath = fromPath.appendingPathComponent($0)
+ let toFilePath = toPath.appendingPathComponent($0)
+ try FileManager.default.moveItem(at: fromFilePath, to: toFilePath)
+ }
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+
+ func removeDirectoryContents(_ path: URL, completion: @escaping(Error?) -> Void) {
+ synchronizationQueue.async {
+ do {
+ let contents = try FileManager.default.contentsOfDirectory(atPath: path.path)
+ if contents.count == 0 {
+ completion(nil)
+ return
+ }
+ try contents.forEach {
+ let filePath = path.appendingPathComponent($0)
+ try FileManager.default.removeItem(at: filePath)
+ }
+ completion(nil)
+ } catch {
+ completion(error)
+ }
+ }
+ }
+}
diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift
index 4279447c1..e8cab6681 100644
--- a/Tests/ParseSwiftTests/APICommandTests.swift
+++ b/Tests/ParseSwiftTests/APICommandTests.swift
@@ -115,28 +115,6 @@ class APICommandTests: XCTestCase {
//This is less common as the HTTP won't be able to produce ParseErrors directly, but used for testing
func testErrorHTTPReturnsParseError1() {
- let originalError = ParseError(code: .connectionFailed, message: "no connection")
- MockURLProtocol.mockRequests { response in
- let response = MockURLResponse(error: originalError)
- return response
- }
- do {
- _ = try API.Command(method: .GET, path: .login, params: nil,
- mapper: { (data) -> ParseError in
- return try JSONDecoder().decode(ParseError.self, from: data)
- }).execute(options: [])
- XCTFail("Should have thrown an error")
- } catch {
- guard let error = error as? ParseError else {
- XCTFail("should be able unwrap final error to ParseError")
- return
- }
- XCTAssertTrue(error.code == .unknownError)
- }
- }
-
- //This is less common as the HTTP won't be able to produce ParseErrors directly, but used for testing
- func testErrorHTTPReturnsParseError2() {
let originalError = ParseError(code: .unknownError, message: "Couldn't decode")
MockURLProtocol.mockRequests { _ in
return MockURLResponse(error: originalError)
diff --git a/Tests/ParseSwiftTests/HashTests.swift b/Tests/ParseSwiftTests/HashTests.swift
new file mode 100644
index 000000000..6091c15a3
--- /dev/null
+++ b/Tests/ParseSwiftTests/HashTests.swift
@@ -0,0 +1,21 @@
+//
+// HashTests.swift
+// ParseSwift
+//
+// Created by Corey Baker on 12/22/20.
+// Copyright © 2020 Parse Community. All rights reserved.
+//
+
+import Foundation
+import XCTest
+@testable import ParseSwift
+
+class HashTests: XCTestCase {
+ func testMD5SimpleHash() {
+ XCTAssertEqual("5eb63bbbe01eeed093cb22bb8f5acdc3", ParseHash.md5HashFromString("hello world"))
+ }
+
+ func testMD5HashFromUnicode() {
+ XCTAssertEqual("9c853e20bb12ff256734a992dd224f17", ParseHash.md5HashFromString("foo א"))
+ }
+}
diff --git a/Tests/ParseSwiftTests/NetworkMocking/MockURLProtocol.swift b/Tests/ParseSwiftTests/NetworkMocking/MockURLProtocol.swift
index 1f232bc4d..2d182c852 100644
--- a/Tests/ParseSwiftTests/NetworkMocking/MockURLProtocol.swift
+++ b/Tests/ParseSwiftTests/NetworkMocking/MockURLProtocol.swift
@@ -75,7 +75,6 @@ class MockURLProtocol: URLProtocol {
}
override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
- self.mock = nil
super.init(request: request, cachedResponse: cachedResponse, client: client)
guard let mock = MockURLProtocol.firstMockForRequest(request) else {
self.mock = nil
diff --git a/Tests/ParseSwiftTests/ParseFileManagerTests.swift b/Tests/ParseSwiftTests/ParseFileManagerTests.swift
new file mode 100644
index 000000000..a9a7b808a
--- /dev/null
+++ b/Tests/ParseSwiftTests/ParseFileManagerTests.swift
@@ -0,0 +1,203 @@
+//
+// ParseFileManagerTests.swift
+// ParseSwift
+//
+// Created by Corey Baker on 12/26/20.
+// Copyright © 2020 Parse Community. All rights reserved.
+//
+
+import Foundation
+import XCTest
+@testable import ParseSwift
+
+struct FileUploadResponse: Codable {
+ let name: String
+ let url: URL
+}
+
+class ParseFileManagerTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ super.setUp()
+ guard let url = URL(string: "http://localhost:1337/1") else {
+ XCTFail("Should create valid URL")
+ return
+ }
+ ParseSwift.initialize(applicationId: "applicationId",
+ clientKey: "clientKey",
+ masterKey: "masterKey",
+ serverURL: url)
+
+ guard let fileManager = ParseFileManager(),
+ let defaultDirectory = fileManager.defaultDataDirectoryPath else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+ try fileManager.createDirectoryIfNeeded(defaultDirectory.relativePath)
+ }
+
+ override func tearDownWithError() throws {
+ super.tearDown()
+ MockURLProtocol.removeAll()
+ try KeychainStore.shared.deleteAll()
+ try ParseStorage.shared.deleteAll()
+
+ guard let fileManager = ParseFileManager(),
+ let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+
+ let expectation1 = XCTestExpectation(description: "Delete files1")
+ fileManager.removeDirectoryContents(defaultDirectoryPath) { error in
+ guard let error = error else {
+ expectation1.fulfill()
+ return
+ }
+ XCTFail(error.localizedDescription)
+ expectation1.fulfill()
+ }
+ let directory2 = defaultDirectoryPath
+ .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true)
+ let expectation2 = XCTestExpectation(description: "Delete files2")
+ fileManager.removeDirectoryContents(directory2) { _ in
+ expectation2.fulfill()
+ }
+ wait(for: [expectation1, expectation2], timeout: 20.0)
+ }
+
+ func testWriteData() throws {
+ guard let data = "Hello World".data(using: .utf8),
+ let fileManager = ParseFileManager(),
+ let filePath = fileManager.dataItemPathForPathComponent("test.txt") else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ fileManager.writeData(data, filePath: filePath) { error in
+ guard let error = error else {
+ guard let readFile = try? Data(contentsOf: filePath) else {
+ XCTFail("Should have read as string")
+ return
+ }
+ XCTAssertEqual(readFile, data)
+ expectation1.fulfill()
+ return
+ }
+ XCTFail(error.localizedDescription)
+ expectation1.fulfill()
+ }
+
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testCopyItem() throws {
+ let dataAsString = "Hello World"
+ guard let fileManager = ParseFileManager(),
+ let filePath = fileManager.dataItemPathForPathComponent("test.txt"),
+ let filePath2 = fileManager.dataItemPathForPathComponent("test2.txt") else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ fileManager.writeString(dataAsString, filePath: filePath) { error in
+ guard let error = error else {
+ guard let readFile = try? String(contentsOf: filePath) else {
+ XCTFail("Should have read as string")
+ return
+ }
+ XCTAssertEqual(readFile, dataAsString)
+
+ fileManager.copyItem(filePath, toPath: filePath2) { _ in
+ guard let readFile = try? String(contentsOf: filePath),
+ let readFile2 = try? String(contentsOf: filePath2) else {
+ XCTFail("Should have read as string")
+ return
+ }
+
+ XCTAssertEqual(readFile, dataAsString)
+ XCTAssertEqual(readFile2, dataAsString)
+ expectation1.fulfill()
+ }
+ return
+ }
+ XCTFail(error.localizedDescription)
+ expectation1.fulfill()
+ }
+
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testMoveItem() throws {
+ let dataAsString = "Hello World"
+ guard let fileManager = ParseFileManager(),
+ let filePath = fileManager.dataItemPathForPathComponent("test.txt"),
+ let filePath2 = fileManager.dataItemPathForPathComponent("test2.txt") else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ fileManager.writeString(dataAsString, filePath: filePath) { error in
+ guard let error = error else {
+ guard let readFile = try? String(contentsOf: filePath) else {
+ XCTFail("Should have read as string")
+ return
+ }
+ XCTAssertEqual(readFile, dataAsString)
+
+ fileManager.moveItem(filePath, toPath: filePath2) { _ in
+ guard let readFile2 = try? String(contentsOf: filePath2) else {
+ XCTFail("Should have read as string")
+ return
+ }
+ XCTAssertFalse(FileManager.default.fileExists(atPath: filePath.relativePath))
+ XCTAssertEqual(readFile2, dataAsString)
+ expectation1.fulfill()
+ }
+ return
+ }
+ XCTFail(error.localizedDescription)
+ expectation1.fulfill()
+ }
+
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testMoveContentsOfDirectory() throws {
+ let dataAsString = "Hello World"
+ guard let fileManager = ParseFileManager(),
+ let defaultFilePath = fileManager.defaultDataDirectoryPath else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+
+ let oldPath = defaultFilePath.appendingPathComponent("old")
+ try fileManager.createDirectoryIfNeeded(oldPath.relativePath)
+ let filePath = oldPath.appendingPathComponent("test.txt")
+ let filePath2 = defaultFilePath.appendingPathComponent("new/")
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ fileManager.writeString(dataAsString, filePath: filePath) { error in
+ guard let error = error else {
+ guard let readFile = try? String(contentsOf: filePath) else {
+ XCTFail("Should have read as string")
+ return
+ }
+ XCTAssertEqual(readFile, dataAsString)
+
+ fileManager.moveContentsOfDirectory(oldPath, toPath: filePath2) { _ in
+ let movedFilePath = filePath2.appendingPathComponent("test.txt")
+ guard let readFile2 = try? String(contentsOf: movedFilePath) else {
+ XCTFail("Should have read as string")
+ return
+ }
+ XCTAssertFalse(FileManager.default.fileExists(atPath: filePath.relativePath))
+ XCTAssertEqual(readFile2, dataAsString)
+ expectation1.fulfill()
+ }
+ return
+ }
+ XCTFail(error.localizedDescription)
+ expectation1.fulfill()
+ }
+
+ wait(for: [expectation1], timeout: 20.0)
+ }
+}
diff --git a/Tests/ParseSwiftTests/ParseFileTests.swift b/Tests/ParseSwiftTests/ParseFileTests.swift
new file mode 100644
index 000000000..6c0812ea0
--- /dev/null
+++ b/Tests/ParseSwiftTests/ParseFileTests.swift
@@ -0,0 +1,1122 @@
+//
+// ParseFileTests.swift
+// ParseSwift
+//
+// Created by Corey Baker on 12/23/20.
+// Copyright © 2020 Parse Community. All rights reserved.
+//
+
+import Foundation
+import XCTest
+@testable import ParseSwift
+
+class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length
+
+ let temporaryDirectory = "\(NSTemporaryDirectory())test/"
+
+ struct FileUploadResponse: Codable {
+ let name: String
+ let url: URL
+ }
+
+ override func setUpWithError() throws {
+ super.setUp()
+ guard let url = URL(string: "http://localhost:1337/1") else {
+ XCTFail("Should create valid URL")
+ return
+ }
+ ParseSwift.initialize(applicationId: "applicationId",
+ clientKey: "clientKey",
+ masterKey: "masterKey",
+ serverURL: url)
+
+ guard let fileManager = ParseFileManager() else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+ try fileManager.createDirectoryIfNeeded(temporaryDirectory)
+ ParseSwift.setupForTesting()
+ }
+
+ override func tearDownWithError() throws {
+ super.tearDown()
+ MockURLProtocol.removeAll()
+ try KeychainStore.shared.deleteAll()
+ try ParseStorage.shared.deleteAll()
+
+ guard let fileManager = ParseFileManager(),
+ let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+ let directory = URL(fileURLWithPath: temporaryDirectory, isDirectory: true)
+ let expectation1 = XCTestExpectation(description: "Delete files1")
+ fileManager.removeDirectoryContents(directory) { error in
+ guard let error = error else {
+ expectation1.fulfill()
+ return
+ }
+ XCTFail(error.localizedDescription)
+ expectation1.fulfill()
+ }
+ let directory2 = defaultDirectoryPath
+ .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true)
+ let expectation2 = XCTestExpectation(description: "Delete files2")
+ fileManager.removeDirectoryContents(directory2) { _ in
+ expectation2.fulfill()
+ }
+ wait(for: [expectation1, expectation2], timeout: 20.0)
+ }
+
+ func testUploadCommand() {
+ guard let url = URL(string: "http://localhost/") else {
+ XCTFail("Should have created url")
+ return
+ }
+ let file = ParseFile(name: "a", cloudURL: url)
+
+ let command = file.uploadFileCommand()
+ XCTAssertNotNil(command)
+ XCTAssertEqual(command.path.urlComponent, "/files/a")
+ XCTAssertEqual(command.method, API.Method.POST)
+ XCTAssertNil(command.params)
+ XCTAssertNil(command.body)
+ XCTAssertNil(command.data)
+
+ let file2 = ParseFile(cloudURL: url)
+
+ let command2 = file2.uploadFileCommand()
+ XCTAssertNotNil(command2)
+ XCTAssertEqual(command2.path.urlComponent, "/files/file")
+ XCTAssertEqual(command2.method, API.Method.POST)
+ XCTAssertNil(command2.params)
+ XCTAssertNil(command2.body)
+ XCTAssertNil(command2.data)
+ }
+
+ func testDeleteCommand() {
+ guard let url = URL(string: "http://localhost/") else {
+ XCTFail("Should have created url")
+ return
+ }
+ var file = ParseFile(name: "a", cloudURL: url)
+ file.url = url
+ let command = file.deleteFileCommand()
+ XCTAssertNotNil(command)
+ XCTAssertEqual(command.path.urlComponent, "/files/a")
+ XCTAssertEqual(command.method, API.Method.DELETE)
+ XCTAssertNil(command.params)
+ XCTAssertNil(command.body)
+ XCTAssertNil(command.data)
+
+ var file2 = ParseFile(cloudURL: url)
+ file2.url = url
+ let command2 = file2.deleteFileCommand()
+ XCTAssertNotNil(command2)
+ XCTAssertEqual(command2.path.urlComponent, "/files/file")
+ XCTAssertEqual(command2.method, API.Method.DELETE)
+ XCTAssertNil(command2.params)
+ XCTAssertNil(command2.body)
+ XCTAssertNil(command2.data)
+ }
+
+ func testDownloadCommand() {
+ guard let url = URL(string: "http://localhost/") else {
+ XCTFail("Should have created url")
+ return
+ }
+ var file = ParseFile(name: "a", cloudURL: url)
+ file.url = url
+ let command = file.downloadFileCommand()
+ XCTAssertNotNil(command)
+ XCTAssertEqual(command.path.urlComponent, "/files/a")
+ XCTAssertEqual(command.method, API.Method.GET)
+ XCTAssertNil(command.params)
+ XCTAssertNil(command.body)
+ XCTAssertNil(command.data)
+
+ let file2 = ParseFile(cloudURL: url)
+ let command2 = file2.downloadFileCommand()
+ XCTAssertNotNil(command2)
+ XCTAssertEqual(command2.path.urlComponent, "/files/file")
+ XCTAssertEqual(command2.method, API.Method.GET)
+ XCTAssertNil(command2.params)
+ XCTAssertNil(command2.body)
+ XCTAssertNil(command2.data)
+ }
+
+ func testLocalUUID() throws {
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ let parseFile = ParseFile(name: "sampleData.txt", data: sampleData)
+ let localUUID = parseFile._localUUID
+ XCTAssertNotNil(localUUID)
+ XCTAssertEqual(localUUID,
+ parseFile._localUUID,
+ "localUUID should remain the same no matter how many times the getter is called")
+ }
+
+ func testFileEquality() throws {
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+
+ guard let url1 = URL(string: "https://parseplatform.org/img/logo.svg"),
+ let url2 = URL(string: "https://parseplatform.org/img/logo2.svg") else {
+ throw ParseError(code: .unknownError, message: "Should have created urls")
+ }
+
+ var parseFile1 = ParseFile(name: "sampleData.txt", data: sampleData)
+ parseFile1.url = url1
+ var parseFile2 = ParseFile(name: "sampleData2.txt", data: sampleData)
+ parseFile2.url = url2
+ var parseFile3 = ParseFile(name: "sampleData3.txt", data: sampleData)
+ parseFile3.url = url1
+ XCTAssertNotEqual(parseFile1, parseFile2, "different urls, url takes precedence over localUUID")
+ XCTAssertEqual(parseFile1, parseFile3, "same urls")
+ parseFile1.url = nil
+ parseFile2.url = nil
+ XCTAssertNotEqual(parseFile1, parseFile2, "no urls, but localUUIDs shoud be different")
+ let uuid = UUID()
+ parseFile1._localUUID = uuid
+ parseFile2._localUUID = uuid
+ XCTAssertEqual(parseFile1, parseFile2, "no urls, but localUUIDs shoud be the same")
+ }
+
+ func testSave() throws {
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ let parseFile = ParseFile(name: "sampleData.txt",
+ data: sampleData,
+ metadata: ["Testing": "123"],
+ tags: ["Hey": "now"])
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let savedFile = try parseFile.save()
+ XCTAssertEqual(savedFile.name, response.name)
+ XCTAssertEqual(savedFile.url, response.url)
+ }
+
+ func testSaveWithSpecifyingMime() throws {
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ let parseFile = ParseFile(data: sampleData, mimeType: "application/txt")
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_file") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let savedFile = try parseFile.save()
+ XCTAssertEqual(savedFile.name, response.name)
+ XCTAssertEqual(savedFile.url, response.url)
+ }
+
+ func testSaveLocalFile() throws {
+ let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.txt")
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ try sampleData.write(to: tempFilePath)
+
+ let parseFile = ParseFile(name: "sampleData.txt", localURL: tempFilePath)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let savedFile = try parseFile.save()
+ XCTAssertEqual(savedFile.name, response.name)
+ XCTAssertEqual(savedFile.url, response.url)
+ XCTAssertEqual(savedFile.localURL, tempFilePath)
+ }
+
+ func testSaveCloudFile() throws {
+ guard let tempFilePath = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: tempFilePath)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let savedFile = try parseFile.save()
+ XCTAssertEqual(savedFile.name, response.name)
+ XCTAssertEqual(savedFile.url, response.url)
+ XCTAssertEqual(savedFile.cloudURL, tempFilePath)
+ XCTAssertNotNil(savedFile.localURL)
+ }
+
+ func testCloudFileProgress() throws {
+ guard let tempFilePath = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: tempFilePath)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let savedFile = try parseFile.save { (_, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ XCTAssertGreaterThan(currentProgess, -1)
+ }
+ XCTAssertEqual(savedFile.name, response.name)
+ XCTAssertEqual(savedFile.url, response.url)
+ XCTAssertEqual(savedFile.cloudURL, tempFilePath)
+ XCTAssertNotNil(savedFile.localURL)
+ }
+
+ func testCloudFileCancel() throws {
+ guard let tempFilePath = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: tempFilePath)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let savedFile = try parseFile.save { (task, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ if currentProgess > 10 {
+ task.cancel()
+ }
+ }
+ XCTAssertEqual(savedFile.name, response.name)
+ XCTAssertEqual(savedFile.url, response.url)
+ XCTAssertEqual(savedFile.cloudURL, tempFilePath)
+ XCTAssertNotNil(savedFile.localURL)
+ }
+
+ func testSaveFileStream() throws {
+ let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.dat")
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ try sampleData.write(to: tempFilePath)
+
+ let parseFile = ParseFile(name: "sampleData.data")
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ guard let stream = InputStream(fileAtPath: tempFilePath.relativePath) else {
+ throw ParseError(code: .unknownError, message: "Should have created file stream")
+ }
+ try parseFile.save(options: [], stream: stream, progress: nil)
+ }
+
+ func testSaveFileStreamProgress() throws {
+ let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.dat")
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ try sampleData.write(to: tempFilePath)
+
+ let parseFile = ParseFile(name: "sampleData.data")
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ guard let stream = InputStream(fileAtPath: tempFilePath.relativePath) else {
+ throw ParseError(code: .unknownError, message: "Should have created file stream")
+ }
+
+ try parseFile.save(stream: stream) { (_, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ XCTAssertGreaterThan(currentProgess, -1)
+ }
+ }
+
+ func testSaveFileStreamCancel() throws {
+ let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.dat")
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ try sampleData.write(to: tempFilePath)
+
+ let parseFile = ParseFile(name: "sampleData.data")
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ guard let stream = InputStream(fileAtPath: tempFilePath.relativePath) else {
+ throw ParseError(code: .unknownError, message: "Should have created file stream")
+ }
+
+ try parseFile.save(stream: stream) { (task, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ if currentProgess > 10 {
+ task.cancel()
+ }
+ }
+ }
+
+ func testFetchFile() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let fetchedFile = try parseFile.fetch()
+ XCTAssertEqual(fetchedFile.name, response.name)
+ XCTAssertEqual(fetchedFile.url, response.url)
+ XCTAssertNotNil(fetchedFile.localURL)
+ }
+
+ func testFetchFileProgress() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let fetchedFile = try parseFile.fetch { (_, _, totalDownloaded, totalExpected) in
+ let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100
+ XCTAssertGreaterThan(currentProgess, -1)
+ }
+ XCTAssertEqual(fetchedFile.name, response.name)
+ XCTAssertEqual(fetchedFile.url, response.url)
+ XCTAssertNotNil(fetchedFile.localURL)
+ }
+
+ func testFetchFileStream() throws {
+ let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.dat")
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ try sampleData.write(to: tempFilePath)
+
+ let parseFile = ParseFile(name: "sampleData.data")
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ guard let stream = InputStream(fileAtPath: tempFilePath.relativePath) else {
+ throw ParseError(code: .unknownError, message: "Should have created file stream")
+ }
+ try parseFile.fetch(stream: stream)
+ }
+
+ func testDeleteFile() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ try parseFile.delete(options: [.useMasterKey])
+ }
+
+ // swiftlint:disable:next inclusive_language
+ func testDeleteFileNoMasterKey() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ XCTAssertThrowsError(try parseFile.delete(options: [.removeMimeType]))
+ }
+
+ func testSaveAysnc() throws {
+
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ let parseFile = ParseFile(name: "sampleData.txt", data: sampleData)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.save { result in
+
+ switch result {
+ case .success(let saved):
+ XCTAssertEqual(saved.name, response.name)
+ XCTAssertEqual(saved.url, response.url)
+
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testSaveFileProgressAsync() throws {
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ let parseFile = ParseFile(name: "sampleData.txt", data: sampleData)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.save(progress: { (_, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ XCTAssertGreaterThan(currentProgess, -1)
+ }) { result in // swiftlint:disable:this multiple_closures_with_trailing_closure
+
+ switch result {
+ case .success(let saved):
+ XCTAssertEqual(saved.name, response.name)
+ XCTAssertEqual(saved.url, response.url)
+
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testSaveFileCancelAsync() throws {
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ let parseFile = ParseFile(name: "sampleData.txt", data: sampleData)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.save(progress: { (task, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ if currentProgess > 10 {
+ task.cancel()
+ }
+ }) { result in // swiftlint:disable:this multiple_closures_with_trailing_closure
+
+ switch result {
+ case .success(let saved):
+ XCTAssertEqual(saved.name, response.name)
+ XCTAssertEqual(saved.url, response.url)
+
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testSaveWithSpecifyingMimeAysnc() throws {
+
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ let parseFile = ParseFile(data: sampleData, mimeType: "application/txt")
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_file") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.save { result in
+
+ switch result {
+ case .success(let saved):
+ XCTAssertEqual(saved.name, response.name)
+ XCTAssertEqual(saved.url, response.url)
+
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testSaveLocalFileAysnc() throws {
+
+ let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.txt")
+ guard let sampleData = "Hello World".data(using: .utf8) else {
+ throw ParseError(code: .unknownError, message: "Should have converted to data")
+ }
+ try sampleData.write(to: tempFilePath)
+
+ let parseFile = ParseFile(name: "sampleData.txt", localURL: tempFilePath)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.save { result in
+
+ switch result {
+ case .success(let saved):
+ XCTAssertEqual(saved.name, response.name)
+ XCTAssertEqual(saved.url, response.url)
+ XCTAssertEqual(saved.localURL, tempFilePath)
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testSaveCloudFileAysnc() throws {
+
+ guard let tempFilePath = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: tempFilePath)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.save { result in
+
+ switch result {
+ case .success(let saved):
+ XCTAssertEqual(saved.name, response.name)
+ XCTAssertEqual(saved.url, response.url)
+ XCTAssertEqual(saved.cloudURL, tempFilePath)
+ XCTAssertNotNil(saved.localURL)
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testFetchFileAysnc() throws {
+
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/7793939a2e59b98138c1bbf2412a060c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "7793939a2e59b98138c1bbf2412a060c_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "7793939a2e59b98138c1bbf2412a060c_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.fetch { result in
+
+ switch result {
+ case .success(let fetched):
+ XCTAssertEqual(fetched.name, response.name)
+ XCTAssertEqual(fetched.url, response.url)
+ XCTAssertNotNil(fetched.localURL)
+
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testSaveCloudFileProgressAysnc() throws {
+
+ guard let tempFilePath = URL(string: "https://parseplatform.org/img/logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+
+ let parseFile = ParseFile(name: "logo.svg", cloudURL: tempFilePath)
+
+ // swiftlint:disable:next line_length
+ guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.save(progress: { (_, _, totalWritten, totalExpected) in
+ let currentProgess = Double(totalWritten)/Double(totalExpected) * 100
+ XCTAssertGreaterThan(currentProgess, -1)
+ }) { result in // swiftlint:disable:this multiple_closures_with_trailing_closure
+
+ switch result {
+ case .success(let saved):
+ XCTAssertEqual(saved.name, response.name)
+ XCTAssertEqual(saved.url, response.url)
+ XCTAssertEqual(saved.cloudURL, tempFilePath)
+ XCTAssertNotNil(saved.localURL)
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testFetchFileProgressAsync() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/6f9988ab5faa28f7247664c6ffd9fd85_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "6f9988ab5faa28f7247664c6ffd9fd85_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "6f9988ab5faa28f7247664c6ffd9fd85_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.fetch(progress: { (_, _, totalDownloaded, totalExpected) in
+ let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100
+ XCTAssertGreaterThan(currentProgess, -1)
+ }) { result in // swiftlint:disable:this multiple_closures_with_trailing_closure
+
+ switch result {
+ case .success(let fetched):
+ XCTAssertEqual(fetched.name, response.name)
+ XCTAssertEqual(fetched.url, response.url)
+ XCTAssertNotNil(fetched.localURL)
+
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testFetchFileCancelAsync() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/7793939a2e59b98138c1bbf2412a060c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "7793939a2e59b98138c1bbf2412a060c_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "7793939a2e59b98138c1bbf2412a060c_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.fetch(progress: { (task, _, totalDownloaded, totalExpected) in
+ let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100
+ if currentProgess > 10 {
+ task.cancel()
+ }
+ }) { result in // swiftlint:disable:this multiple_closures_with_trailing_closure
+
+ switch result {
+ case .success(let fetched):
+ XCTAssertEqual(fetched.name, response.name)
+ XCTAssertEqual(fetched.url, response.url)
+ XCTAssertNotNil(fetched.localURL)
+
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testDeleteFileAysnc() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/1b0683d529463e173cbf8046d7d9a613_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "1b0683d529463e173cbf8046d7d9a613_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "1b0683d529463e173cbf8046d7d9a613_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.delete(options: [.useMasterKey]) { error in
+
+ guard let error = error else {
+ expectation1.fulfill()
+ return
+ }
+ XCTFail(error.localizedDescription)
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ // swiftlint:disable:next inclusive_language
+ func testDeleteNoMasterKeyFileAysnc() throws {
+ // swiftlint:disable:next line_length
+ guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+ var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL)
+ parseFile.url = parseFileURL
+
+ let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg",
+ url: parseFileURL)
+ let encoded: Data!
+ do {
+ encoded = try ParseCoding.jsonEncoder().encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ let expectation1 = XCTestExpectation(description: "ParseFile async")
+ parseFile.delete(options: [.removeMimeType]) { error in
+
+ guard error != nil else {
+ XCTFail("Should have thrown error")
+ expectation1.fulfill()
+ return
+ }
+ expectation1.fulfill()
+ }
+ wait(for: [expectation1], timeout: 20.0)
+ }
+} // swiftlint:disable:this file_length
diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift
index 30dca716d..6db521970 100644
--- a/Tests/ParseSwiftTests/ParseInstallationTests.swift
+++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift
@@ -144,7 +144,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
XCTAssertEqual(installationIdFromContainer, installationIdFromCurrent)
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testInstallationMutableValuesCanBeChangedInMemory() {
@@ -160,7 +160,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
XCTAssertNotEqual(originalInstallation.customKey, Installation.current?.customKey)
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testInstallationCustomValuesNotSavedToKeychain() {
@@ -213,7 +213,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
XCTAssertEqual(originalLocaleIdentifier, Installation.current?.localeIdentifier)
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -264,7 +264,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
XCTAssertEqual(originalLocaleIdentifier, keychainInstallation.currentInstallation?.localeIdentifier)
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testInstallationHasApplicationBadge() {
@@ -289,7 +289,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
#endif
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testUpdate() {
@@ -318,19 +318,16 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
DispatchQueue.main.async {
do {
let saved = try installation.save()
- guard let savedCreatedAt = saved.createdAt,
- let savedUpdatedAt = saved.updatedAt else {
+ guard let savedUpdatedAt = saved.updatedAt else {
XCTFail("Should unwrap dates")
expectation1.fulfill()
return
}
- guard let originalCreatedAt = installation.createdAt,
- let originalUpdatedAt = installation.updatedAt else {
+ guard let originalUpdatedAt = installation.updatedAt else {
XCTFail("Should unwrap dates")
expectation1.fulfill()
return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
} catch {
@@ -338,7 +335,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
}
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testUpdateToCurrentInstallation() {
@@ -353,7 +350,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
XCTAssertEqual(savedObjectId, self.testInstallationObjectId)
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -365,55 +362,46 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
switch result {
case .success(let saved):
- guard let savedCreatedAt = saved.createdAt,
- let savedUpdatedAt = saved.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let savedUpdatedAt = saved.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- guard let originalCreatedAt = installation.createdAt,
- let originalUpdatedAt = installation.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let originalUpdatedAt = installation.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
if callbackQueue != .main {
DispatchQueue.main.async {
- guard let savedCreatedAt = Installation.current?.createdAt,
- let savedUpdatedAt = Installation.current?.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let savedUpdatedAt = Installation.current?.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- guard let originalCreatedAt = installation.createdAt,
- let originalUpdatedAt = installation.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let originalUpdatedAt = installation.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(Installation.current?.ACL)
expectation1.fulfill()
}
} else {
- guard let savedCreatedAt = Installation.current?.createdAt,
- let savedUpdatedAt = Installation.current?.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let savedUpdatedAt = Installation.current?.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- guard let originalCreatedAt = installation.createdAt,
- let originalUpdatedAt = installation.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let originalUpdatedAt = installation.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(Installation.current?.ACL)
expectation1.fulfill()
@@ -424,7 +412,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testUpdateAsyncMainQueue() {
@@ -536,7 +524,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testFetchUpdatedCurrentInstallationAsync() { // swiftlint:disable:this function_body_length
@@ -615,7 +603,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testDelete() {
@@ -642,7 +630,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testDeleteAsyncMainQueue() {
@@ -679,7 +667,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -698,7 +686,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300)
installation.customKey = "newValue"
- let installationOnServer = FindResult(results: [installation], count: 1)
+ let installationOnServer = QueryResponse(results: [installation], count: 1)
let encoded: Data!
do {
@@ -766,7 +754,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -784,7 +772,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300)
installation.customKey = "newValue"
- let installationOnServer = FindResult(results: [installation], count: 1)
+ let installationOnServer = QueryResponse(results: [installation], count: 1)
let encoded: Data!
do {
@@ -855,7 +843,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -942,7 +930,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -1031,7 +1019,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testDeleteAll() {
@@ -1047,7 +1035,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
return
}
- let installationOnServer = [BatchResponseItem(success: true, error: nil)]
+ let error: ParseError? = nil
+ let installationOnServer = [error]
let encoded: Data!
do {
@@ -1064,10 +1053,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
do {
let deleted = try [installation].deleteAll()
deleted.forEach {
- switch $0 {
- case .success:
- return
- case .failure(let error):
+ if let error = $0 {
XCTFail("Should have deleted: \(error.localizedDescription)")
}
}
@@ -1077,7 +1063,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testDeleteAllAsyncMainQueue() {
@@ -1092,7 +1078,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
return
}
- let installationOnServer = [BatchResponseItem(success: true, error: nil)]
+ let error: ParseError? = nil
+ let installationOnServer = [error]
let encoded: Data!
do {
@@ -1111,10 +1098,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
case .success(let deleted):
deleted.forEach {
- switch $0 {
- case .success:
- return
- case .failure(let error):
+ if let error = $0 {
XCTFail("Should have deleted: \(error.localizedDescription)")
}
}
@@ -1124,7 +1108,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
}
// swiftlint:disable:this file_length
diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift
index 78861fd44..cf8dd177d 100755
--- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift
+++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift
@@ -278,18 +278,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
case .success(let first):
- guard let savedCreatedAt = first.createdAt,
- let savedUpdatedAt = first.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let savedUpdatedAt = first.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- guard let originalCreatedAt = score.createdAt,
- let originalUpdatedAt = score.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let originalUpdatedAt = score.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
-
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(first.ACL)
@@ -301,18 +297,15 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
case .success(let second):
- guard let savedCreatedAt2 = second.createdAt,
- let savedUpdatedAt2 = second.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let savedUpdatedAt2 = second.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- guard let originalCreatedAt2 = score2.createdAt,
- let originalUpdatedAt2 = score2.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let originalUpdatedAt2 = score2.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- XCTAssertEqual(savedCreatedAt2, originalCreatedAt2)
XCTAssertGreaterThan(savedUpdatedAt2, originalUpdatedAt2)
XCTAssertNil(second.ACL)
@@ -331,18 +324,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
switch saved[0] {
case .success(let first):
- guard let savedCreatedAt = first.createdAt,
- let savedUpdatedAt = first.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let savedUpdatedAt = first.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- guard let originalCreatedAt = score.createdAt,
- let originalUpdatedAt = score.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let originalUpdatedAt = score.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
-
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(first.ACL)
case .failure(let error):
@@ -352,20 +341,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
switch saved[1] {
case .success(let second):
- guard let savedCreatedAt2 = second.createdAt,
- let savedUpdatedAt2 = second.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let savedUpdatedAt2 = second.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- guard let originalCreatedAt2 = score2.createdAt,
- let originalUpdatedAt2 = score2.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let originalUpdatedAt2 = score2.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- /*Date's are not exactly as their original because the URLMocking doesn't use the same dateEncoding
- strategy, so we only compare the day*/
- XCTAssertTrue(Calendar.current.isDate(savedCreatedAt2,
- equalTo: originalCreatedAt2, toGranularity: .day))
XCTAssertGreaterThan(savedUpdatedAt2, originalUpdatedAt2)
XCTAssertNil(second.ACL)
case .failure(let error):
@@ -679,7 +662,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeSaveAllAsync() {
@@ -789,13 +772,13 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
expectation1.fulfill()
return
}
- guard let originalUpdatedAt = scoresOnServer.first?.updatedAt else {
+ guard let originalUpdatedAt = scores.first?.updatedAt else {
XCTFail("Should unwrap dates")
expectation1.fulfill()
return
}
- XCTAssertEqual(savedUpdatedAt, originalUpdatedAt)
+ XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(first.ACL)
case .failure(let error):
XCTFail(error.localizedDescription)
@@ -805,17 +788,17 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
case .success(let second):
guard let savedUpdatedAt2 = second.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- guard let originalUpdatedAt2 = scoresOnServer.last?.updatedAt else {
+ guard let originalUpdatedAt2 = scores.last?.updatedAt else {
XCTFail("Should unwrap dates")
expectation1.fulfill()
return
}
- XCTAssertEqual(savedUpdatedAt2,
+ XCTAssertGreaterThan(savedUpdatedAt2,
originalUpdatedAt2)
XCTAssertNil(second.ACL)
case .failure(let error):
@@ -849,13 +832,13 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
expectation2.fulfill()
return
}
- guard let originalUpdatedAt = scoresOnServer.first?.updatedAt else {
+ guard let originalUpdatedAt = scores.first?.updatedAt else {
XCTFail("Should unwrap dates")
expectation2.fulfill()
return
}
- XCTAssertEqual(savedUpdatedAt, originalUpdatedAt)
+ XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(first.ACL)
case .failure(let error):
XCTFail(error.localizedDescription)
@@ -869,13 +852,13 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
expectation2.fulfill()
return
}
- guard let originalUpdatedAt2 = scoresOnServer.last?.updatedAt else {
+ guard let originalUpdatedAt2 = scores.last?.updatedAt else {
XCTFail("Should unwrap dates")
expectation2.fulfill()
return
}
- XCTAssertEqual(savedUpdatedAt2,
+ XCTAssertGreaterThan(savedUpdatedAt2,
originalUpdatedAt2)
XCTAssertNil(second.ACL)
case .failure(let error):
@@ -887,7 +870,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeUpdateAllAsync() {
@@ -994,7 +977,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
scoreOnServer2.updatedAt = scoreOnServer2.createdAt
scoreOnServer2.ACL = nil
- let response = FindResult(results: [scoreOnServer, scoreOnServer2], count: 2)
+ let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2)
let encoded: Data!
do {
encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response)
@@ -1150,7 +1133,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testThreadSafeFetchAllAsync() {
@@ -1169,7 +1152,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
scoreOnServer2.updatedAt = Date()
scoreOnServer2.ACL = nil
- let response = FindResult(results: [scoreOnServer, scoreOnServer2], count: 2)
+ let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2)
let encoded: Data!
do {
encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response)
@@ -1209,7 +1192,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
scoreOnServer2.updatedAt = scoreOnServer2.createdAt
scoreOnServer2.ACL = nil
- let response = FindResult(results: [scoreOnServer, scoreOnServer2], count: 2)
+ let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2)
let encoded: Data!
do {
encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(response)
@@ -1232,9 +1215,9 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
func testDeleteAll() {
let score = GameScore(score: 10)
+ let error: ParseError? = nil
+ let response = [error]
- let response = [BatchResponseItem(success: true, error: nil),
- BatchResponseItem(success: true, error: nil)]
let encoded: Data!
do {
encoded = try score.getEncoder(skipKeys: false).encode(response)
@@ -1249,27 +1232,49 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
do {
let fetched = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll()
- XCTAssertEqual(fetched.count, 2)
- guard let firstObject = fetched.first,
- let secondObject = fetched.last else {
+ XCTAssertEqual(fetched.count, 1)
+ guard let firstObject = fetched.first else {
XCTFail("Should unwrap")
return
}
- switch firstObject {
-
- case .success(let first):
- XCTAssertTrue(first)
- case .failure(let error):
+ if let error = firstObject {
XCTFail(error.localizedDescription)
}
- switch secondObject {
+ } catch {
+ XCTFail(error.localizedDescription)
+ }
+ }
- case .success(let second):
- XCTAssertTrue(second)
- case .failure(let error):
- XCTFail(error.localizedDescription)
+ func testDeleteAllError() {
+ let score = GameScore(score: 10)
+ let parseError = ParseError(code: .objectNotFound, message: "Object not found")
+ let response = [parseError]
+ let encoded: Data!
+ do {
+ encoded = try score.getEncoder(skipKeys: false).encode(response)
+ } catch {
+ XCTFail("Should have encoded/decoded. Error \(error)")
+ return
+ }
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ do {
+ let fetched = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll()
+
+ XCTAssertEqual(fetched.count, 1)
+ guard let firstObject = fetched.first else {
+ XCTFail("Should have thrown ParseError")
+ return
+ }
+
+ if let error = firstObject {
+ XCTAssertEqual(error.code, parseError.code)
+ } else {
+ XCTFail("Should have thrown ParseError")
}
} catch {
@@ -1287,28 +1292,65 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
switch result {
case .success(let fetched):
- XCTAssertEqual(fetched.count, 2)
- guard let firstObject = fetched.first,
- let secondObject = fetched.last else {
- XCTFail("Should unwrap")
- expectation1.fulfill()
- return
+ XCTAssertEqual(fetched.count, 1)
+ guard let firstObject = fetched.first else {
+ XCTFail("Should unwrap")
+ expectation1.fulfill()
+ return
}
- switch firstObject {
-
- case .success(let first):
- XCTAssertTrue(first)
- case .failure(let error):
+ if let error = firstObject {
XCTFail(error.localizedDescription)
}
- switch secondObject {
+ case .failure(let error):
+ XCTFail(error.localizedDescription)
+ }
+ expectation1.fulfill()
+ }
- case .success(let second):
- XCTAssertTrue(second)
- case .failure(let error):
- XCTFail(error.localizedDescription)
+ wait(for: [expectation1], timeout: 20.0)
+ }
+
+ func testDeleteAllAsyncMainQueue() {
+ let score = GameScore(score: 10)
+ let error: ParseError? = nil
+ let response = [error]
+
+ do {
+ let encoded = try score.getEncoder(skipKeys: false).encode(response)
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+ } catch {
+ XCTFail("Should have encoded/decoded. Error \(error)")
+ return
+ }
+
+ self.deleteAllAsync(callbackQueue: .main)
+ }
+
+ func deleteAllAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) {
+
+ let expectation1 = XCTestExpectation(description: "Delete object1")
+
+ [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll(options: [],
+ callbackQueue: callbackQueue) { result in
+
+ switch result {
+
+ case .success(let fetched):
+ XCTAssertEqual(fetched.count, 1)
+ guard let firstObject = fetched.first else {
+ XCTFail("Should have thrown ParseError")
+ expectation1.fulfill()
+ return
+ }
+
+ if let error = firstObject {
+ XCTAssertEqual(error.code, parseError.code)
+ } else {
+ XCTFail("Should have thrown ParseError")
}
case .failure(let error):
@@ -1317,14 +1359,14 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
- func testDeleteAllAsyncMainQueue() {
+ func testDeleteAllAsyncMainQueueError() {
let score = GameScore(score: 10)
- let response = [BatchResponseItem(success: true, error: nil),
- BatchResponseItem(success: true, error: nil)]
+ let parseError = ParseError(code: .objectNotFound, message: "Object not found")
+ let response = [parseError]
do {
let encoded = try score.getEncoder(skipKeys: false).encode(response)
@@ -1336,6 +1378,6 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le
return
}
- self.deleteAllAsync(callbackQueue: .main)
+ self.deleteAllAsyncError(parseError: parseError, callbackQueue: .main)
}
}// swiftlint:disable:this file_length
diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift
index 9c1125c1c..bc1afa50b 100644
--- a/Tests/ParseSwiftTests/ParseObjectTests.swift
+++ b/Tests/ParseSwiftTests/ParseObjectTests.swift
@@ -31,8 +31,8 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
var ACL: ParseACL?
//: Your own properties
- var score = 0
- var player = "Jen"
+ var score: Int?
+ var player: String?
var level: Level?
var levels: [Level]?
@@ -42,6 +42,11 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
}
init(score: Int) {
self.score = score
+ self.player = "Jen"
+ }
+ init(score: Int, name: String) {
+ self.score = score
+ self.player = name
}
}
@@ -56,6 +61,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
var score: GameScore
var scores = [GameScore]()
var name = "Hello"
+ var profilePicture: ParseFile?
//: a custom initializer
init(score: GameScore) {
@@ -63,6 +69,18 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
}
}
+ struct Game2: ParseObject {
+ //: Those are required for Object
+ var objectId: String?
+ var createdAt: Date?
+ var updatedAt: Date?
+ var ACL: ParseACL?
+
+ //: Your own properties
+ var name = "Hello"
+ var profilePicture: ParseFile?
+ }
+
class GameScoreClass: ParseObject {
//: Those are required for Object
@@ -180,11 +198,24 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
serverURL: url)
}
- override func tearDown() {
+ override func tearDownWithError() throws {
super.tearDown()
MockURLProtocol.removeAll()
- try? KeychainStore.shared.deleteAll()
- try? ParseStorage.shared.deleteAll()
+ try KeychainStore.shared.deleteAll()
+ try ParseStorage.shared.deleteAll()
+
+ guard let fileManager = ParseFileManager(),
+ let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else {
+ throw ParseError(code: .unknownError, message: "Should have initialized file manage")
+ }
+
+ let directory2 = defaultDirectoryPath
+ .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true)
+ let expectation2 = XCTestExpectation(description: "Delete files2")
+ fileManager.removeDirectoryContents(directory2) { _ in
+ expectation2.fulfill()
+ }
+ wait(for: [expectation2], timeout: 20.0)
}
func testFetchCommand() {
@@ -372,7 +403,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeFetchAsync() {
@@ -465,10 +496,6 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.createdAt = Date()
scoreOnServer.updatedAt = scoreOnServer.createdAt
- var newACL = ParseACL()
- newACL.setReadAccess(userId: "yarr", value: true)
- scoreOnServer.ACL = newACL
-
let encoded: Data!
do {
encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer)
@@ -552,11 +579,11 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
XCTFail("Should unwrap dates")
return
}
- guard let originalUpdatedAt = scoreOnServer.updatedAt else {
+ guard let originalUpdatedAt = score.updatedAt else {
XCTFail("Should unwrap dates")
return
}
- XCTAssertEqual(savedUpdatedAt, originalUpdatedAt)
+ XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
} catch {
XCTFail(error.localizedDescription)
@@ -568,11 +595,11 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
XCTFail("Should unwrap dates")
return
}
- guard let originalUpdatedAt = scoreOnServer.updatedAt else {
+ guard let originalUpdatedAt = score.updatedAt else {
XCTFail("Should unwrap dates")
return
}
- XCTAssertEqual(savedUpdatedAt, originalUpdatedAt)
+ XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
} catch {
XCTFail(error.localizedDescription)
@@ -638,7 +665,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeSaveAsync() {
@@ -649,10 +676,6 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.createdAt = Date()
scoreOnServer.updatedAt = scoreOnServer.createdAt
- var newACL = ParseACL()
- newACL.setReadAccess(userId: "yarr", value: true)
- scoreOnServer.ACL = newACL
-
let encoded: Data!
do {
encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(scoreOnServer)
@@ -709,12 +732,12 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
return
}
- guard let originalUpdatedAt = scoreOnServer.updatedAt else {
+ guard let originalUpdatedAt = score.updatedAt else {
XCTFail("Should unwrap dates")
expectation1.fulfill()
return
}
- XCTAssertEqual(savedUpdatedAt, originalUpdatedAt)
+ XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
case .failure(let error):
XCTFail(error.localizedDescription)
@@ -733,19 +756,19 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation2.fulfill()
return
}
- guard let originalUpdatedAt = scoreOnServer.updatedAt else {
+ guard let originalUpdatedAt = score.updatedAt else {
XCTFail("Should unwrap dates")
expectation2.fulfill()
return
}
- XCTAssertEqual(savedUpdatedAt, originalUpdatedAt)
+ XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
case .failure(let error):
XCTFail(error.localizedDescription)
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeUpdateAsync() {
@@ -851,6 +874,47 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
}
}
+ func testDeleteError() {
+ var score = GameScore(score: 10)
+ let objectId = "yarr"
+ score.objectId = objectId
+
+ let parseError = ParseError(code: .objectNotFound, message: "Object not found")
+
+ let encoded: Data!
+ do {
+ encoded = try score.getEncoder(skipKeys: false).encode(parseError)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+ do {
+ try score.delete(options: [])
+ XCTFail("Should have thrown ParseError")
+ } catch {
+ if let error = error as? ParseError {
+ XCTAssertEqual(error.code, parseError.code)
+ } else {
+ XCTFail("Should have thrown ParseError")
+ }
+ }
+
+ do {
+ try score.delete(options: [.useMasterKey])
+ XCTFail("Should have thrown ParseError")
+ } catch {
+ if let error = error as? ParseError {
+ XCTAssertEqual(error.code, parseError.code)
+ } else {
+ XCTFail("Should have thrown ParseError")
+ }
+ }
+ }
+
func deleteAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) {
let expectation1 = XCTestExpectation(description: "Delete object1")
@@ -874,7 +938,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
XCTFail(error.localizedDescription)
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeDeleteAsync() {
@@ -931,10 +995,58 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
self.deleteAsync(score: score, scoreOnServer: scoreOnServer, callbackQueue: .main)
}
+ func deleteAsyncError(score: GameScore, parseError: ParseError, callbackQueue: DispatchQueue) {
+
+ let expectation1 = XCTestExpectation(description: "Delete object1")
+ score.delete(options: [], callbackQueue: callbackQueue) { error in
+
+ guard let error = error else {
+ XCTFail("Should have thrown ParseError")
+ expectation1.fulfill()
+ return
+ }
+ XCTAssertEqual(error.code, parseError.code)
+ expectation1.fulfill()
+ }
+
+ let expectation2 = XCTestExpectation(description: "Delete object2")
+ score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { error in
+
+ guard let error = error else {
+ XCTFail("Should have thrown ParseError")
+ expectation1.fulfill()
+ return
+ }
+ XCTAssertEqual(error.code, parseError.code)
+ expectation2.fulfill()
+ }
+ wait(for: [expectation1, expectation2], timeout: 20.0)
+ }
+
+ func testDeleteAsyncMainQueueError() {
+ var score = GameScore(score: 10)
+ let objectId = "yarr"
+ score.objectId = objectId
+
+ let parseError = ParseError(code: .objectNotFound, message: "Object not found")
+ let encoded: Data!
+ do {
+ encoded = try score.getEncoder(skipKeys: false).encode(parseError)
+ } catch {
+ XCTFail("Should have encoded/decoded: Error: \(error)")
+ return
+ }
+
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+ self.deleteAsyncError(score: score, parseError: parseError, callbackQueue: .main)
+ }
+
+ // swiftlint:disable:next function_body_length
func testDeepSaveOneDeep() throws {
let score = GameScore(score: 10)
var game = Game(score: score)
- game.objectId = "nice"
var scoreOnServer = score
scoreOnServer.createdAt = Date()
@@ -955,18 +1067,68 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
}
- game.ensureDeepSave { results in
- switch results {
+ game.ensureDeepSave { (savedChildren, savedChildFiles, parseError) in
- case .success(let savedChildren):
- XCTAssertEqual(savedChildren.count, 1)
- savedChildren.forEach { (_, value) in
- XCTAssertEqual(value.className, "GameScore")
- XCTAssertEqual(value.objectId, "yarr")
+ XCTAssertEqual(savedChildren.count, 1)
+ XCTAssertEqual(savedChildFiles.count, 0)
+ var counter = 0
+ var savedChildObject: PointerType?
+ savedChildren.forEach { (_, value) in
+ XCTAssertEqual(value.className, "GameScore")
+ XCTAssertEqual(value.objectId, "yarr")
+ if counter == 0 {
+ savedChildObject = value
}
- case .failure(let error):
- XCTFail(error.localizedDescription)
+ counter += 1
+ }
+ XCTAssertNil(parseError)
+
+ //Saved updated info for game
+ let encodedScore: Data
+ do {
+ encodedScore = try game.getEncoder(skipKeys: false).encode(savedChildObject)
+ //Decode Pointer as GameScore
+ game.score = try game.getDecoder().decode(GameScore.self, from: encodedScore)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
}
+
+ //Setup ParseObject to return from mocker
+ MockURLProtocol.removeAll()
+
+ var gameOnServer = game
+ gameOnServer.objectId = "nice"
+ gameOnServer.createdAt = Date()
+ gameOnServer.updatedAt = Date()
+
+ let encodedGamed: Data
+ do {
+ encodedGamed = try game.getEncoder(skipKeys: false).encode(gameOnServer)
+ //Get dates in correct format from ParseDecoding strategy
+ gameOnServer = try game.getDecoder().decode(Game.self, from: encodedGamed)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encodedGamed, statusCode: 200, delay: 0.0)
+ }
+
+ guard let savedGame = try? game
+ .saveCommand()
+ .execute(options: [],
+ childObjects: savedChildren,
+ childFiles: savedChildFiles) else {
+ XCTFail("Should have saved game")
+ return
+ }
+ XCTAssertEqual(savedGame.objectId, gameOnServer.objectId)
+ XCTAssertEqual(savedGame.createdAt, gameOnServer.createdAt)
+ XCTAssertEqual(savedGame.updatedAt, gameOnServer.updatedAt)
+ XCTAssertEqual(savedGame.score, gameOnServer.score)
+
}
}
@@ -976,14 +1138,13 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
game.objectId = "nice"
score.game = game
- game.ensureDeepSave { results in
- switch results {
+ game.ensureDeepSave { (_, _, parseError) in
- case .success:
+ guard let error = parseError else {
XCTFail("Should have failed with an error of detecting a circular dependency")
- case .failure(let error):
- XCTAssertTrue(error.message.contains("circular"))
+ return
}
+ XCTAssertTrue(error.message.contains("circular"))
}
}
@@ -1011,35 +1172,32 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
}
- game.ensureDeepSave { results in
- switch results {
+ game.ensureDeepSave { (savedChildren, savedChildFiles, parseError) in
- case .success(let savedChildren):
- XCTAssertEqual(savedChildren.count, 2)
- let gameScore = savedChildren.compactMap { (_, value) -> PointerType? in
- if value.className == "GameScore" {
- return value
- } else {
- return nil
- }
+ XCTAssertEqual(savedChildFiles.count, 0)
+ XCTAssertEqual(savedChildren.count, 2)
+ let gameScore = savedChildren.compactMap { (_, value) -> PointerType? in
+ if value.className == "GameScore" {
+ return value
+ } else {
+ return nil
}
- XCTAssertEqual(gameScore.count, 1)
- XCTAssertEqual(gameScore.first?.className, "GameScore")
- XCTAssertEqual(gameScore.first?.objectId, "yarr")
-
- let level = savedChildren.compactMap { (_, value) -> PointerType? in
- if value.className == "Level" {
- return value
- } else {
- return nil
- }
+ }
+ XCTAssertEqual(gameScore.count, 1)
+ XCTAssertEqual(gameScore.first?.className, "GameScore")
+ XCTAssertEqual(gameScore.first?.objectId, "yarr")
+
+ let level = savedChildren.compactMap { (_, value) -> PointerType? in
+ if value.className == "Level" {
+ return value
+ } else {
+ return nil
}
- XCTAssertEqual(level.count, 1)
- XCTAssertEqual(level.first?.className, "Level")
- XCTAssertEqual(level.first?.objectId, "yarr") //This is because mocker is only returning 1 response
- case .failure(let error):
- XCTFail(error.localizedDescription)
}
+ XCTAssertEqual(level.count, 1)
+ XCTAssertEqual(level.first?.className, "Level")
+ XCTAssertEqual(level.first?.objectId, "yarr") //This is because mocker is only returning 1 response
+ XCTAssertNil(parseError)
}
}
@@ -1096,6 +1254,92 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
return
}
}
+
+ // swiftlint:disable:next function_body_length
+ func testDeepSaveObjectWithFile() throws {
+ ParseSwift.setupForTesting()
+ var game = Game2()
+
+ guard let cloudPath = URL(string: "https://parseplatform.org/img/logo.svg"),
+ // swiftlint:disable:next line_length
+ let parseURL = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_logo.svg") else {
+ XCTFail("Should create URL")
+ return
+ }
+
+ let parseFile = ParseFile(name: "profile.svg", cloudURL: cloudPath)
+ game.profilePicture = parseFile
+
+ let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: parseURL)
+
+ let encoded: Data!
+ do {
+ encoded = try game.getEncoder(skipKeys: false).encode(response)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
+ }
+
+ game.ensureDeepSave { (savedChildren, savedChildFiles, parseError) in
+
+ XCTAssertEqual(savedChildren.count, 0)
+ XCTAssertEqual(savedChildFiles.count, 1)
+ var counter = 0
+ var savedFile: ParseFile?
+ savedChildFiles.forEach { (_, value) in
+ XCTAssertEqual(value.url, response.url)
+ XCTAssertEqual(value.name, response.name)
+ if counter == 0 {
+ savedFile = value
+ }
+ counter += 1
+ }
+ XCTAssertNil(parseError)
+
+ //Saved updated info for game
+ game.profilePicture = savedFile
+
+ //Setup ParseObject to return from mocker
+ MockURLProtocol.removeAll()
+
+ var gameOnServer = game
+ gameOnServer.objectId = "nice"
+ gameOnServer.createdAt = Date()
+ gameOnServer.updatedAt = Date()
+ gameOnServer.profilePicture = savedFile
+
+ let encodedGamed: Data
+ do {
+ encodedGamed = try game.getEncoder(skipKeys: false).encode(gameOnServer)
+ //Get dates in correct format from ParseDecoding strategy
+ gameOnServer = try game.getDecoder().decode(Game2.self, from: encodedGamed)
+ } catch {
+ XCTFail("Should encode/decode. Error \(error)")
+ return
+ }
+
+ MockURLProtocol.mockRequests { _ in
+ return MockURLResponse(data: encodedGamed, statusCode: 200, delay: 0.0)
+ }
+
+ guard let savedGame = try? game
+ .saveCommand()
+ .execute(options: [],
+ childObjects: savedChildren,
+ childFiles: savedChildFiles) else {
+ XCTFail("Should have saved game")
+ return
+ }
+ XCTAssertEqual(savedGame.objectId, gameOnServer.objectId)
+ XCTAssertEqual(savedGame.createdAt, gameOnServer.createdAt)
+ XCTAssertEqual(savedGame.updatedAt, gameOnServer.updatedAt)
+ XCTAssertEqual(savedGame.profilePicture, gameOnServer.profilePicture)
+ }
+ }
}
// swiftlint:disable:this file_length
diff --git a/Tests/ParseSwiftTests/ParsePointerTests.swift b/Tests/ParseSwiftTests/ParsePointerTests.swift
index 453beffff..f9f8afbe2 100644
--- a/Tests/ParseSwiftTests/ParsePointerTests.swift
+++ b/Tests/ParseSwiftTests/ParsePointerTests.swift
@@ -179,7 +179,7 @@ class ParsePointerTests: XCTestCase {
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeFetchAsync() {
diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift
index d8af0ff88..6031248ed 100755
--- a/Tests/ParseSwiftTests/ParseQueryTests.swift
+++ b/Tests/ParseSwiftTests/ParseQueryTests.swift
@@ -177,7 +177,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -259,7 +259,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testThreadSafeFindAsync() {
@@ -269,7 +269,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -291,7 +291,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -310,7 +310,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -336,7 +336,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
func testFirstNoObjectFound() {
let scoreOnServer = GameScore(score: 10)
- let results = FindResult(results: [GameScore](), count: 0)
+ let results = QueryResponse(results: [GameScore](), count: 0)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -378,7 +378,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func firstAsync(scoreOnServer: GameScore, callbackQueue: DispatchQueue) {
@@ -396,7 +396,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testThreadSafeFirstAsync() {
@@ -406,7 +406,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -428,7 +428,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -442,7 +442,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
func testThreadSafeFirstAsyncNoObjectFound() {
let scoreOnServer = GameScore(score: 10)
- let results = FindResult(results: [GameScore](), count: 0)
+ let results = QueryResponse(results: [GameScore](), count: 0)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -459,7 +459,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
func testFirstAsyncNoObjectFoundMainQueue() {
let scoreOnServer = GameScore(score: 10)
- let results = FindResult(results: [GameScore](), count: 0)
+ let results = QueryResponse(results: [GameScore](), count: 0)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -478,7 +478,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -513,7 +513,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testThreadSafeCountAsync() {
@@ -523,7 +523,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -545,7 +545,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
scoreOnServer.updatedAt = Date()
scoreOnServer.ACL = nil
- let results = FindResult(results: [scoreOnServer], count: 1)
+ let results = QueryResponse(results: [scoreOnServer], count: 1)
MockURLProtocol.mockRequests { _ in
do {
let encoded = try scoreOnServer.getEncoder(skipKeys: false).encode(results)
@@ -1678,7 +1678,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testExplainFirstSynchronous() {
@@ -1743,7 +1743,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testExplainCountSynchronous() {
@@ -1808,7 +1808,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testHintFindSynchronous() {
@@ -1873,7 +1873,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testHintFirstSynchronous() {
@@ -1938,7 +1938,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
func testHintCountSynchronous() {
@@ -2003,7 +2003,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation.fulfill()
}
- wait(for: [expectation], timeout: 10.0)
+ wait(for: [expectation], timeout: 20.0)
}
}
// swiftlint:disable:this file_length
diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift
index 8082521ec..fae9a92cc 100644
--- a/Tests/ParseSwiftTests/ParseUserTests.swift
+++ b/Tests/ParseSwiftTests/ParseUserTests.swift
@@ -286,7 +286,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -347,7 +347,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeFetchAsync() {
@@ -529,7 +529,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testUpdate() { // swiftlint:disable:this function_body_length
@@ -557,17 +557,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
do {
let saved = try user.save()
- guard let savedCreatedAt = saved.createdAt,
- let savedUpdatedAt = saved.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let savedUpdatedAt = saved.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- guard let originalCreatedAt = user.createdAt,
- let originalUpdatedAt = user.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let originalUpdatedAt = user.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
} catch {
@@ -576,17 +573,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
do {
let saved = try user.save(options: [.useMasterKey])
- guard let savedCreatedAt = saved.createdAt,
- let savedUpdatedAt = saved.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let savedUpdatedAt = saved.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- guard let originalCreatedAt = user.createdAt,
- let originalUpdatedAt = user.updatedAt else {
- XCTFail("Should unwrap dates")
- return
+ guard let originalUpdatedAt = user.updatedAt else {
+ XCTFail("Should unwrap dates")
+ return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
} catch {
@@ -594,7 +588,6 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
}
- // swiftlint:disable:next function_body_length
func updateAsync(user: User, userOnServer: User, callbackQueue: DispatchQueue) {
let expectation1 = XCTestExpectation(description: "Update user1")
@@ -603,19 +596,16 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
switch result {
case .success(let saved):
- guard let savedCreatedAt = saved.createdAt,
- let savedUpdatedAt = saved.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let savedUpdatedAt = saved.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- guard let originalCreatedAt = user.createdAt,
- let originalUpdatedAt = user.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation1.fulfill()
- return
+ guard let originalUpdatedAt = user.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation1.fulfill()
+ return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
case .failure(let error):
@@ -630,19 +620,17 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
switch result {
case .success(let saved):
- guard let savedCreatedAt = saved.createdAt,
- let savedUpdatedAt = saved.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation2.fulfill()
- return
+ guard let savedUpdatedAt = saved.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation2.fulfill()
+ return
}
- guard let originalCreatedAt = user.createdAt,
- let originalUpdatedAt = user.updatedAt else {
- XCTFail("Should unwrap dates")
- expectation2.fulfill()
- return
+ guard let originalUpdatedAt = user.updatedAt else {
+ XCTFail("Should unwrap dates")
+ expectation2.fulfill()
+ return
}
- XCTAssertEqual(savedCreatedAt, originalCreatedAt)
+
XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt)
XCTAssertNil(saved.ACL)
case .failure(let error):
@@ -650,7 +638,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation2.fulfill()
}
- wait(for: [expectation1, expectation2], timeout: 10.0)
+ wait(for: [expectation1, expectation2], timeout: 20.0)
}
func testThreadSafeUpdateAsync() {
@@ -786,7 +774,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testSignUpAsyncMainQueue() {
@@ -884,7 +872,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testLoginAsyncMainQueue() {
@@ -944,7 +932,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testLogoutAsyncMainQueue() {
@@ -998,7 +986,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testDeleteAsyncMainQueue() {
@@ -1035,7 +1023,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -1054,7 +1042,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
user.updatedAt = user.updatedAt?.addingTimeInterval(+300)
user.customKey = "newValue"
- let userOnServer = FindResult(results: [user], count: 1)
+ let userOnServer = QueryResponse(results: [user], count: 1)
let encoded: Data!
do {
@@ -1122,7 +1110,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -1140,7 +1128,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
user.updatedAt = user.updatedAt?.addingTimeInterval(+300)
user.customKey = "newValue"
- let userOnServer = FindResult(results: [user], count: 1)
+ let userOnServer = QueryResponse(results: [user], count: 1)
let encoded: Data!
do {
@@ -1210,7 +1198,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -1297,7 +1285,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
// swiftlint:disable:next function_body_length
@@ -1385,7 +1373,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testDeleteAll() {
@@ -1401,7 +1389,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
return
}
- let userOnServer = [BatchResponseItem(success: true, error: nil)]
+ let error: ParseError? = nil
+ let userOnServer = [error]
let encoded: Data!
do {
@@ -1418,10 +1407,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
do {
let deleted = try [user].deleteAll()
deleted.forEach {
- switch $0 {
- case .success:
- return
- case .failure(let error):
+ if let error = $0 {
XCTFail("Should have deleted: \(error.localizedDescription)")
}
}
@@ -1431,7 +1417,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
func testDeleteAllAsyncMainQueue() {
@@ -1446,7 +1432,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
return
}
- let userOnServer = [BatchResponseItem(success: true, error: nil)]
+ let error: ParseError? = nil
+ let userOnServer = [error]
let encoded: Data!
do {
@@ -1465,10 +1452,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
case .success(let deleted):
deleted.forEach {
- switch $0 {
- case .success:
- return
- case .failure(let error):
+ if let error = $0 {
XCTFail("Should have deleted: \(error.localizedDescription)")
}
}
@@ -1478,7 +1462,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
expectation1.fulfill()
}
}
- wait(for: [expectation1], timeout: 10.0)
+ wait(for: [expectation1], timeout: 20.0)
}
}
// swiftlint:disable:this file_length