diff --git a/CHANGELOG.md b/CHANGELOG.md index 726ce7392..eaa66d26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,22 @@ ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.2...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 4.0.0 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.2...4.0.0) + +__New features__ +- (Breaking Change) Add the ability to merge updated ParseObject's with original objects when using the + .mergeable property. To do this, developers need to add an implementation of merge() to + respective ParseObject's. The compiler will recommend the new originalData property be added to + every ParseObject. If you used ParseObjectMutable in the past, you should remove it as it is now + part of ParseObject. In addition, all ParseObject properties should be optional and every object + needs to have a default initilizer of init(). See the Playgrounds for recommendations on how to + define a ParseObject. Look at the PR for + details on why this is important when using the SDK ([#315](https://github.com/parse-community/Parse-Swift/pull/315)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 3.1.2 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.1...3.1.2) @@ -31,7 +44,7 @@ __New features__ - Adds equalTo QueryConstraint along with ability to change the SDK default behavior of using $eq QueryConstraint parameter or not ([#310](https://github.com/parse-community/Parse-Swift/pull/310)), thanks to [Corey Baker](https://github.com/cbaker6). - Adds isNull and isNotNull QueryConstraint along with the ability set/forceSet null using ParseOperation ([#308](https://github.com/parse-community/Parse-Swift/pull/308)), thanks to [Corey Baker](https://github.com/cbaker6). - Adds auth support for GitHub, Google, and LinkedIn ([#307](https://github.com/parse-community/Parse-Swift/pull/307)), thanks to [Corey Baker](https://github.com/cbaker6). -- (Breaking Change) Adds options to matchesText QueryConstraint along with the ability to see matching score. The compiler should recommend the new score property to all ParseObjects ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6). +- (Breaking Change) Adds options to matchesText QueryConstraint along with the ability to see matching score. The compiler will recommend the new score property be added to all ParseObjects ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6). - Adds withCount query ([#306](https://github.com/parse-community/Parse-Swift/pull/306)), thanks to [Corey Baker](https://github.com/cbaker6). __Improvements__ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7026d70f1..f7ed4d130 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -119,7 +119,7 @@ Currently, we are not making use of the commit _scope_, which would be written a ## Evolution -It's not intended as a port of the Parse Objective-c SDK and has many new philosophies. Please see [this thread](https://github.com/parse-community/Parse-Swift/issues/3) for a detailed discussion about the intended evolution of this SDK. +The ParseSwift SDK is not a port of the [Parse-SDK-iOS-OSX SDK](https://github.com/parse-community/Parse-SDK-iOS-OSX) and though some of it may feel familiar, it is not backwards compatible and is designed using [protocol oriented programming (POP) and value types](https://www.pluralsight.com/guides/protocol-oriented-programming-in-swift) instead of OOP and reference types. You can learn more about POP by watching [this](https://developer.apple.com/videos/play/wwdc2015/408/) or [that](https://developer.apple.com/videos/play/wwdc2016/419/) videos from previous WWDC's. Please see [this thread](https://github.com/parse-community/Parse-Swift/issues/3) for a detailed discussion about the intended evolution of this SDK. ## Code of Conduct 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 962f3179c..f4c98cb13 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -30,15 +30,26 @@ do { } //: Create your own value typed `ParseObject`. -struct GameScore: ParseObject, ParseObjectMutable { +struct GameScore: ParseObject { //: These are required by ParseObject var objectId: String? var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -60,12 +71,27 @@ struct GameData: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. var polygon: ParsePolygon? //: `ParseBytes` needs to be a part of the original schema //: or else you will need your masterKey to force an upgrade. var bytes: ParseBytes? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if shouldRestoreKey(\.polygon, + original: object) { + updated.polygon = object.polygon + } + if shouldRestoreKey(\.bytes, + original: object) { + updated.bytes = object.bytes + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -99,19 +125,14 @@ score.save { result in allows you to only send the updated keys to the parse server as opposed to the whole object. */ - var changedScore = savedScore.mutable + var changedScore = savedScore.mergeable changedScore.points = 200 changedScore.save { result in switch result { - case .success(var savedChangedScore): + case .success(let savedChangedScore): assert(savedChangedScore.points == 200) assert(savedScore.objectId == savedChangedScore.objectId) - /*: Note that savedChangedScore is mutable since it's - a var after success. - */ - savedChangedScore.points = 500 - case .failure(let error): assertionFailure("Error saving: \(error)") } @@ -132,7 +153,10 @@ var score2ForFetchedLater: GameScore? otherResults.forEach { otherResult in switch otherResult { case .success(let savedScore): - print("Saved \"\(savedScore.className)\" with points \(savedScore.points) successfully") + print(""" + Saved \"\(savedScore.className)\" with + points \(String(describing: savedScore.points)) successfully + """) if index == 1 { score2ForFetchedLater = savedScore } @@ -191,14 +215,15 @@ assert(savedScore?.points == 10) allows you to only send the updated keys to the parse server as opposed to the whole object. */ -guard var changedScore = savedScore?.mutable else { - fatalError() +guard var changedScore = savedScore?.mergeable else { + fatalError("Should have produced mutable changedScore") } changedScore.points = 200 let savedChangedScore: GameScore? do { savedChangedScore = try changedScore.save() + print("Updated score: \(String(describing: savedChangedScore))") } catch { savedChangedScore = nil fatalError("Error saving: \(error)") @@ -220,7 +245,7 @@ assert(otherResults != nil) otherResults!.forEach { result in switch result { case .success(let savedScore): - print("Saved \"\(savedScore.className)\" with points \(savedScore.points) successfully") + print("Saved \"\(savedScore.className)\" with points \(String(describing: savedScore.points)) successfully") case .failure(let error): assertionFailure("Error saving: \(error)") } diff --git a/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift index 3b2a7d16b..7319682ba 100644 --- a/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/10 - Cloud Code.xcplaygroundpage/Contents.swift @@ -106,9 +106,20 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated + } } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift index c45d6baab..e49ae8cae 100644 --- a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift @@ -16,11 +16,30 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? var location: ParseGeoPoint? var name: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.location, + original: object) { + updated.location = object.location + } + if updated.shouldRestoreKey(\.name, + original: object) { + updated.name = object.name + } + return updated + } } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift index a4b5a9685..5379d8401 100644 --- a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift @@ -19,6 +19,7 @@ struct User: ParseUser { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: These are required by `ParseUser`. var username: String? @@ -29,6 +30,16 @@ struct User: ParseUser { //: Your custom keys. var customKey: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + return updated + } } struct Role: ParseRole { @@ -38,25 +49,43 @@ struct Role: ParseRole { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Provided by Role. - var name: String - - init() { - self.name = "" + var name: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.name, + original: object) { + updated.name = object.name + } + return updated } } //: Create your own value typed `ParseObject`. -struct GameScore: ParseObject, ParseObjectMutable { +struct GameScore: ParseObject { //: These are required by ParseObject var objectId: String? var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -136,7 +165,10 @@ do { try savedRole!.users.query(templateUser).find { result in switch result { case .success(let relatedUsers): - print("The following users are part of the \"\(savedRole!.name) role: \(relatedUsers)") + print(""" + The following users are part of the + \"\(String(describing: savedRole!.name)) role: \(relatedUsers) + """) case .failure(let error): print("Error saving role: \(error)") @@ -220,7 +252,10 @@ do { savedRole!.queryRoles?.find { result in switch result { case .success(let relatedRoles): - print("The following roles are part of the \"\(savedRole!.name) role: \(relatedRoles)") + print(""" + The following roles are part of the + \"\(String(describing: savedRole!.name)) role: \(relatedRoles) + """) case .failure(let error): print("Error saving role: \(error)") @@ -263,7 +298,7 @@ let score2 = GameScore(points: 57) switch result { case .success(let saved): print("The relation saved successfully: \(saved)") - print("Check \"pointss\" field in your \"_User\" class in Parse Dashboard.") + print("Check \"points\" field in your \"_User\" class in Parse Dashboard.") case .failure(let error): print("Error saving role: \(error)") diff --git a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift index 644c66e7a..92a964ea6 100644 --- a/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/13 - Operations.xcplaygroundpage/Contents.swift @@ -19,10 +19,25 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int? = 0 + var points: Int? var name: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.name, + original: object) { + updated.name = object.name + } + return updated + } } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift index ac1aad317..fd697cb92 100644 --- a/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/15 - Custom ObjectId.xcplaygroundpage/Contents.swift @@ -21,15 +21,26 @@ npm start -- --appId applicationId --clientKey clientKey --masterKey masterKey - initializeParseCustomObjectId() //: Create your own value typed `ParseObject`. -struct GameScore: ParseObject, ParseObjectMutable { +struct GameScore: ParseObject { //: These are required by ParseObject var objectId: String? var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -67,11 +78,11 @@ score.save { result in print("Saved score: \(savedScore)") /*: To modify, need to make it a var as the value type - was initialized as immutable. Using `mutable` + was initialized as immutable. Using `.mergeable` allows you to only send the updated keys to the parse server as opposed to the whole object. */ - var changedScore = savedScore.mutable + var changedScore = savedScore.mergeable changedScore.points = 200 changedScore.save { result in switch result { diff --git a/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift index e5ea5aa40..06654652f 100644 --- a/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift @@ -25,12 +25,35 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? var location: ParseGeoPoint? var name: String? var myFiles: [ParseFile]? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.name, + original: object) { + updated.name = object.name + } + if updated.shouldRestoreKey(\.myFiles, + original: object) { + updated.myFiles = object.myFiles + } + if updated.shouldRestoreKey(\.location, + original: object) { + updated.location = object.location + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -95,7 +118,7 @@ struct ContentView: View { //: Warning - List seems to only work in Playgrounds Xcode 13+. List(viewModel.results, id: \.id) { result in VStack(alignment: .leading) { - Text("Points: \(result.points)") + Text("Points: \(String(describing: result.points))") .font(.headline) if let createdAt = result.createdAt { Text("\(createdAt.description)") diff --git a/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift index 59603376e..059e9d968 100644 --- a/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift @@ -26,11 +26,30 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? var location: ParseGeoPoint? var name: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.name, + original: object) { + updated.name = object.name + } + if updated.shouldRestoreKey(\.location, + original: object) { + updated.location = object.location + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -93,7 +112,7 @@ struct ContentView: View { //: Warning - List seems to only work in Playgrounds Xcode 13+. List(viewModel.objects, id: \.id) { object in VStack(alignment: .leading) { - Text("Points: \(object.points)") + Text("Points: \(String(describing: object.points))") .font(.headline) if let createdAt = object.createdAt { Text("\(createdAt.description)") diff --git a/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift index 43b040181..c79743a10 100644 --- a/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift @@ -24,11 +24,30 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? = 0 var location: ParseGeoPoint? var name: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.name, + original: object) { + updated.name = object.name + } + if updated.shouldRestoreKey(\.location, + original: object) { + updated.location = object.location + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -65,15 +84,15 @@ struct ContentView: View { switch event.event { case .entered(let object): - Text("Entered with points: \(object.points)") + Text("Entered with points: \(String(describing: object.points))") case .left(let object): - Text("Left with points: \(object.points)") + Text("Left with points: \(String(describing: object.points))") case .created(let object): - Text("Created with points: \(object.points)") + Text("Created with points: \(String(describing: object.points))") case .updated(let object): - Text("Updated with points: \(object.points)") + Text("Updated with points: \(String(describing: object.points))") case .deleted(let object): - Text("Deleted with points: \(object.points)") + Text("Deleted with points: \(String(describing: object.points))") } } else { Text("Not subscribed to a query") diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift index 1dc33f127..fb5df2e62 100644 --- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift @@ -18,12 +18,35 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. var points: Int? var timeStamp: Date? = Date() var oldScore: Int? var isHighest: Bool? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.timeStamp, + original: object) { + updated.timeStamp = object.timeStamp + } + if updated.shouldRestoreKey(\.oldScore, + original: object) { + updated.oldScore = object.oldScore + } + if updated.shouldRestoreKey(\.isHighest, + original: object) { + updated.isHighest = object.isHighest + } + return updated + } } var score = GameScore() @@ -112,7 +135,7 @@ query.withCount { results in } //: Query based on relative time. -let queryRelative = GameScore.query(relative("createdAt" < "10 minutes ago")) +let queryRelative = GameScore.query(relative("createdAt" < "in 10 minutes")) queryRelative.find { results in switch results { case .success(let scores): diff --git a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift index c912a6568..d95110d40 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -19,7 +19,7 @@ struct User: ParseUser { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? - var score: Double? + var originalData: Data? //: These are required by `ParseUser`. var username: String? @@ -30,6 +30,16 @@ struct User: ParseUser { //: Your custom keys. var customKey: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + return updated + } } /*: Sign up user asynchronously - Performs work on background diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 6b3874917..9ce4c2bf5 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -13,12 +13,13 @@ import ParseSwift PlaygroundPage.current.needsIndefiniteExecution = true initializeParse() -struct User: ParseUser, ParseObjectMutable { +struct User: ParseUser { //: These are required by `ParseObject`. var objectId: String? var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: These are required by `ParseUser`. var username: String? @@ -32,6 +33,28 @@ struct User: ParseUser, ParseObjectMutable { var gameScore: GameScore? var targetScore: GameScore? var allScores: [GameScore]? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + if updated.shouldRestoreKey(\.gameScore, + original: object) { + updated.gameScore = object.gameScore + } + if updated.shouldRestoreKey(\.targetScore, + original: object) { + updated.targetScore = object.targetScore + } + if updated.shouldRestoreKey(\.allScores, + original: object) { + updated.allScores = object.allScores + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -52,9 +75,20 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. var points: Int? = 0 + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -103,10 +137,10 @@ User.login(username: "hello", password: "world") { result in Asynchrounously - Performs work on background queue and returns to specified callbackQueue. If no callbackQueue is specified it returns to main queue. - Using `mutable` allows you to only send the updated keys to the + Using `.mergeable` allows you to only send the updated keys to the parse server as opposed to the whole object. */ -var currentUser = User.current?.mutable +var currentUser = User.current?.mergeable currentUser?.customKey = "myCustom" currentUser?.gameScore = GameScore(points: 12) currentUser?.targetScore = GameScore(points: 100) @@ -210,7 +244,7 @@ User.anonymous.login { result in } //: Convert the anonymous user to a real new user. -var currentUser2 = User.current +var currentUser2 = User.current?.mergeable currentUser2?.username = "bye" currentUser2?.password = "world" currentUser2?.signup { result in diff --git a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift index a5ede49b6..d04f21d50 100644 --- a/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/5 - ACL.xcplaygroundpage/Contents.swift @@ -29,12 +29,19 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties - var points: Int + var points: Int? - init() { - self.points = 0 + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated } } diff --git a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift index 07f360fa5..31fa843ee 100644 --- a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift @@ -19,6 +19,7 @@ struct Installation: ParseInstallation { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: These are required by `ParseInstallation`. var installationId: String? @@ -35,6 +36,16 @@ struct Installation: ParseInstallation { //: Your custom keys var customKey: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + return updated + } } /*: Save your first `customKey` value to your `ParseInstallation`. @@ -59,7 +70,7 @@ currentInstallation?.save { results in designated callbackQueue. If no callbackQueue is specified it returns to main queue. */ -var installationToUpdate = Installation.current +var installationToUpdate = Installation.current?.mergeable installationToUpdate?.customKey = "myCustomInstallationKey2" installationToUpdate?.save { results in diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index a20a39371..7d8b7e2f3 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -21,9 +21,20 @@ struct GameScore: ParseObject { var updatedAt: Date? var ACL: ParseACL? var location: ParseGeoPoint? + var originalData: Data? //: Your own properties var points: Int? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated + } } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift index ed12cc5db..5ef18160c 100644 --- a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift @@ -21,10 +21,25 @@ struct Book: ParseObject, ParseQueryScorable { var updatedAt: Date? var ACL: ParseACL? var score: Double? + var originalData: Data? //: Your own properties. var title: String? var relatedBook: Pointer? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.title, + original: object) { + updated.title = object.title + } + if updated.shouldRestoreKey(\.relatedBook, + original: object) { + updated.relatedBook = object.relatedBook + } + return updated + } } //: It's recommended to place custom initializers in an extension @@ -42,15 +57,29 @@ struct Author: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var name: String - var book: Book + var name: String? + var book: Book? var otherBooks: [Book]? - init() { - self.name = "hello" - self.book = Book() + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.name, + original: object) { + updated.name = object.name + } + if updated.shouldRestoreKey(\.book, + original: object) { + updated.book = object.book + } + if updated.shouldRestoreKey(\.otherBooks, + original: object) { + updated.otherBooks = object.otherBooks + } + return updated } } @@ -131,7 +160,9 @@ query2.first { results in switch results { case .success(let author): //: Save the book to use later - newBook = author.book + if let book = author.book { + newBook = book + } print("Found author and included \"book\": \(author)") @@ -187,15 +218,15 @@ do { case .success(let author): print("Found author and included \"book\": \(author)") //: Setup related books. - newBook.relatedBook = try? author.otherBooks?.first?.toPointer() + var modifiedNewBook = newBook.mergeable + modifiedNewBook.relatedBook = try? author.otherBooks?.first?.toPointer() - newBook.save { result in + modifiedNewBook.save { result in switch result { case .success(let updatedBook): assert(updatedBook.objectId != nil) assert(updatedBook.createdAt != nil) assert(updatedBook.updatedAt != nil) - assert(updatedBook.ACL == nil) assert(updatedBook.relatedBook != nil) print("Saved \(updatedBook)") diff --git a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift index 2d6a518f0..f3547875f 100644 --- a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift @@ -20,11 +20,30 @@ struct GameScore: ParseObject { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties. - var points: Int = 0 + var points: Int? = 0 var profilePicture: ParseFile? var myData: ParseFile? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.profilePicture, + original: object) { + updated.profilePicture = object.profilePicture + } + if updated.shouldRestoreKey(\.myData, + original: object) { + updated.myData = object.myData + } + return updated + } } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index c4eaf152e..3b1b96e98 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -679,10 +679,6 @@ 91F346C3269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; 91F346C4269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; 91F346C5269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; - CD106A00272D481800939151 /* ParseObjectMutable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1069FF272D481800939151 /* ParseObjectMutable.swift */; }; - CD106A01272D481800939151 /* ParseObjectMutable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1069FF272D481800939151 /* ParseObjectMutable.swift */; }; - CD106A02272D481800939151 /* ParseObjectMutable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1069FF272D481800939151 /* ParseObjectMutable.swift */; }; - CD106A03272D481800939151 /* ParseObjectMutable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1069FF272D481800939151 /* ParseObjectMutable.swift */; }; F971F4F624DE381A006CB79B /* ParseEncoderExtraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */; }; F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; @@ -1078,7 +1074,6 @@ 91F346B8269B766C005727B6 /* CloudViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudViewModel.swift; sourceTree = ""; }; 91F346BD269B77B5005727B6 /* CloudObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudObservable.swift; sourceTree = ""; }; 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudViewModelTests.swift; sourceTree = ""; }; - CD1069FF272D481800939151 /* ParseObjectMutable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseObjectMutable.swift; sourceTree = ""; }; F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseEncoderExtraTests.swift; sourceTree = ""; }; F97B45B424D9C6F200F4A88B /* ParseCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseCoding.swift; sourceTree = ""; }; F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = ""; }; @@ -1387,7 +1382,6 @@ 700396E925A3892D0052CB31 /* LiveQuerySocketDelegate.swift */, 700396F725A394AE0052CB31 /* ParseLiveQueryDelegate.swift */, 700395D025A147BE0052CB31 /* QuerySubscribable.swift */, - 70A98D812794AB3C009B58F2 /* ParseQueryScorable.swift */, ); path = Protocols; sourceTree = ""; @@ -1411,7 +1405,7 @@ F97B45C524D9C6F200F4A88B /* Fetchable.swift */, 705A9A2E25991C1400B3547F /* Fileable.swift */, 70BC988F252A5B5C00FF3074 /* Objectable.swift */, - CD1069FF272D481800939151 /* ParseObjectMutable.swift */, + 70A98D812794AB3C009B58F2 /* ParseQueryScorable.swift */, 70647E9B259E3A9A004C1004 /* ParseType.swift */, F97B45C824D9C6F200F4A88B /* Queryable.swift */, 91BB8FCE2690BA70005A6BA5 /* QueryObservable.swift */, @@ -2268,7 +2262,6 @@ 703B090226BD9652005A112F /* ParseAnalytics+async.swift in Sources */, 703B093F26BF47AC005A112F /* ParseApple+async.swift in Sources */, F97B45E624D9C6F200F4A88B /* Query.swift in Sources */, - CD106A00272D481800939151 /* ParseObjectMutable.swift in Sources */, 703B093526BF43D9005A112F /* ParseAnonymous+async.swift in Sources */, 705D950825BE4C08003EF6F8 /* SubscriptionCallback.swift in Sources */, 70C5509225B4A99100B5DBC2 /* AddRelation.swift in Sources */, @@ -2501,7 +2494,6 @@ 703B090326BD9652005A112F /* ParseAnalytics+async.swift in Sources */, 703B094026BF47AC005A112F /* ParseApple+async.swift in Sources */, F97B45E724D9C6F200F4A88B /* Query.swift in Sources */, - CD106A01272D481800939151 /* ParseObjectMutable.swift in Sources */, 703B093626BF43D9005A112F /* ParseAnonymous+async.swift in Sources */, 705D950925BE4C08003EF6F8 /* SubscriptionCallback.swift in Sources */, 70C5509325B4A99100B5DBC2 /* AddRelation.swift in Sources */, @@ -2838,7 +2830,6 @@ 703B090526BD9652005A112F /* ParseAnalytics+async.swift in Sources */, 703B094226BF47AC005A112F /* ParseApple+async.swift in Sources */, F97B466724D9C88600F4A88B /* SecureStorage.swift in Sources */, - CD106A03272D481800939151 /* ParseObjectMutable.swift in Sources */, 703B093826BF43D9005A112F /* ParseAnonymous+async.swift in Sources */, 705D950B25BE4C08003EF6F8 /* SubscriptionCallback.swift in Sources */, 70C5509525B4A99100B5DBC2 /* AddRelation.swift in Sources */, @@ -2978,7 +2969,6 @@ 703B090426BD9652005A112F /* ParseAnalytics+async.swift in Sources */, 703B094126BF47AC005A112F /* ParseApple+async.swift in Sources */, F97B466624D9C88600F4A88B /* SecureStorage.swift in Sources */, - CD106A02272D481800939151 /* ParseObjectMutable.swift in Sources */, 703B093726BF43D9005A112F /* ParseAnonymous+async.swift in Sources */, 705D950A25BE4C08003EF6F8 /* SubscriptionCallback.swift in Sources */, 70C5509425B4A99100B5DBC2 /* AddRelation.swift in Sources */, diff --git a/README.md b/README.md index 84c9570b2..6ec49f9b8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ A pure Swift library that gives you access to the powerful Parse Server backend from your Swift applications. -For more information about the Parse Platform and its features, see the public [documentation][docs]. The ParseSwift SDK is not a port of the [Parse-SDK-iOS-OSX SDK](https://github.com/parse-community/Parse-SDK-iOS-OSX) and though some of it may feel familiar, it is not backwards compatible and is designed with a new philosophy. For more details visit the [api documentation](http://parseplatform.org/Parse-Swift/api/). +For more information about the Parse Platform and its features, see the public [documentation][docs]. The ParseSwift SDK is not a port of the [Parse-SDK-iOS-OSX SDK](https://github.com/parse-community/Parse-SDK-iOS-OSX) and though some of it may feel familiar, it is not backwards compatible and is designed using [protocol oriented programming (POP) and value types](https://www.pluralsight.com/guides/protocol-oriented-programming-in-swift) instead of OOP and reference types. You can learn more about POP by watching [Protocol-Oriented Programming in Swift](https://developer.apple.com/videos/play/wwdc2015/408/) or [Protocol and Value Oriented Programming in UIKit Apps](https://developer.apple.com/videos/play/wwdc2016/419/) videos from previous WWDC's. For more details about ParseSwift, visit the [api documentation](http://parseplatform.org/Parse-Swift/api/). To learn how to use or experiment with ParseSwift, you can run and edit the [ParseSwift.playground](https://github.com/parse-community/Parse-Swift/tree/main/ParseSwift.playground/Pages). You can use the parse-server in [this repo](https://github.com/netreconlab/parse-hipaa/tree/parse-swift) which has docker compose files (`docker-compose up` gives you a working server) configured to connect with the playground files, has [Parse Dashboard](https://github.com/parse-community/parse-dashboard), and can be used with MongoDB or PostgreSQL. You can also configure the Swift Playgrounds to work with your own Parse Server by editing the configuation in [Common.swift](https://github.com/parse-community/Parse-Swift/blob/e9ba846c399257100b285d25d2bd055628b13b4b/ParseSwift.playground/Sources/Common.swift#L4-L19). To learn more, check out [CONTRIBUTING.md](https://github.com/parse-community/Parse-Swift/blob/main/CONTRIBUTING.md#swift-playgrounds). @@ -57,7 +57,7 @@ import PackageDescription let package = Package( name: "YOUR_PROJECT_NAME", dependencies: [ - .package(url: "https://github.com/parse-community/Parse-Swift", from: "3.1.2"), + .package(url: "https://github.com/parse-community/Parse-Swift", from: "4.0.0"), ] ) ``` diff --git a/Sources/ParseSwift/API/API+Command.swift b/Sources/ParseSwift/API/API+Command.swift index 6560d834a..e0475bb82 100644 --- a/Sources/ParseSwift/API/API+Command.swift +++ b/Sources/ParseSwift/API/API+Command.swift @@ -385,13 +385,15 @@ internal extension API.Command { // MARK: Saving ParseObjects static func save(_ object: T, + original data: Data?, isIgnoreCustomObjectIdConfig: Bool) throws -> API.Command where T: ParseObject { if ParseSwift.configuration.isAllowingCustomObjectIds && object.objectId == nil && !isIgnoreCustomObjectIdConfig { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if object.isSaved { - return try replace(object) // Should be switched to "update" when server supports PATCH. + // MARK: Should be switched to "update" when server supports PATCH. + return try replace(object, original: data) } return create(object) } @@ -411,13 +413,25 @@ internal extension API.Command { mapper: mapper) } - static func replace(_ object: T) throws -> API.Command where T: ParseObject { + static func replace(_ object: T, original data: Data?) throws -> API.Command where T: ParseObject { guard object.objectId != nil else { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } - let mapper = { (data) -> T in - try ParseCoding.jsonDecoder().decode(ReplaceResponse.self, from: data).apply(to: object) + let mapper = { (mapperData: Data) -> T in + var updatedObject = object + updatedObject.originalData = nil + let object = try ParseCoding + .jsonDecoder() + .decode(ReplaceResponse.self, from: mapperData) + .apply(to: updatedObject) + guard let originalData = data, + let original = try? ParseCoding.jsonDecoder().decode(T.self, + from: originalData), + original.hasSameObjectId(as: object) else { + return object + } + return try object.merge(original) } return API.Command(method: .PUT, path: object.endpoint, @@ -425,13 +439,25 @@ internal extension API.Command { mapper: mapper) } - static func update(_ object: T) throws -> API.Command where T: ParseObject { + static func update(_ object: T, original data: Data?) throws -> API.Command where T: ParseObject { guard object.objectId != nil else { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } - let mapper = { (data) -> T in - try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: object) + let mapper = { (mapperData: Data) -> T in + var updatedObject = object + updatedObject.originalData = nil + let object = try ParseCoding + .jsonDecoder() + .decode(UpdateResponse.self, from: mapperData) + .apply(to: updatedObject) + guard let originalData = data, + let original = try? ParseCoding.jsonDecoder().decode(T.self, + from: originalData), + original.hasSameObjectId(as: object) else { + return object + } + return try object.merge(original) } return API.Command(method: .PATCH, path: object.endpoint, diff --git a/Sources/ParseSwift/Coding/ParseCoding.swift b/Sources/ParseSwift/Coding/ParseCoding.swift index 7b232314e..14d7df0f1 100644 --- a/Sources/ParseSwift/Coding/ParseCoding.swift +++ b/Sources/ParseSwift/Coding/ParseCoding.swift @@ -37,7 +37,8 @@ extension ParseCoding { /// types in a way meaninful for a Parse Server to consume. static func parseEncoder() -> ParseEncoder { ParseEncoder( - dateEncodingStrategy: parseDateEncodingStrategy + dateEncodingStrategy: parseDateEncodingStrategy, + outputFormatting: .sortedKeys ) } } diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index cf45a1dbc..6e682af88 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -54,6 +54,7 @@ extension Dictionary: _JSONStringDictionaryEncodableMarker where Key == String, */ public struct ParseEncoder { let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? + let outputFormatting: JSONEncoder.OutputFormatting? /// Keys to skip during encoding. public enum SkipKeys { @@ -69,12 +70,22 @@ public struct ParseEncoder { case custom(Set) func keys() -> Set { + let defaultObjectKeys = Set(["createdAt", + "updatedAt", + "objectId", + "className", + "emailVerified", + "id", + "score", + "originalData"]) switch self { case .object: - return Set(["createdAt", "updatedAt", "objectId", "className", "emailVerified", "id", "score"]) + return defaultObjectKeys case .customObjectId: - return Set(["createdAt", "updatedAt", "className", "emailVerified", "id", "score"]) + var mutableKeys = defaultObjectKeys + _ = mutableKeys.remove("objectId") + return mutableKeys case .cloud: return Set(["functionJobName"]) case .none: @@ -86,16 +97,20 @@ public struct ParseEncoder { } init( - dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil + dateEncodingStrategy: JSONEncoder.DateEncodingStrategy? = nil, + outputFormatting: JSONEncoder.OutputFormatting? = .sortedKeys ) { self.dateEncodingStrategy = dateEncodingStrategy + self.outputFormatting = outputFormatting } func encode(_ value: Encodable) throws -> Data { let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: SkipKeys.none.keys()) if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = dateEncodingStrategy - encoder.outputFormatting = .sortedKeys + } + if let outputFormatting = outputFormatting { + encoder.outputFormatting = outputFormatting } return try encoder.encodeObject(value, collectChildren: false, @@ -115,7 +130,9 @@ public struct ParseEncoder { if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = dateEncodingStrategy } - encoder.outputFormatting = .sortedKeys + if let outputFormatting = outputFormatting { + encoder.outputFormatting = outputFormatting + } return try encoder.encodeObject(value, collectChildren: false, uniquePointer: nil, @@ -137,7 +154,9 @@ public struct ParseEncoder { if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = dateEncodingStrategy } - encoder.outputFormatting = .sortedKeys + if let outputFormatting = outputFormatting { + encoder.outputFormatting = outputFormatting + } return try encoder.encodeObject(value, collectChildren: true, uniquePointer: try? value.toPointer(), @@ -160,7 +179,9 @@ public struct ParseEncoder { if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = dateEncodingStrategy } - encoder.outputFormatting = .sortedKeys + if let outputFormatting = outputFormatting { + encoder.outputFormatting = outputFormatting + } return try encoder.encodeObject(value, collectChildren: collectChildren, uniquePointer: nil, diff --git a/Sources/ParseSwift/InternalObjects/BaseParseInstallation.swift b/Sources/ParseSwift/InternalObjects/BaseParseInstallation.swift index b7a39fa17..a4476c3b3 100644 --- a/Sources/ParseSwift/InternalObjects/BaseParseInstallation.swift +++ b/Sources/ParseSwift/InternalObjects/BaseParseInstallation.swift @@ -24,6 +24,7 @@ internal struct BaseParseInstallation: ParseInstallation { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? static func createNewInstallationIfNeeded() { guard let installationId = Self.currentContainer.installationId, diff --git a/Sources/ParseSwift/InternalObjects/BaseParseUser.swift b/Sources/ParseSwift/InternalObjects/BaseParseUser.swift index 091409f71..9516445a7 100644 --- a/Sources/ParseSwift/InternalObjects/BaseParseUser.swift +++ b/Sources/ParseSwift/InternalObjects/BaseParseUser.swift @@ -18,4 +18,5 @@ internal struct BaseParseUser: ParseUser { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? } diff --git a/Sources/ParseSwift/LiveQuery/Protocols/ParseQueryScorable.swift b/Sources/ParseSwift/LiveQuery/Protocols/ParseQueryScorable.swift deleted file mode 100644 index e85163e4b..000000000 --- a/Sources/ParseSwift/LiveQuery/Protocols/ParseQueryScorable.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ParseQueryScorable.swift -// ParseSwift -// -// Created by Corey Baker on 1/16/22. -// Copyright © 2022 Parse Community. All rights reserved. -// - -import Foundation - -/** - Conform to this protocol to add the required properties to your `ParseObject` - for using `QueryConstraint.matchesText()` and `Query.sortByTextScore()`. - - note: In order to sort you must use `Query.sortByTextScore()`. - To retrieve the weight/rank, access the "score" property of your `ParseObject`. - */ -public protocol ParseQueryScorable { - /** - The weight/rank of a `QueryConstraint.matchesText()`. - */ - var score: Double? { get } -} diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 7efe18504..3386014e7 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -35,43 +35,58 @@ import Foundation public protocol ParseInstallation: ParseObject { /** - The device type for the `ParseInstallation`. + The device type for the `ParseInstallation`. */ var deviceType: String? { get set } /** - The installationId for the `ParseInstallation`. + The installationId for the `ParseInstallation`. */ var installationId: String? { get set } /** - The device token for the `ParseInstallation`. + The device token for the `ParseInstallation`. */ var deviceToken: String? { get set } /** - The badge for the `ParseInstallation`. + The badge for the `ParseInstallation`. */ var badge: Int? { get set } /** - The name of the time zone for the `ParseInstallation`. + The name of the time zone for the `ParseInstallation`. */ var timeZone: String? { get set } /** - The channels for the `ParseInstallation`. + The channels for the `ParseInstallation`. */ var channels: [String]? { get set } + /** + The application name for the `ParseInstallation`. + */ var appName: String? { get set } + /** + The application identifier for the `ParseInstallation`. + */ var appIdentifier: String? { get set } + /** + The application version for the `ParseInstallation`. + */ var appVersion: String? { get set } + /** + The sdk version for the `ParseInstallation`. + */ var parseVersion: String? { get set } + /** + The locale identifier for the `ParseInstallation`. + */ var localeIdentifier: String? { get set } } @@ -80,6 +95,67 @@ public extension ParseInstallation { static var className: String { "_Installation" } + + func mergeParse(_ object: Self) throws -> Self { + guard hasSameObjectId(as: object) == true else { + throw ParseError(code: .unknownError, + message: "objectId's of objects don't match") + } + var updatedInstallation = self + if shouldRestoreKey(\.ACL, + original: object) { + updatedInstallation.ACL = object.ACL + } + if shouldRestoreKey(\.deviceType, + original: object) { + updatedInstallation.deviceType = object.deviceType + } + if shouldRestoreKey(\.installationId, + original: object) { + updatedInstallation.installationId = object.installationId + } + if shouldRestoreKey(\.deviceToken, + original: object) { + updatedInstallation.deviceToken = object.deviceToken + } + if shouldRestoreKey(\.badge, + original: object) { + updatedInstallation.badge = object.badge + } + if shouldRestoreKey(\.timeZone, + original: object) { + updatedInstallation.timeZone = object.timeZone + } + if shouldRestoreKey(\.channels, + original: object) { + updatedInstallation.channels = object.channels + } + if shouldRestoreKey(\.appName, + original: object) { + updatedInstallation.appName = object.appName + } + if shouldRestoreKey(\.appIdentifier, + original: object) { + updatedInstallation.appIdentifier = object.appIdentifier + } + if shouldRestoreKey(\.appVersion, + original: object) { + updatedInstallation.appVersion = object.appVersion + } + if shouldRestoreKey(\.parseVersion, + original: object) { + updatedInstallation.parseVersion = object.parseVersion + } + if shouldRestoreKey(\.localeIdentifier, + original: object) { + updatedInstallation.localeIdentifier = object.localeIdentifier + } + return updatedInstallation + } + + func merge(_ object: Self) throws -> Self { + try mergeParse(object) + } } // MARK: Convenience @@ -191,8 +267,9 @@ public extension ParseInstallation { } internal static func saveCurrentContainerToKeychain() { + Self.currentContainer.currentInstallation?.originalData = nil #if !os(Linux) && !os(Android) && !os(Windows) - try? KeychainStore.shared.set(Self.currentContainer, for: ParseStorage.Keys.currentInstallation) + try? KeychainStore.shared.set(currentContainer, for: ParseStorage.Keys.currentInstallation) #endif } @@ -664,7 +741,7 @@ extension ParseInstallation { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if isSaved { - return try replaceCommand() // Should be switched to "updateCommand" when server supports PATCH. + return try replaceCommand() // MARK: Should be switched to "updateCommand" when server supports PATCH. } return createCommand() } @@ -690,8 +767,18 @@ extension ParseInstallation { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } - let mapper = { (data) -> Self in - try ParseCoding.jsonDecoder().decode(ReplaceResponse.self, from: data).apply(to: self) + let mapper = { (data: Data) -> Self in + var updatedObject = self + updatedObject.originalData = nil + let object = try ParseCoding.jsonDecoder().decode(ReplaceResponse.self, from: data).apply(to: updatedObject) + // MARK: The lines below should be removed when server supports PATCH. + guard let originalData = self.originalData, + let original = try? ParseCoding.jsonDecoder().decode(Self.self, + from: originalData), + original.hasSameObjectId(as: object) else { + return object + } + return try object.merge(original) } return API.Command(method: .PUT, path: endpoint, @@ -704,8 +791,17 @@ extension ParseInstallation { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } - let mapper = { (data) -> Self in - try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: self) + let mapper = { (data: Data) -> Self in + var updatedObject = self + updatedObject.originalData = nil + let object = try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: updatedObject) + guard let originalData = self.originalData, + let original = try? ParseCoding.jsonDecoder().decode(Self.self, + from: originalData), + original.hasSameObjectId(as: object) else { + return object + } + return try object.merge(original) } return API.Command(method: .PATCH, path: endpoint, diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index bd600eae2..03731e668 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -16,17 +16,19 @@ import Foundation If you are using value types the the compiler will assist you with conforming to `ParseObject` protocol. If you are thinking of using reference types, see the warning. - It's recommended the developer conforms to the `ParseObjectMutable` protocol. - Gets an empty version of the respective object. This can be used when you only need to update a - a subset of the fields of an object as oppose to updating every field of an object. Using an empty object and updating - a subset of the fields reduces the amount of data sent between client and server when using `save` and `saveAll` - to update objects. + After a `ParseObject`is saved/created to a Parse Server. It is recommended to conduct the rest of your updates on a + `mergeable` copy of your `ParseObject`. This allows a subset of the fields to be updated (PATCH) of an object + as oppose to replacing all of the fields of an object (PUT). This reduces the amount of data + sent between client and server when using `save`, `saveAll`, `update`, + `updateAll`, `replace`, `replaceAll`, to update objects. - - important: It is recommended that all added properties be optional properties so they can eventually be used as + - important: It is required that all added properties be optional properties so they can eventually be used as Parse `Pointer`'s. If a developer really wants to have a required key, they should require it on the server-side or create methods to check the respective properties on the client-side before saving objects. See [here](https://github.com/parse-community/Parse-Swift/issues/157#issuecomment-858671025) for more information. + - important: To take advantage of `mergeable`, the developer should implement the `merge` method in every + `ParseObject`. - warning: If you plan to use "reference types" (classes), you are using at your risk as this SDK is not designed for reference types and may have unexpected behavior when it comes to threading. You will also need to implement your own `==` method to conform to `Equatable` along with with the `hash` method to conform to `Hashable`. @@ -44,7 +46,88 @@ public protocol ParseObject: Objectable, Identifiable, Hashable, CustomDebugStringConvertible, - CustomStringConvertible { } + CustomStringConvertible { + + /** + A JSON encoded version of this `ParseObject` before `mergeable` was called and + properties were changed. + - warning: This property is not intended to be set or modified by the developer. + */ + var originalData: Data? { get set } + + /** + An empty copy of the respective object that allows you to update a + a subset of the fields (PATCH) of an object as oppose to replacing an object (PUT). + - note: It is recommended to use this to create a mergeable copy of your `ParseObject`. + - warning: `mergeable` should only be used on `ParseObject`'s that have already + been saved at least once to a Parse Server and have a valid `objectId`. In addition, + the developer should have implemented added all of their properties to `merge`. + */ + var mergeable: Self { get } + + /** + Determines if a `KeyPath` of the current `ParseObject` should be restored + by comparing it to another `ParseObject`. + - parameter original: The original `ParseObject`. + - returns: Returns a `true` if the keyPath should be restored or `false` otherwise. + */ + func shouldRestoreKey(_ key: KeyPath, + original: Self) -> Bool where W: Equatable + + /** + Merges two `ParseObject`'s with the resulting object consisting of all modified + and unchanged Parse properties. + - parameter object: The original installation. + - returns: The updated installation. + - throws: An error of type `ParseError`. + - note: This is used in combination with `merge` to only send updated + properties to the server and then merge those changes with the original object. + - warning: You should only call this method and shouldn't implement it directly + as it's already implemented for developers to use. + */ + func mergeParse(_ object: Self) throws -> Self + + /** + Merges two `ParseObject`'s with the resulting object consisting of all modified + and unchanged properties. + + //: Create your own value typed `ParseObject`. + struct GameScore: ParseObject { + //: These are required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties. + var points: Int? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + return updated + } + } + + - parameter object: The original object. + - returns: The merged object. + - throws: An error of type `ParseError`. + - note: Use this in combination with `ParseMutable` to only send updated + properties to the server and then merge those changes with the original object. + - important: It is recommend you provide an implementation of this method + for all of your `ParseObject`'s as the developer has access to all properties of a + `ParseObject`. You should always call `mergeParse` + in the beginning of your implementation to handle all default Parse properties. In addition, + use `shouldRestoreKey` to compare key modifications between objects. + */ + func merge(_ object: Self) throws -> Self + + init() +} // MARK: Default Implementations public extension ParseObject { @@ -61,6 +144,14 @@ public extension ParseObject { return objectId } + var mergeable: Self { + var object = Self() + object.objectId = objectId + object.createdAt = createdAt + object.originalData = try? ParseCoding.jsonEncoder().encode(self) + return object + } + /** Determines if two objects have the same objectId. - parameter as: Object to compare. @@ -77,6 +168,28 @@ public extension ParseObject { func toPointer() throws -> Pointer { return try Pointer(self) } + + func shouldRestoreKey(_ key: KeyPath, + original: Self) -> Bool where W: Equatable { + self[keyPath: key] == nil && original[keyPath: key] != self[keyPath: key] + } + + func mergeParse(_ object: Self) throws -> Self { + guard hasSameObjectId(as: object) == true else { + throw ParseError(code: .unknownError, + message: "objectId's of objects don't match") + } + var updated = self + if shouldRestoreKey(\.ACL, + original: object) { + updated.ACL = object.ACL + } + return updated + } + + func merge(_ object: Self) throws -> Self { + return try mergeParse(object) + } } // MARK: Batch Support @@ -948,7 +1061,9 @@ extension ParseObject { } internal func saveCommand(isIgnoreCustomObjectIdConfig: Bool = false) throws -> API.Command { - try API.Command.save(self, isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) + try API.Command.save(self, + original: originalData, + isIgnoreCustomObjectIdConfig: isIgnoreCustomObjectIdConfig) } internal func createCommand() -> API.Command { @@ -956,11 +1071,13 @@ extension ParseObject { } internal func replaceCommand() throws -> API.Command { - try API.Command.replace(self) + try API.Command.replace(self, + original: originalData) } internal func updateCommand() throws -> API.Command { - try API.Command.update(self) + try API.Command.update(self, + original: originalData) } // swiftlint:disable:next function_body_length diff --git a/Sources/ParseSwift/Objects/ParseRole.swift b/Sources/ParseSwift/Objects/ParseRole.swift index 876cec669..91d2a80d1 100644 --- a/Sources/ParseSwift/Objects/ParseRole.swift +++ b/Sources/ParseSwift/Objects/ParseRole.swift @@ -25,12 +25,11 @@ public protocol ParseRole: ParseObject { and cannot be set once the role has been saved. - warning: A role's name can only contain alphanumeric characters, `_`, `-`, and spaces. */ - var name: String { get set } + var name: String? { get set } /** Create a `ParseRole`. It's best to use the provided initializers, `init(name: String)` - or `init(name: String, acl: ParseACL)`. The provided initializers will overwrite - whatever name is specified here, so you can use `self.name = ""` + or `init(name: String, acl: ParseACL)`. */ init() } @@ -49,6 +48,7 @@ public extension ParseRole { init(name: String) throws { try Self.checkName(name) self.init() + self.name = name } /** diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 5d4e6bb14..dc1770739 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -37,32 +37,40 @@ public protocol ParseUser: ParseObject { var authData: [String: [String: String]?]? { get set } } -// MARK: SignupLoginBody -struct SignupLoginBody: ParseType { - var username: String? - var password: String? - var authData: [String: [String: String]?]? - - init(username: String, password: String) { - self.username = username - self.password = password - } - - init(authData: [String: [String: String]?]) { - self.authData = authData - } -} - -// MARK: EmailBody -struct EmailBody: ParseType { - let email: String -} - // MARK: Default Implementations public extension ParseUser { static var className: String { "_User" } + + func mergeParse(_ object: Self) throws -> Self { + guard hasSameObjectId(as: object) == true else { + throw ParseError(code: .unknownError, + message: "objectId's of objects do not match") + } + var updatedUser = self + if shouldRestoreKey(\.ACL, + original: object) { + updatedUser.ACL = object.ACL + } + if shouldRestoreKey(\.username, + original: object) { + updatedUser.username = object.username + } + if shouldRestoreKey(\.email, + original: object) { + updatedUser.email = object.email + } + if shouldRestoreKey(\.authData, + original: object) { + updatedUser.authData = object.authData + } + return updatedUser + } + + func merge(_ object: Self) throws -> Self { + try mergeParse(object) + } } // MARK: Convenience @@ -116,8 +124,9 @@ public extension ParseUser { } internal static func saveCurrentContainerToKeychain() { + Self.currentContainer?.currentUser?.originalData = nil #if !os(Linux) && !os(Android) && !os(Windows) - try? KeychainStore.shared.set(Self.currentContainer, for: ParseStorage.Keys.currentUser) + try? KeychainStore.shared.set(currentContainer, for: ParseStorage.Keys.currentUser) #endif } @@ -153,6 +162,27 @@ public extension ParseUser { } } +// MARK: SignupLoginBody +struct SignupLoginBody: ParseType { + var username: String? + var password: String? + var authData: [String: [String: String]?]? + + init(username: String, password: String) { + self.username = username + self.password = password + } + + init(authData: [String: [String: String]?]) { + self.authData = authData + } +} + +// MARK: EmailBody +struct EmailBody: ParseType { + let email: String +} + // MARK: Logging In extension ParseUser { @@ -1046,7 +1076,7 @@ extension ParseUser { throw ParseError(code: .missingObjectId, message: "objectId must not be nil") } if isSaved { - return try replaceCommand() // Should be switched to "updateCommand" when server supports PATCH. + return try replaceCommand() // MARK: Should be switched to "updateCommand" when server supports PATCH. } return createCommand() } @@ -1087,8 +1117,18 @@ extension ParseUser { } #endif } - let mapper = { (data) -> Self in - try ParseCoding.jsonDecoder().decode(ReplaceResponse.self, from: data).apply(to: self) + let mapper = { (data: Data) -> Self in + var updatedObject = self + updatedObject.originalData = nil + let object = try ParseCoding.jsonDecoder().decode(ReplaceResponse.self, from: data).apply(to: updatedObject) + // MARK: The lines below should be removed when server supports PATCH. + guard let originalData = self.originalData, + let original = try? ParseCoding.jsonDecoder().decode(Self.self, + from: originalData), + original.hasSameObjectId(as: object) else { + return object + } + return try object.merge(original) } return API.Command(method: .PUT, path: endpoint, @@ -1116,8 +1156,17 @@ extension ParseUser { } #endif } - let mapper = { (data) -> Self in - try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: self) + let mapper = { (data: Data) -> Self in + var updatedObject = self + updatedObject.originalData = nil + let object = try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: updatedObject) + guard let originalData = self.originalData, + let original = try? ParseCoding.jsonDecoder().decode(Self.self, + from: originalData), + original.hasSameObjectId(as: object) else { + return object + } + return try object.merge(original) } return API.Command(method: .PATCH, path: endpoint, diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index c7ec95433..4f8da54fb 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "3.1.2" + static let version = "4.0.0" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Sources/ParseSwift/Protocols/ParseObjectMutable.swift b/Sources/ParseSwift/Protocols/ParseObjectMutable.swift deleted file mode 100644 index d692b56fc..000000000 --- a/Sources/ParseSwift/Protocols/ParseObjectMutable.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// ParseObjectMutable.swift -// ParseSwift -// -// Created by Damian Van de Kauter on 30/10/2021. -// Copyright © 2021 Parse Community. All rights reserved. -// - -import Foundation - -/** - The `ParseObjectMutable` protocol creates an empty copy of the respective object. - This can be used when you only need to update a subset of the fields (PATCH) of an object - as oppose to replacing (PUT) an object. - Using the mutable copy and updating a subset of the fields reduces the amount of data - sent between client and server when using `save` and `saveAll` - to update objects. - - **Example use case for `ParseUser`:** - ```` - struct User: ParseUser, ParseObjectMutable { - //: These are required by `ParseObject`. - var objectId: String? - var createdAt: Date? - var updatedAt: Date? - var ACL: ParseACL? - - //: These are required by `ParseUser`. - var username: String? - var email: String? - var emailVerified: Bool? - var password: String? - var authData: [String: [String: String]?]? - - //: Your custom keys. - var customKey: String? - var gameScore: GameScore? - var targetScore: GameScore? - var allScores: [GameScore]? - } - - var user = User.current?.mutable - user?.customKey = "newValue" - - do { - try await user?.save() - } catch { - // Handle error - } - ```` - - **Example use case for a general `ParseObject`:** - ```` - struct GameScore: ParseObject, ParseObjectMutable { - //: These are required by ParseObject - var objectId: String? - var createdAt: Date? - var updatedAt: Date? - var ACL: ParseACL? - - //: Your own properties. - var points: Int = 0 - } - //: It's recommended to place custom initializers in an extension - //: to preserve the convenience initializer. - extension GameScore { - - init(points: Int) { - self.points = points - } - - init(objectId: String?) { - self.objectId = objectId - } - } - - var newScore = GameScore(points: 10).mutable - newScore.points = 200 - - do { - try await newScore.save() - } catch { - // Handle error - } - ```` - - - warning: Using the `ParseObjectMutable` protocol requires the developer to - initialize all of the `ParseObject` properties. This can be accomplished by making all properties - optional or setting default values for non-optional properties. - This also allows your objects to be used as Parse `Pointer`‘s. - It's recommended to place custom initializers in an extension - to preserve the convenience initializer. -*/ -public protocol ParseObjectMutable: ParseObject { - init() - - /** - An empty copy of the respective object that allows you to update a - a subset of the fields of an object as oppose to updating every field of an object. - - note: It is recommended to use this to create a mutable copy of your `ParseObject`. - */ - var mutable: Self { get } -} - -public extension ParseObjectMutable { - var mutable: Self { - var object = Self() - object.objectId = objectId - object.createdAt = createdAt - return object - } -} diff --git a/Sources/ParseSwift/Protocols/ParseQueryScorable.swift b/Sources/ParseSwift/Protocols/ParseQueryScorable.swift new file mode 100644 index 000000000..f77fb71a4 --- /dev/null +++ b/Sources/ParseSwift/Protocols/ParseQueryScorable.swift @@ -0,0 +1,42 @@ +// +// ParseQueryScorable.swift +// ParseSwift +// +// Created by Corey Baker on 1/16/22. +// Copyright © 2022 Parse Community. All rights reserved. +// + +import Foundation + +/** + Conform to this protocol to add the required properties to your `ParseObject` + for using `QueryConstraint.matchesText()` and `Query.sortByTextScore()`. + - note: In order to sort you must use `Query.sortByTextScore()`. + To retrieve the weight/rank, access the "score" property of your `ParseObject`. + */ +public protocol ParseQueryScorable { + /** + The weight/rank of a `QueryConstraint.matchesText()`. + */ + var score: Double? { get } +} + +// MARK: ParseQueryScorable +extension Query where T: ParseObject & ParseQueryScorable { + /** + Method to sort the full text search by text score. + - parameter value: String or Object of index that should be used when executing query. + - note: Your `ParseObject` should conform to `ParseQueryScorable` to retrieve + the weight/rank via the "score" property of your `ParseObject`. + */ + public func sortByTextScore() -> Query { + var mutableQuery = self + let ascendingScore = Order.ascending(QueryConstraint.Comparator.score.rawValue) + if mutableQuery.order != nil { + mutableQuery.order!.append(ascendingScore) + } else { + mutableQuery.order = [ascendingScore] + } + return mutableQuery.select(QueryConstraint.Comparator.score.rawValue) + } +} diff --git a/Sources/ParseSwift/Types/ParseACL.swift b/Sources/ParseSwift/Types/ParseACL.swift index 8e3df7255..ca8c39c48 100644 --- a/Sources/ParseSwift/Types/ParseACL.swift +++ b/Sources/ParseSwift/Types/ParseACL.swift @@ -202,7 +202,8 @@ public struct ParseACL: ParseType, - returns: `true` if the `ParseRole` has read access, otherwise `false`. */ public func getReadAccess(role: T) -> Bool where T: ParseRole { - get(toRole(roleName: role.name), access: .read) + guard let name = role.name else { return false } + return get(toRole(roleName: name), access: .read) } /** @@ -224,7 +225,8 @@ public struct ParseACL: ParseType, - returns: `true` if the role has read access, otherwise `false`. */ public func getWriteAccess(role: T) -> Bool where T: ParseRole { - get(toRole(roleName: role.name), access: .write) + guard let name = role.name else { return false } + return get(toRole(roleName: name), access: .write) } /** @@ -244,7 +246,8 @@ public struct ParseACL: ParseType, - parameter role: The `ParseRole` to set access for. */ public mutating func setReadAccess(role: T, value: Bool) where T: ParseRole { - set(toRole(roleName: role.name), access: .read, value: value) + guard let name = role.name else { return } + set(toRole(roleName: name), access: .read, value: value) } /** @@ -264,7 +267,8 @@ public struct ParseACL: ParseType, - parameter role: The `ParseRole` to set access for. */ public mutating func setWriteAccess(role: T, value: Bool) where T: ParseRole { - set(toRole(roleName: role.name), access: .write, value: value) + guard let name = role.name else { return } + set(toRole(roleName: name), access: .write, value: value) } private func toRole(roleName: String) -> String { diff --git a/Sources/ParseSwift/Types/ParseConfig.swift b/Sources/ParseSwift/Types/ParseConfig.swift index 7a7e4116b..355b95ec8 100644 --- a/Sources/ParseSwift/Types/ParseConfig.swift +++ b/Sources/ParseSwift/Types/ParseConfig.swift @@ -105,7 +105,7 @@ extension ParseConfig { internal func updateCommand() -> API.Command, Bool> { let body = ConfigUpdateBody(params: self) - return API.Command(method: .PUT, // Should be switched to ".PATCH" when server supports PATCH. + return API.Command(method: .PUT, // MARK: Should be switched to ".PATCH" when server supports PATCH. path: .config, body: body) { (data) -> Bool in let updated = try ParseCoding.jsonDecoder().decode(ConfigUpdateResponse.self, from: data).result diff --git a/Sources/ParseSwift/Types/ParseOperation.swift b/Sources/ParseSwift/Types/ParseOperation.swift index ef732b706..f4a6af2ac 100644 --- a/Sources/ParseSwift/Types/ParseOperation.swift +++ b/Sources/ParseSwift/Types/ParseOperation.swift @@ -427,7 +427,7 @@ extension ParseOperation { } func saveCommand() throws -> API.NonParseBodyCommand, T> { - // Should be switched to ".PATCH" when server supports PATCH. + // MARK: Should be switched to ".PATCH" when server supports PATCH. API.NonParseBodyCommand(method: .PUT, path: target.endpoint, body: self) { try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: $0).apply(to: self.target) } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index a17510f4d..815351b19 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -172,23 +172,6 @@ public struct Query: Encodable, Equatable where T: ParseObject { return mutableQuery } - /** - Method to sort the full text search by text score. - - parameter value: String or Object of index that should be used when executing query. - - note: Your `ParseObject` should conform to `ParseQueryScorable` to retrieve - the weight/rank via the "score" property of your `ParseObject`. - */ - public func sortByTextScore() -> Query { - var mutableQuery = self - let ascendingScore = Order.ascending(QueryConstraint.Comparator.score.rawValue) - if mutableQuery.order != nil { - mutableQuery.order!.append(ascendingScore) - } else { - mutableQuery.order = [ascendingScore] - } - return mutableQuery.select(QueryConstraint.Comparator.score.rawValue) - } - /** Changes the read preference that the backend will use when performing the query to the database. - parameter readPreference: The read preference for the main query. diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index 39172219d..1ec49d01e 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -22,6 +22,8 @@ class APICommandTests: XCTestCase { var ACL: ParseACL? var name = "First" + + var originalData: Data? } override func setUpWithError() throws { @@ -53,6 +55,7 @@ class APICommandTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -72,6 +75,7 @@ class APICommandTests: XCTestCase { var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/IOS13Tests.swift b/Tests/ParseSwiftTests/IOS13Tests.swift index 385a07d94..51effb8c8 100644 --- a/Tests/ParseSwiftTests/IOS13Tests.swift +++ b/Tests/ParseSwiftTests/IOS13Tests.swift @@ -20,6 +20,8 @@ class IOS13Tests: XCTestCase { // swiftlint:disable:this type_body_length var ACL: ParseACL? + var originalData: Data? + var name = "First" } @@ -30,6 +32,7 @@ class IOS13Tests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? diff --git a/Tests/ParseSwiftTests/InitializeSDKTests.swift b/Tests/ParseSwiftTests/InitializeSDKTests.swift index 1a21ab1b9..1f61965fb 100644 --- a/Tests/ParseSwiftTests/InitializeSDKTests.swift +++ b/Tests/ParseSwiftTests/InitializeSDKTests.swift @@ -27,6 +27,7 @@ class InitializeSDKTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? var customKey: String? } diff --git a/Tests/ParseSwiftTests/ParseACLTests.swift b/Tests/ParseSwiftTests/ParseACLTests.swift index 887ecbb01..44ca16be8 100644 --- a/Tests/ParseSwiftTests/ParseACLTests.swift +++ b/Tests/ParseSwiftTests/ParseACLTests.swift @@ -40,6 +40,7 @@ class ParseACLTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -59,6 +60,7 @@ class ParseACLTests: XCTestCase { var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -90,13 +92,10 @@ class ParseACLTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // provided by Role - var name: String - - init() { - self.name = "roleMe" - } + var name: String? } func testCantSetDefaultACLWhenNotLoggedIn() throws { diff --git a/Tests/ParseSwiftTests/ParseAnonymousAsyncTests.swift b/Tests/ParseSwiftTests/ParseAnonymousAsyncTests.swift index fd7b790ce..5c60fd928 100644 --- a/Tests/ParseSwiftTests/ParseAnonymousAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseAnonymousAsyncTests.swift @@ -22,6 +22,7 @@ class ParseAnonymousAsyncTests: XCTestCase { // swiftlint:disable:this type_body var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseAnonymousAsyncTests: XCTestCase { // swiftlint:disable:this type_body var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift index 40a92ff2d..a5e3f678b 100644 --- a/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift @@ -22,6 +22,7 @@ class ParseAnonymousCombineTests: XCTestCase { // swiftlint:disable:this type_bo var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseAnonymousCombineTests: XCTestCase { // swiftlint:disable:this type_bo var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAnonymousTests.swift b/Tests/ParseSwiftTests/ParseAnonymousTests.swift index c051cdeb2..02b47cb12 100644 --- a/Tests/ParseSwiftTests/ParseAnonymousTests.swift +++ b/Tests/ParseSwiftTests/ParseAnonymousTests.swift @@ -19,6 +19,7 @@ class ParseAnonymousTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -35,6 +36,7 @@ class ParseAnonymousTests: XCTestCase { var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAppleAsyncTests.swift b/Tests/ParseSwiftTests/ParseAppleAsyncTests.swift index cf70488ff..b133a7599 100644 --- a/Tests/ParseSwiftTests/ParseAppleAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseAppleAsyncTests.swift @@ -22,6 +22,7 @@ class ParseAppleAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseAppleAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAppleCombineTests.swift b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift index 69bfd1942..10da1b71b 100644 --- a/Tests/ParseSwiftTests/ParseAppleCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift @@ -22,6 +22,7 @@ class ParseAppleCombineTests: XCTestCase { // swiftlint:disable:this type_body_l var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseAppleCombineTests: XCTestCase { // swiftlint:disable:this type_body_l var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAppleTests.swift b/Tests/ParseSwiftTests/ParseAppleTests.swift index 61a8479d8..c651ebbe1 100644 --- a/Tests/ParseSwiftTests/ParseAppleTests.swift +++ b/Tests/ParseSwiftTests/ParseAppleTests.swift @@ -18,6 +18,7 @@ class ParseAppleTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -34,6 +35,7 @@ class ParseAppleTests: XCTestCase { var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift index 73e7d80ad..2f73ef4dd 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift @@ -25,6 +25,7 @@ class ParseAuthenticationAsyncTests: XCTestCase { // swiftlint:disable:this type var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -41,6 +42,7 @@ class ParseAuthenticationAsyncTests: XCTestCase { // swiftlint:disable:this type var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAuthenticationCombineTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationCombineTests.swift index 02cc98cb1..ebf465945 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationCombineTests.swift @@ -25,6 +25,7 @@ class ParseAuthenticationCombineTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -41,6 +42,7 @@ class ParseAuthenticationCombineTests: XCTestCase { var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift index 50944b45c..083973325 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -25,6 +25,7 @@ class ParseAuthenticationTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -41,6 +42,7 @@ class ParseAuthenticationTests: XCTestCase { var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseConfigAsyncTests.swift b/Tests/ParseSwiftTests/ParseConfigAsyncTests.swift index 150bd74cf..f0bd52a09 100644 --- a/Tests/ParseSwiftTests/ParseConfigAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseConfigAsyncTests.swift @@ -27,6 +27,7 @@ class ParseConfigAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -46,6 +47,7 @@ class ParseConfigAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseConfigCombineTests.swift b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift index 02fce40ac..e8b3d93f0 100644 --- a/Tests/ParseSwiftTests/ParseConfigCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift @@ -27,6 +27,7 @@ class ParseConfigCombineTests: XCTestCase { // swiftlint:disable:this type_body_ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -46,6 +47,7 @@ class ParseConfigCombineTests: XCTestCase { // swiftlint:disable:this type_body_ var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseConfigTests.swift b/Tests/ParseSwiftTests/ParseConfigTests.swift index 6fc8600bf..b5627881b 100644 --- a/Tests/ParseSwiftTests/ParseConfigTests.swift +++ b/Tests/ParseSwiftTests/ParseConfigTests.swift @@ -24,6 +24,7 @@ class ParseConfigTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -43,6 +44,7 @@ class ParseConfigTests: XCTestCase { // swiftlint:disable:this type_body_length var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift b/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift index 06070f666..43d11c4f0 100644 --- a/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift +++ b/Tests/ParseSwiftTests/ParseEncoderTests/ParseEncoderExtraTests.swift @@ -17,6 +17,7 @@ class ParseEncoderTests: XCTestCase { var updatedAt: Date? var ACL: ParseACL? var score: Double? + var originalData: Data? //: ParseUser property var emailVerified: Bool? diff --git a/Tests/ParseSwiftTests/ParseFacebookAsyncTests.swift b/Tests/ParseSwiftTests/ParseFacebookAsyncTests.swift index 49a67746d..cbfced4f0 100644 --- a/Tests/ParseSwiftTests/ParseFacebookAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseFacebookAsyncTests.swift @@ -22,6 +22,7 @@ class ParseFacebookAsyncTests: XCTestCase { // swiftlint:disable:this type_body_ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseFacebookAsyncTests: XCTestCase { // swiftlint:disable:this type_body_ var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseFacebookCombineTests.swift b/Tests/ParseSwiftTests/ParseFacebookCombineTests.swift index 4e2b92d02..fa86f2995 100644 --- a/Tests/ParseSwiftTests/ParseFacebookCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseFacebookCombineTests.swift @@ -22,6 +22,7 @@ class ParseFacebookCombineTests: XCTestCase { // swiftlint:disable:this type_bod var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseFacebookCombineTests: XCTestCase { // swiftlint:disable:this type_bod var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseFacebookTests.swift b/Tests/ParseSwiftTests/ParseFacebookTests.swift index 4c2ec39f4..fc2567c89 100644 --- a/Tests/ParseSwiftTests/ParseFacebookTests.swift +++ b/Tests/ParseSwiftTests/ParseFacebookTests.swift @@ -18,6 +18,7 @@ class ParseFacebookTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -34,6 +35,7 @@ class ParseFacebookTests: XCTestCase { var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseGitHubCombineTests.swift b/Tests/ParseSwiftTests/ParseGitHubCombineTests.swift index e54eeddc6..542a82c36 100644 --- a/Tests/ParseSwiftTests/ParseGitHubCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseGitHubCombineTests.swift @@ -22,6 +22,7 @@ class ParseGitHubCombineTests: XCTestCase { // swiftlint:disable:this type_body_ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseGitHubCombineTests: XCTestCase { // swiftlint:disable:this type_body_ var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseGitHubTests.swift b/Tests/ParseSwiftTests/ParseGitHubTests.swift index 41539069e..5afa3da85 100644 --- a/Tests/ParseSwiftTests/ParseGitHubTests.swift +++ b/Tests/ParseSwiftTests/ParseGitHubTests.swift @@ -21,6 +21,7 @@ class ParseGitHubTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -37,6 +38,7 @@ class ParseGitHubTests: XCTestCase { // swiftlint:disable:this type_body_length var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseGoogleCombineTests.swift b/Tests/ParseSwiftTests/ParseGoogleCombineTests.swift index 0ac6ff6e0..314ffed97 100644 --- a/Tests/ParseSwiftTests/ParseGoogleCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseGoogleCombineTests.swift @@ -22,6 +22,7 @@ class ParseGoogleCombineTests: XCTestCase { // swiftlint:disable:this type_body_ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseGoogleCombineTests: XCTestCase { // swiftlint:disable:this type_body_ var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseGoogleTests.swift b/Tests/ParseSwiftTests/ParseGoogleTests.swift index 0603f2f32..60779a872 100644 --- a/Tests/ParseSwiftTests/ParseGoogleTests.swift +++ b/Tests/ParseSwiftTests/ParseGoogleTests.swift @@ -21,6 +21,7 @@ class ParseGoogleTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -37,6 +38,7 @@ class ParseGoogleTests: XCTestCase { // swiftlint:disable:this type_body_length var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift index 47561106e..346ed35bb 100644 --- a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift @@ -23,6 +23,7 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -42,6 +43,7 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -82,7 +84,37 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? var customKey: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + return updated + } + } + + struct InstallationDefault: ParseInstallation { + var installationId: String? + var deviceType: String? + var deviceToken: String? + var badge: Int? + var timeZone: String? + var channels: [String]? + var appName: String? + var appIdentifier: String? + var appVersion: String? + var parseVersion: String? + var localeIdentifier: String? + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var originalData: Data? } let testInstallationObjectId = "yarr" @@ -171,6 +203,46 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b } } + func testOriginalDataNeverSavesToKeychain() async throws { + // Save current Installation + try saveCurrentInstallation() + MockURLProtocol.removeAll() + + Installation.current?.originalData = Data() + let original = Installation.current + Installation.saveCurrentContainerToKeychain() + + let expectation1 = XCTestExpectation(description: "Original installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let original = original, + let saved = Installation.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameInstallationId(as: original)) + XCTAssertTrue(saved.hasSameObjectId(as: original)) + XCTAssertNotNil(original.originalData) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.customKey, original.customKey) + XCTAssertEqual(saved.badge, original.badge) + XCTAssertEqual(saved.deviceType, original.deviceType) + XCTAssertEqual(saved.deviceToken, original.deviceToken) + XCTAssertEqual(saved.channels, original.channels) + XCTAssertEqual(saved.installationId, original.installationId) + XCTAssertEqual(saved.timeZone, original.timeZone) + XCTAssertEqual(saved.appName, original.appName) + XCTAssertEqual(saved.appVersion, original.appVersion) + XCTAssertEqual(saved.appIdentifier, original.appIdentifier) + XCTAssertEqual(saved.parseVersion, original.parseVersion) + XCTAssertEqual(saved.localeIdentifier, original.localeIdentifier) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, original.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + @MainActor func testFetch() async throws { try saveCurrentInstallation() @@ -386,6 +458,164 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b XCTAssertEqual(saved.updatedAt, serverResponse.updatedAt) } + func testUpdateMutableMergeCurrentInstallation() async throws { + // Save current Installation + try saveCurrentInstallation() + MockURLProtocol.removeAll() + + guard let original = Installation.current else { + XCTFail("Should unwrap") + return + } + var originalResponse = original.mergeable + originalResponse.createdAt = nil + originalResponse.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try originalResponse.getEncoder().encode(originalResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + originalResponse = try originalResponse.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let response = originalResponse + var originalUpdate = original.mergeable + originalUpdate.customKey = "hello" + originalUpdate.deviceToken = "1234" + let updated = originalUpdate + + do { + let saved = try await updated.update() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let newCurrentInstallation = Installation.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameInstallationId(as: newCurrentInstallation)) + XCTAssertTrue(saved.hasSameObjectId(as: newCurrentInstallation)) + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.customKey, updated.customKey) + XCTAssertEqual(saved.badge, original.badge) + XCTAssertEqual(saved.deviceType, original.deviceType) + XCTAssertEqual(saved.deviceToken, updated.deviceToken) + XCTAssertEqual(saved.channels, original.channels) + XCTAssertEqual(saved.installationId, original.installationId) + XCTAssertEqual(saved.timeZone, original.timeZone) + XCTAssertEqual(saved.appName, original.appName) + XCTAssertEqual(saved.appVersion, original.appVersion) + XCTAssertEqual(saved.appIdentifier, original.appIdentifier) + XCTAssertEqual(saved.parseVersion, original.parseVersion) + XCTAssertEqual(saved.localeIdentifier, original.localeIdentifier) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.customKey, newCurrentInstallation.customKey) + XCTAssertEqual(saved.badge, newCurrentInstallation.badge) + XCTAssertEqual(saved.deviceType, newCurrentInstallation.deviceType) + XCTAssertEqual(saved.deviceToken, newCurrentInstallation.deviceToken) + XCTAssertEqual(saved.channels, newCurrentInstallation.channels) + XCTAssertEqual(saved.installationId, newCurrentInstallation.installationId) + XCTAssertEqual(saved.timeZone, newCurrentInstallation.timeZone) + XCTAssertEqual(saved.appName, newCurrentInstallation.appName) + XCTAssertEqual(saved.appVersion, newCurrentInstallation.appVersion) + XCTAssertEqual(saved.appIdentifier, newCurrentInstallation.appIdentifier) + XCTAssertEqual(saved.parseVersion, newCurrentInstallation.parseVersion) + XCTAssertEqual(saved.localeIdentifier, newCurrentInstallation.localeIdentifier) + XCTAssertEqual(saved.createdAt, newCurrentInstallation.createdAt) + XCTAssertEqual(saved.updatedAt, newCurrentInstallation.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUpdateMutableMergeCurrentInstallationDefault() async throws { + // Save current Installation + try saveCurrentInstallation() + MockURLProtocol.removeAll() + + guard let original = InstallationDefault.current else { + XCTFail("Should unwrap") + return + } + var originalResponse = original.mergeable + originalResponse.createdAt = nil + originalResponse.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try originalResponse.getEncoder().encode(originalResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + originalResponse = try originalResponse.getDecoder().decode(InstallationDefault.self, + from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let response = originalResponse + var originalUpdate = original.mergeable + originalUpdate.deviceToken = "1234" + let updated = originalUpdate + + do { + let saved = try await updated.update() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let newCurrentInstallation = Installation.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameInstallationId(as: newCurrentInstallation)) + XCTAssertTrue(saved.hasSameObjectId(as: newCurrentInstallation)) + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.badge, original.badge) + XCTAssertEqual(saved.deviceType, original.deviceType) + XCTAssertEqual(saved.deviceToken, updated.deviceToken) + XCTAssertEqual(saved.channels, original.channels) + XCTAssertEqual(saved.installationId, original.installationId) + XCTAssertEqual(saved.timeZone, original.timeZone) + XCTAssertEqual(saved.appName, original.appName) + XCTAssertEqual(saved.appVersion, original.appVersion) + XCTAssertEqual(saved.appIdentifier, original.appIdentifier) + XCTAssertEqual(saved.parseVersion, original.parseVersion) + XCTAssertEqual(saved.localeIdentifier, original.localeIdentifier) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.badge, newCurrentInstallation.badge) + XCTAssertEqual(saved.deviceType, newCurrentInstallation.deviceType) + XCTAssertEqual(saved.deviceToken, newCurrentInstallation.deviceToken) + XCTAssertEqual(saved.channels, newCurrentInstallation.channels) + XCTAssertEqual(saved.installationId, newCurrentInstallation.installationId) + XCTAssertEqual(saved.timeZone, newCurrentInstallation.timeZone) + XCTAssertEqual(saved.appName, newCurrentInstallation.appName) + XCTAssertEqual(saved.appVersion, newCurrentInstallation.appVersion) + XCTAssertEqual(saved.appIdentifier, newCurrentInstallation.appIdentifier) + XCTAssertEqual(saved.parseVersion, newCurrentInstallation.parseVersion) + XCTAssertEqual(saved.localeIdentifier, newCurrentInstallation.localeIdentifier) + XCTAssertEqual(saved.createdAt, newCurrentInstallation.createdAt) + XCTAssertEqual(saved.updatedAt, newCurrentInstallation.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } catch { + XCTFail(error.localizedDescription) + } + } + @MainActor func testUpdateClientMissingObjectId() async throws { var installation = Installation() diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index 4a841826b..b4ff30e2a 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -22,6 +22,7 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -41,6 +42,7 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -81,6 +83,7 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? var customKey: String? } diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index 786d68c5d..8c70fb639 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -19,6 +19,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -78,7 +80,18 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? var customKey: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + return updated + } } let testInstallationObjectId = "yarr" @@ -252,6 +265,86 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } #endif + func testMerge() throws { + guard var original = Installation.current else { + XCTFail("Should have unwrapped") + return + } + original.objectId = "yolo" + original.createdAt = Date() + original.updatedAt = Date() + original.badge = 10 + var acl = ParseACL() + acl.publicRead = true + original.ACL = acl + + var updated = original.mergeable + updated.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + updated.badge = 1 + updated.deviceToken = "12345" + updated.customKey = "newKey" + let merged = try updated.merge(original) + XCTAssertEqual(merged.customKey, updated.customKey) + XCTAssertEqual(merged.badge, updated.badge) + XCTAssertEqual(merged.deviceType, original.deviceType) + XCTAssertEqual(merged.deviceToken, updated.deviceToken) + XCTAssertEqual(merged.channels, original.channels) + XCTAssertEqual(merged.installationId, original.installationId) + XCTAssertEqual(merged.timeZone, original.timeZone) + XCTAssertEqual(merged.appName, original.appName) + XCTAssertEqual(merged.appVersion, original.appVersion) + XCTAssertEqual(merged.appIdentifier, original.appIdentifier) + XCTAssertEqual(merged.parseVersion, original.parseVersion) + XCTAssertEqual(merged.localeIdentifier, original.localeIdentifier) + XCTAssertEqual(merged.ACL, original.ACL) + XCTAssertEqual(merged.createdAt, original.createdAt) + XCTAssertEqual(merged.updatedAt, updated.updatedAt) + } + + func testMerge2() throws { + guard var original = Installation.current else { + XCTFail("Should have unwrapped") + return + } + original.objectId = "yolo" + original.createdAt = Date() + original.updatedAt = Date() + original.badge = 10 + original.deviceToken = "bruh" + original.channels = ["halo"] + var acl = ParseACL() + acl.publicRead = true + original.ACL = acl + + var updated = original.mergeable + updated.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + updated.customKey = "newKey" + let merged = try updated.merge(original) + XCTAssertEqual(merged.customKey, updated.customKey) + XCTAssertEqual(merged.badge, original.badge) + XCTAssertEqual(merged.deviceType, original.deviceType) + XCTAssertEqual(merged.deviceToken, original.deviceToken) + XCTAssertEqual(merged.channels, original.channels) + XCTAssertEqual(merged.installationId, original.installationId) + XCTAssertEqual(merged.timeZone, original.timeZone) + XCTAssertEqual(merged.appName, original.appName) + XCTAssertEqual(merged.appVersion, original.appVersion) + XCTAssertEqual(merged.appIdentifier, original.appIdentifier) + XCTAssertEqual(merged.parseVersion, original.parseVersion) + XCTAssertEqual(merged.localeIdentifier, original.localeIdentifier) + XCTAssertEqual(merged.ACL, original.ACL) + XCTAssertEqual(merged.createdAt, original.createdAt) + XCTAssertEqual(merged.updatedAt, updated.updatedAt) + } + + func testMergeDifferentObjectId() throws { + var installation = Installation() + installation.objectId = "yolo" + var installation2 = installation + installation2.objectId = "nolo" + XCTAssertThrowsError(try installation2.merge(installation)) + } + func testUpdate() { var installation = Installation() installation.objectId = testInstallationObjectId @@ -384,6 +477,84 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } } + func testSaveMutableMergeCurrentInstallation() throws { + // Save current Installation + try testSaveCurrentInstallation() + MockURLProtocol.removeAll() + + guard let original = Installation.current else { + XCTFail("Should unwrap") + return + } + var response = original.mergeable + response.createdAt = nil + response.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try response.getEncoder().encode(response, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + response = try response.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + var updated = original.mergeable + updated.customKey = "hello" + updated.deviceToken = "1234" + + do { + let saved = try updated.save() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let newCurrentInstallation = Installation.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameInstallationId(as: newCurrentInstallation)) + XCTAssertTrue(saved.hasSameObjectId(as: newCurrentInstallation)) + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.customKey, updated.customKey) + XCTAssertEqual(saved.badge, original.badge) + XCTAssertEqual(saved.deviceType, original.deviceType) + XCTAssertEqual(saved.deviceToken, updated.deviceToken) + XCTAssertEqual(saved.channels, original.channels) + XCTAssertEqual(saved.installationId, original.installationId) + XCTAssertEqual(saved.timeZone, original.timeZone) + XCTAssertEqual(saved.appName, original.appName) + XCTAssertEqual(saved.appVersion, original.appVersion) + XCTAssertEqual(saved.appIdentifier, original.appIdentifier) + XCTAssertEqual(saved.parseVersion, original.parseVersion) + XCTAssertEqual(saved.localeIdentifier, original.localeIdentifier) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.customKey, newCurrentInstallation.customKey) + XCTAssertEqual(saved.badge, newCurrentInstallation.badge) + XCTAssertEqual(saved.deviceType, newCurrentInstallation.deviceType) + XCTAssertEqual(saved.deviceToken, newCurrentInstallation.deviceToken) + XCTAssertEqual(saved.channels, newCurrentInstallation.channels) + XCTAssertEqual(saved.installationId, newCurrentInstallation.installationId) + XCTAssertEqual(saved.timeZone, newCurrentInstallation.timeZone) + XCTAssertEqual(saved.appName, newCurrentInstallation.appName) + XCTAssertEqual(saved.appVersion, newCurrentInstallation.appVersion) + XCTAssertEqual(saved.appIdentifier, newCurrentInstallation.appIdentifier) + XCTAssertEqual(saved.parseVersion, newCurrentInstallation.parseVersion) + XCTAssertEqual(saved.localeIdentifier, newCurrentInstallation.localeIdentifier) + XCTAssertEqual(saved.createdAt, newCurrentInstallation.createdAt) + XCTAssertEqual(saved.updatedAt, newCurrentInstallation.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } catch { + XCTFail(error.localizedDescription) + } + } + func testSaveCurrentInstallationWithDefaultACL() throws { try userLogin() guard let userObjectId = User.current?.objectId else { diff --git a/Tests/ParseSwiftTests/ParseLDAPAsyncTests.swift b/Tests/ParseSwiftTests/ParseLDAPAsyncTests.swift index 7dbe12e42..d2ce5910d 100644 --- a/Tests/ParseSwiftTests/ParseLDAPAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseLDAPAsyncTests.swift @@ -22,6 +22,7 @@ class ParseLDAPAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseLDAPAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseLDAPCombineTests.swift b/Tests/ParseSwiftTests/ParseLDAPCombineTests.swift index 5cc79771b..fe28deb27 100644 --- a/Tests/ParseSwiftTests/ParseLDAPCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseLDAPCombineTests.swift @@ -22,6 +22,7 @@ class ParseLDAPCombineTests: XCTestCase { // swiftlint:disable:this type_body_le var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseLDAPCombineTests: XCTestCase { // swiftlint:disable:this type_body_le var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseLDAPTests.swift b/Tests/ParseSwiftTests/ParseLDAPTests.swift index 3bef5bb2d..a594ac426 100644 --- a/Tests/ParseSwiftTests/ParseLDAPTests.swift +++ b/Tests/ParseSwiftTests/ParseLDAPTests.swift @@ -18,6 +18,7 @@ class ParseLDAPTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -34,6 +35,7 @@ class ParseLDAPTests: XCTestCase { var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseLinkedInCombineTests.swift b/Tests/ParseSwiftTests/ParseLinkedInCombineTests.swift index a9996fd3b..5eb909168 100644 --- a/Tests/ParseSwiftTests/ParseLinkedInCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseLinkedInCombineTests.swift @@ -22,6 +22,7 @@ class ParseLinkedInCombineTests: XCTestCase { // swiftlint:disable:this type_bod var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseLinkedInCombineTests: XCTestCase { // swiftlint:disable:this type_bod var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseLinkedInTests.swift b/Tests/ParseSwiftTests/ParseLinkedInTests.swift index 91756ba2c..bd5d4adf3 100644 --- a/Tests/ParseSwiftTests/ParseLinkedInTests.swift +++ b/Tests/ParseSwiftTests/ParseLinkedInTests.swift @@ -21,6 +21,7 @@ class ParseLinkedInTests: XCTestCase { // swiftlint:disable:this type_body_lengt var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -37,6 +38,7 @@ class ParseLinkedInTests: XCTestCase { // swiftlint:disable:this type_body_lengt var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 49529dd8c..6253aeea3 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -17,6 +17,7 @@ class ParseLiveQueryTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int = 0 diff --git a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift index 85b7b28f6..ea3bfc59a 100644 --- a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift @@ -23,11 +23,26 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? var player: String? + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.player, + original: object) { + updated.player = object.player + } + return updated + } + init() { } //custom initializers @@ -44,6 +59,16 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le } } + struct GameScoreDefault: ParseObject { + + //: These are required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var originalData: Data? + } + override func setUpWithError() throws { try super.setUpWithError() guard let url = URL(string: "http://localhost:1337/1") else { @@ -144,6 +169,46 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(saved.ACL, scoreOnServer.ACL) } + @MainActor + func testSaveMutable() async throws { + var original = GameScore(points: 10) + original.objectId = "yarr" + original.player = "beast" + + var originalResponse = original.mergeable + originalResponse.createdAt = nil + originalResponse.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try originalResponse.getEncoder().encode(originalResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + originalResponse = try originalResponse.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let response = originalResponse + var originalUpdated = original.mergeable + originalUpdated.points = 50 + let updated = originalUpdated + + do { + let saved = try await updated.save() + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.points, 50) + XCTAssertEqual(saved.player, original.player) + XCTAssertEqual(saved.createdAt, response.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + } catch { + XCTFail(error.localizedDescription) + } + } + @MainActor func testCreate() async throws { let score = GameScore(points: 10) @@ -321,6 +386,82 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le } } + @MainActor + func testUpdateMutable() async throws { + var original = GameScore(points: 10) + original.objectId = "yarr" + original.player = "beast" + + var originalResponse = original.mergeable + originalResponse.createdAt = nil + originalResponse.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try originalResponse.getEncoder().encode(originalResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + originalResponse = try originalResponse.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let response = originalResponse + var originalUpdated = original.mergeable + originalUpdated.points = 50 + let updated = originalUpdated + + do { + let saved = try await updated.update() + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.points, 50) + XCTAssertEqual(saved.player, original.player) + XCTAssertEqual(saved.createdAt, response.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + } catch { + XCTFail(error.localizedDescription) + } + } + + @MainActor + func testUpdateMutableDefault() async throws { + var original = GameScoreDefault() + original.objectId = "yarr" + + var originalResponse = original.mergeable + originalResponse.createdAt = nil + originalResponse.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try originalResponse.getEncoder().encode(originalResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + originalResponse = try originalResponse.getDecoder().decode(GameScoreDefault.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let response = originalResponse + let originalUpdated = original.mergeable + let updated = originalUpdated + + do { + let saved = try await updated.update() + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.createdAt, response.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + } catch { + XCTFail(error.localizedDescription) + } + } + @MainActor func testDelete() async throws { var score = GameScore(points: 10) diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index 57514d1ab..1144573b9 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -18,6 +18,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // Custom properties var points: Int = 0 @@ -42,6 +43,7 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var name = "Hello" diff --git a/Tests/ParseSwiftTests/ParseObjectCombineTests.swift b/Tests/ParseSwiftTests/ParseObjectCombineTests.swift index 4040ca434..13d31f77a 100644 --- a/Tests/ParseSwiftTests/ParseObjectCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCombineTests.swift @@ -21,6 +21,7 @@ class ParseObjectCombineTests: XCTestCase { // swiftlint:disable:this type_body_ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift index c1e271030..af84d5c26 100644 --- a/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectIdTests.swift @@ -20,6 +20,8 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ var ACL: ParseACL? + var originalData: Data? + var name = "First" } @@ -29,6 +31,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? @@ -57,6 +60,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var gameScore: GameScore @@ -80,6 +84,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -108,6 +113,7 @@ class ParseObjectCustomObjectIdTests: XCTestCase { // swiftlint:disable:this typ var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? var customKey: String? } diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index fba215b33..4799ba063 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -22,17 +22,20 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var name: String? + var originalData: Data? + init() { name = "First" } } - struct GameScore: ParseObject, ParseObjectMutable { + struct GameScore: ParseObject { //: These are required by ParseObject var objectId: String? var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? @@ -41,7 +44,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var levels: [Level]? var nextLevel: Level? - //custom initializers + //: custom initializers init() {} init(objectId: String?) { @@ -55,6 +58,28 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length self.points = points self.player = name } + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.level, + original: object) { + updated.level = object.level + } + if updated.shouldRestoreKey(\.levels, + original: object) { + updated.levels = object.levels + } + if updated.shouldRestoreKey(\.nextLevel, + original: object) { + updated.nextLevel = object.nextLevel + } + return updated + } } struct Game: ParseObject { @@ -63,6 +88,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var gameScore: GameScore @@ -86,19 +112,21 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var name = "Hello" var profilePicture: ParseFile? } - class GameScoreClass: ParseObject { + final class GameScoreClass: ParseObject { //: These are required by ParseObject var objectId: String? var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int @@ -150,13 +178,14 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length } } - class GameClass: ParseObject { + final class GameClass: ParseObject { //: These are required by ParseObject var objectId: String? var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var gameScore: GameScoreClass @@ -212,6 +241,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -231,6 +261,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -321,15 +352,81 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(score.id, objectId) } + func testIsRestoreOriginalKey() throws { + let score1 = GameScore(points: 5) + var score2 = GameScore(points: 5, name: "world") + score2.levels = [Level()] + score2.nextLevel = Level() + XCTAssertFalse(score1.shouldRestoreKey(\.player, original: score2)) + XCTAssertTrue(score1.shouldRestoreKey(\.levels, original: score2)) + XCTAssertFalse(score1.shouldRestoreKey(\.points, original: score2)) + XCTAssertFalse(score1.shouldRestoreKey(\.level, original: score2)) + XCTAssertTrue(score1.shouldRestoreKey(\.nextLevel, original: score2)) + } + func testParseObjectMutable() throws { var score = GameScore(points: 19, name: "fire") score.objectId = "yolo" score.createdAt = Date() - let empty = score.mutable + let empty = score.mergeable XCTAssertTrue(score.hasSameObjectId(as: empty)) XCTAssertEqual(score.createdAt, empty.createdAt) } + func testMerge() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + score.createdAt = Date() + score.updatedAt = Date() + var acl = ParseACL() + acl.publicRead = true + score.ACL = acl + var level = Level() + level.objectId = "hello" + var level2 = Level() + level2.objectId = "world" + score.level = level + score.levels = [level] + score.nextLevel = level2 + var updated = score.mergeable + updated.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + updated.points = 30 + updated.player = "moreFire" + updated.levels = [level, level2] + let merged = try updated.merge(score) + XCTAssertEqual(merged.points, updated.points) + XCTAssertEqual(merged.player, updated.player) + XCTAssertEqual(merged.level, score.level) + XCTAssertEqual(merged.levels, updated.levels) + XCTAssertEqual(merged.nextLevel, score.nextLevel) + XCTAssertEqual(merged.ACL, score.ACL) + XCTAssertEqual(merged.createdAt, score.createdAt) + XCTAssertEqual(merged.updatedAt, updated.updatedAt) + } + + func testMergeDefaultImplementation() throws { + var score = Game() + score.objectId = "yolo" + score.createdAt = Date() + score.updatedAt = Date() + var updated = score.mergeable + updated.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + updated.name = "moreFire" + let merged = try updated.merge(score) + XCTAssertEqual(merged.name, updated.name) + XCTAssertEqual(merged.gameScore, score.gameScore) + XCTAssertEqual(merged.gameScores, score.gameScores) + XCTAssertEqual(merged.profilePicture, updated.profilePicture) + } + + func testMergeDifferentObjectId() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var score2 = score + score2.objectId = "nolo" + XCTAssertThrowsError(try score2.merge(score)) + } + func testFetchCommand() { var score = GameScore(points: 10) let className = score.className @@ -659,7 +756,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length score.createdAt = Date() score.updatedAt = score.createdAt - let command = try score.mutable.saveCommand() + let command = try score.mergeable.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) @@ -678,7 +775,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertEqual(decoded, expected) - var empty = score.mutable + var empty = score.mergeable empty.player = "Jennifer" let command2 = try empty.saveCommand() guard let body2 = command2.body else { diff --git a/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift b/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift index 7b73446aa..6840eb432 100644 --- a/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift @@ -21,6 +21,7 @@ class ParseOperationAsyncTests: XCTestCase { // swiftlint:disable:this type_body var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? diff --git a/Tests/ParseSwiftTests/ParseOperationCombineTests.swift b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift index e3e15f24b..2a1b30f8b 100644 --- a/Tests/ParseSwiftTests/ParseOperationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift @@ -21,6 +21,7 @@ class ParseOperationCombineTests: XCTestCase { // swiftlint:disable:this type_bo var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift index df5aafb82..fe146aafa 100644 --- a/Tests/ParseSwiftTests/ParseOperationTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -17,6 +17,7 @@ class ParseOperationTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? @@ -43,6 +44,7 @@ class ParseOperationTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var level: Int diff --git a/Tests/ParseSwiftTests/ParsePointerAsyncTests.swift b/Tests/ParseSwiftTests/ParsePointerAsyncTests.swift index 4af8cfb68..2d5765897 100644 --- a/Tests/ParseSwiftTests/ParsePointerAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerAsyncTests.swift @@ -22,6 +22,7 @@ class ParsePointerAsyncTests: XCTestCase { // swiftlint:disable:this type_body_l var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int diff --git a/Tests/ParseSwiftTests/ParsePointerCombineTests.swift b/Tests/ParseSwiftTests/ParsePointerCombineTests.swift index 8183ca7e3..339b0b6db 100644 --- a/Tests/ParseSwiftTests/ParsePointerCombineTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerCombineTests.swift @@ -21,6 +21,7 @@ class ParsePointerCombineTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int diff --git a/Tests/ParseSwiftTests/ParsePointerTests.swift b/Tests/ParseSwiftTests/ParsePointerTests.swift index 331fed93d..e389e1987 100644 --- a/Tests/ParseSwiftTests/ParsePointerTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerTests.swift @@ -18,6 +18,7 @@ class ParsePointerTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int diff --git a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift index 03bda3046..1a116a0bf 100644 --- a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift @@ -21,6 +21,7 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? diff --git a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift index a5d2edec1..7176f8ea7 100644 --- a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift @@ -21,6 +21,7 @@ class ParseQueryCombineTests: XCTestCase { // swiftlint:disable:this type_body_l var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int? diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 176f47422..3b415c17f 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -19,6 +19,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length var updatedAt: Date? var ACL: ParseACL? var score: Double? + var originalData: Data? //: Your own properties var points: Int @@ -39,6 +40,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? var points: Int? } diff --git a/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift b/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift index 673e0bfcb..b15bce6c4 100644 --- a/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift @@ -18,6 +18,7 @@ class ParseQueryViewModelTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int = 0 diff --git a/Tests/ParseSwiftTests/ParseRelationTests.swift b/Tests/ParseSwiftTests/ParseRelationTests.swift index 6f3e3e128..5dee388fd 100644 --- a/Tests/ParseSwiftTests/ParseRelationTests.swift +++ b/Tests/ParseSwiftTests/ParseRelationTests.swift @@ -17,6 +17,7 @@ class ParseRelationTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int @@ -38,6 +39,7 @@ class ParseRelationTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var level: Int diff --git a/Tests/ParseSwiftTests/ParseRoleTests.swift b/Tests/ParseSwiftTests/ParseRoleTests.swift index e13444e5a..e14961021 100644 --- a/Tests/ParseSwiftTests/ParseRoleTests.swift +++ b/Tests/ParseSwiftTests/ParseRoleTests.swift @@ -17,6 +17,7 @@ class ParseRoleTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var points: Int @@ -40,6 +41,7 @@ class ParseRoleTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -59,13 +61,10 @@ class ParseRoleTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // provided by Role - var name: String - - init() { - self.name = "roleMe" - } + var name: String? } struct Level: ParseObject { @@ -74,6 +73,7 @@ class ParseRoleTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? //: Your own properties var level: Int @@ -113,7 +113,7 @@ class ParseRoleTests: XCTestCase { func testName() throws { let role1 = try Role(name: "Hello9_- ") - let role2 = try Role(name: "Hello9_- ", acl: ParseACL()) + let role2 = try Role(name: "Hello10_- ", acl: ParseACL()) let roles = [role1: "hello", role2: "world"] XCTAssertEqual(role1, role1) @@ -121,7 +121,7 @@ class ParseRoleTests: XCTestCase { XCTAssertEqual(roles[role1], "hello") XCTAssertEqual(roles[role2], "world") XCTAssertThrowsError(try Role(name: "Hello9!")) - XCTAssertThrowsError(try Role(name: "Hello9!", acl: ParseACL())) + XCTAssertThrowsError(try Role(name: "Hello10!", acl: ParseACL())) } func testEndPoint() throws { diff --git a/Tests/ParseSwiftTests/ParseSessionTests.swift b/Tests/ParseSwiftTests/ParseSessionTests.swift index 74169389d..118b00c58 100644 --- a/Tests/ParseSwiftTests/ParseSessionTests.swift +++ b/Tests/ParseSwiftTests/ParseSessionTests.swift @@ -20,6 +20,7 @@ class ParseSessionTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -37,6 +38,7 @@ class ParseSessionTests: XCTestCase { var createdWith: [String: String] var installationId: String var expiresAt: Date + var originalData: Data? var objectId: String? var createdAt: Date? diff --git a/Tests/ParseSwiftTests/ParseTwitterAsyncTests.swift b/Tests/ParseSwiftTests/ParseTwitterAsyncTests.swift index 25f628138..80dc2d1d7 100644 --- a/Tests/ParseSwiftTests/ParseTwitterAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseTwitterAsyncTests.swift @@ -22,6 +22,7 @@ class ParseTwitterAsyncTests: XCTestCase { // swiftlint:disable:this type_body_l var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseTwitterAsyncTests: XCTestCase { // swiftlint:disable:this type_body_l var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseTwitterCombineTests.swift b/Tests/ParseSwiftTests/ParseTwitterCombineTests.swift index 3a958728a..8d66df2d6 100644 --- a/Tests/ParseSwiftTests/ParseTwitterCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseTwitterCombineTests.swift @@ -22,6 +22,7 @@ class ParseTwitterCombineTests: XCTestCase { // swiftlint:disable:this type_body var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -38,6 +39,7 @@ class ParseTwitterCombineTests: XCTestCase { // swiftlint:disable:this type_body var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseTwitterTests.swift b/Tests/ParseSwiftTests/ParseTwitterTests.swift index 3f2c3d151..b2e377e03 100644 --- a/Tests/ParseSwiftTests/ParseTwitterTests.swift +++ b/Tests/ParseSwiftTests/ParseTwitterTests.swift @@ -18,6 +18,7 @@ class ParseTwitterTests: XCTestCase { var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -34,6 +35,7 @@ class ParseTwitterTests: XCTestCase { var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseUserAsyncTests.swift b/Tests/ParseSwiftTests/ParseUserAsyncTests.swift index 7df6e3044..497a969b3 100644 --- a/Tests/ParseSwiftTests/ParseUserAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseUserAsyncTests.swift @@ -23,6 +23,7 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -33,6 +34,33 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng // Your custom keys var customKey: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + return updated + } + } + + struct UserDefault: ParseUser { + + //: These are required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var originalData: Data? + + // These are required by ParseUser + var username: String? + var email: String? + var emailVerified: Bool? + var password: String? + var authData: [String: [String: String]?]? } struct LoginSignupResponse: ParseUser { @@ -42,6 +70,7 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -91,6 +120,40 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng try ParseStorage.shared.deleteAll() } + func testOriginalDataNeverSavesToKeychain() async throws { + // Signup current User + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + User.current?.originalData = Data() + let original = User.current + User.saveCurrentContainerToKeychain() + + let expectation1 = XCTestExpectation(description: "Original installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let original = original, + let saved = User.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameObjectId(as: original)) + XCTAssertNotNil(original.originalData) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.customKey, original.customKey) + XCTAssertEqual(saved.email, original.email) + XCTAssertEqual(saved.username, original.username) + XCTAssertEqual(saved.emailVerified, original.emailVerified) + XCTAssertEqual(saved.password, original.password) + XCTAssertEqual(saved.authData, original.authData) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, original.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + @MainActor func testSignup() async throws { let loginResponse = LoginSignupResponse() @@ -707,6 +770,139 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng } } + func testUpdateMutableMergeCurrentUser() async throws { + // Signup current User + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let original = User.current else { + XCTFail("Should unwrap") + return + } + var originalResponse = original.mergeable + originalResponse.createdAt = nil + originalResponse.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try originalResponse.getEncoder().encode(originalResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + originalResponse = try originalResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let response = originalResponse + var originalUpdated = original.mergeable + originalUpdated.customKey = "beast" + originalUpdated.username = "mode" + let updated = originalUpdated + + do { + let saved = try await updated.update() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let newCurrentUser = User.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameObjectId(as: newCurrentUser)) + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.customKey, updated.customKey) + XCTAssertEqual(saved.email, original.email) + XCTAssertEqual(saved.username, updated.username) + XCTAssertEqual(saved.emailVerified, original.emailVerified) + XCTAssertEqual(saved.password, original.password) + XCTAssertEqual(saved.authData, original.authData) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.customKey, newCurrentUser.customKey) + XCTAssertEqual(saved.email, newCurrentUser.email) + XCTAssertEqual(saved.username, newCurrentUser.username) + XCTAssertEqual(saved.emailVerified, newCurrentUser.emailVerified) + XCTAssertEqual(saved.password, newCurrentUser.password) + XCTAssertEqual(saved.authData, newCurrentUser.authData) + XCTAssertEqual(saved.createdAt, newCurrentUser.createdAt) + XCTAssertEqual(saved.updatedAt, newCurrentUser.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUpdateMutableMergeCurrentUserDefault() async throws { + // Signup current User + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(UserDefault.current?.objectId) + + guard let original = UserDefault.current else { + XCTFail("Should unwrap") + return + } + var originalResponse = original.mergeable + originalResponse.createdAt = nil + originalResponse.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try originalResponse.getEncoder().encode(originalResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + originalResponse = try originalResponse.getDecoder().decode(UserDefault.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let response = originalResponse + var originalUpdated = original.mergeable + originalUpdated.username = "mode" + let updated = originalUpdated + + do { + let saved = try await updated.update() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let newCurrentUser = User.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameObjectId(as: newCurrentUser)) + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.email, original.email) + XCTAssertEqual(saved.username, updated.username) + XCTAssertEqual(saved.emailVerified, original.emailVerified) + XCTAssertEqual(saved.password, original.password) + XCTAssertEqual(saved.authData, original.authData) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.email, newCurrentUser.email) + XCTAssertEqual(saved.username, newCurrentUser.username) + XCTAssertEqual(saved.emailVerified, newCurrentUser.emailVerified) + XCTAssertEqual(saved.password, newCurrentUser.password) + XCTAssertEqual(saved.authData, newCurrentUser.authData) + XCTAssertEqual(saved.createdAt, newCurrentUser.createdAt) + XCTAssertEqual(saved.updatedAt, newCurrentUser.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } catch { + XCTFail(error.localizedDescription) + } + } + @MainActor func testDelete() async throws { login() diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 0183984fa..1a04b4d37 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -22,6 +22,7 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -41,6 +42,7 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 10111c2b6..ff83e3384 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -19,6 +19,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length var createdAt: Date? var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -29,6 +30,16 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length // Your custom keys var customKey: String? + + //: Implement your own version of merge + func merge(_ object: Self) throws -> Self { + var updated = try mergeParse(object) + if updated.shouldRestoreKey(\.customKey, + original: object) { + updated.customKey = object.customKey + } + return updated + } } struct LoginSignupResponse: ParseUser { @@ -38,6 +49,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length var sessionToken: String var updatedAt: Date? var ACL: ParseACL? + var originalData: Data? // These are required by ParseUser var username: String? @@ -98,6 +110,79 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length try ParseStorage.shared.deleteAll() } + func testMerge() throws { + // Signup current User + XCTAssertNil(User.current?.objectId) + try userSignUp() + XCTAssertNotNil(User.current?.objectId) + + guard var original = User.current else { + XCTFail("Should have unwrapped") + return + } + original.objectId = "yolo" + original.createdAt = Date() + original.updatedAt = Date() + original.authData = ["hello": ["world": "yolo"]] + var acl = ParseACL() + acl.publicRead = true + original.ACL = acl + + var updated = original.mergeable + updated.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + updated.email = "swift@parse.com" + updated.username = "12345" + updated.customKey = "newKey" + let merged = try updated.merge(original) + XCTAssertEqual(merged.customKey, updated.customKey) + XCTAssertEqual(merged.email, updated.email) + XCTAssertEqual(merged.emailVerified, original.emailVerified) + XCTAssertEqual(merged.username, updated.username) + XCTAssertEqual(merged.authData, original.authData) + XCTAssertEqual(merged.ACL, original.ACL) + XCTAssertEqual(merged.createdAt, original.createdAt) + XCTAssertEqual(merged.updatedAt, updated.updatedAt) + } + + func testMerge2() throws { + // Signup current User + XCTAssertNil(User.current?.objectId) + try userSignUp() + XCTAssertNotNil(User.current?.objectId) + + guard var original = User.current else { + XCTFail("Should have unwrapped") + return + } + original.objectId = "yolo" + original.createdAt = Date() + original.updatedAt = Date() + var acl = ParseACL() + acl.publicRead = true + original.ACL = acl + + var updated = original.mergeable + updated.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + updated.customKey = "newKey" + let merged = try updated.merge(original) + XCTAssertEqual(merged.customKey, updated.customKey) + XCTAssertEqual(merged.email, original.email) + XCTAssertEqual(merged.emailVerified, original.emailVerified) + XCTAssertEqual(merged.username, original.username) + XCTAssertEqual(merged.authData, original.authData) + XCTAssertEqual(merged.ACL, original.ACL) + XCTAssertEqual(merged.createdAt, original.createdAt) + XCTAssertEqual(merged.updatedAt, updated.updatedAt) + } + + func testMergeDifferentObjectId() throws { + var user = User() + user.objectId = "yolo" + var user2 = user + user2.objectId = "nolo" + XCTAssertThrowsError(try user2.merge(user)) + } + func testFetchCommand() { var user = User() XCTAssertThrowsError(try user.fetchCommand(include: nil)) @@ -743,6 +828,72 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testSaveMutableMergeCurrentUser() throws { + // Signup current User + XCTAssertNil(User.current?.objectId) + try userSignUp() + XCTAssertNotNil(User.current?.objectId) + + guard let original = User.current else { + XCTFail("Should unwrap") + return + } + var response = original.mergeable + response.createdAt = nil + response.updatedAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + let encoded: Data! + do { + encoded = try response.getEncoder().encode(response, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + response = try response.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + var updated = original.mergeable + updated.customKey = "beast" + updated.username = "mode" + + do { + let saved = try updated.save() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard let newCurrentUser = User.current else { + XCTFail("Should have a new current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(saved.hasSameObjectId(as: newCurrentUser)) + XCTAssertTrue(saved.hasSameObjectId(as: response)) + XCTAssertEqual(saved.customKey, updated.customKey) + XCTAssertEqual(saved.email, original.email) + XCTAssertEqual(saved.username, updated.username) + XCTAssertEqual(saved.emailVerified, original.emailVerified) + XCTAssertEqual(saved.password, original.password) + XCTAssertEqual(saved.authData, original.authData) + XCTAssertEqual(saved.createdAt, original.createdAt) + XCTAssertEqual(saved.updatedAt, response.updatedAt) + XCTAssertNil(saved.originalData) + XCTAssertEqual(saved.customKey, newCurrentUser.customKey) + XCTAssertEqual(saved.email, newCurrentUser.email) + XCTAssertEqual(saved.username, newCurrentUser.username) + XCTAssertEqual(saved.emailVerified, newCurrentUser.emailVerified) + XCTAssertEqual(saved.password, newCurrentUser.password) + XCTAssertEqual(saved.authData, newCurrentUser.authData) + XCTAssertEqual(saved.createdAt, newCurrentUser.createdAt) + XCTAssertEqual(saved.updatedAt, newCurrentUser.updatedAt) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } catch { + XCTFail(error.localizedDescription) + } + } + func testSaveAsyncAndUpdateCurrentUser() throws { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) try userSignUp()