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