From edf760614d5674b28b8cdb20290322e66b0ef38c Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 4 Sep 2022 11:20:17 -0400 Subject: [PATCH 1/3] feat: add revertKeyPath() and revertObject() methods to ParseObject --- CHANGELOG.md | 3 + Sources/ParseSwift/Objects/ParseObject.swift | 59 ++++++++++- Tests/ParseSwiftTests/ParseObjectTests.swift | 103 +++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caa0e2d80..17088b984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.3...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +__New features__ +- Add revertKeyPath() and revertObject() methods to ParseObject which allow developers to revert to original values of key paths or objects after mutating ParseObjects that were already have an objectId ([#402](https://github.com/parse-community/Parse-Swift/pull/402)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 4.9.3 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.2...4.9.3) diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 604f0b587..6be1805e0 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -83,6 +83,7 @@ public protocol ParseObject: ParseTypeable, /** Determines if a `KeyPath` of the current `ParseObject` should be restored by comparing it to another `ParseObject`. + - parameter key: The `KeyPath` to check. - parameter original: The original `ParseObject`. - returns: Returns a **true** if the keyPath should be restored or **false** otherwise. */ @@ -140,6 +141,23 @@ public protocol ParseObject: ParseTypeable, use `shouldRestoreKey` to compare key modifications between objects. */ func merge(with object: Self) throws -> Self + + /** + Reverts the `KeyPath` of the `ParseObject` back to the original `KeyPath` + before mutations began. + - throws: An error of type `ParseError`. + - important: This reverts to the contents in `originalData`. This means `originalData` should have + been populated by calling `mergeable` or some other means. + */ + mutating func revertKeyPath(_ keyPath: WritableKeyPath) throws where W: Equatable + + /** + Reverts the `ParseObject` back to the original object before mutations began. + - throws: An error of type `ParseError`. + - important: This reverts to the contents in `originalData`. This means `originalData` should have + been populated by calling `mergeable` or some other means. + */ + mutating func revertObject() throws } // MARK: Default Implementations @@ -198,7 +216,7 @@ public extension ParseObject { } var updated = self if shouldRestoreKey(\.ACL, - original: object) { + original: object) { updated.ACL = object.ACL } return updated @@ -207,6 +225,45 @@ public extension ParseObject { func merge(with object: Self) throws -> Self { return try mergeParse(with: object) } + + mutating func revertKeyPath(_ keyPath: WritableKeyPath) throws where W: Equatable { + guard let originalData = originalData else { + throw ParseError(code: .unknownError, + message: "Missing original data to revert to") + } + let original = try ParseCoding.jsonDecoder().decode(Self.self, + from: originalData) + guard hasSameObjectId(as: original) else { + throw ParseError(code: .unknownError, + message: "The current object does not have the same objectId as the original") + } + if shouldRevertKey(keyPath, + original: original) { + self[keyPath: keyPath] = original[keyPath: keyPath] + } + } + + mutating func revertObject() throws { + guard let originalData = originalData else { + throw ParseError(code: .unknownError, + message: "Missing original data to revert to") + } + let original = try ParseCoding.jsonDecoder().decode(Self.self, + from: originalData) + guard hasSameObjectId(as: original) else { + throw ParseError(code: .unknownError, + message: "The current object does not have the same objectId as the original") + } + self = original + } +} + +// MARK: Default Implementations (Internal) +extension ParseObject { + func shouldRevertKey(_ key: KeyPath, + original: Self) -> Bool where W: Equatable { + original[keyPath: key] != self[keyPath: key] + } } // MARK: Batch Support diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 663c25373..1abc56fc8 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -427,6 +427,109 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertThrowsError(try score2.merge(with: score)) } + func testRevertObject() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var mutableScore = score.mergeable + mutableScore.points = 50 + mutableScore.player = "ali" + XCTAssertNotEqual(mutableScore, score) + try mutableScore.revertObject() + XCTAssertEqual(mutableScore, score) + } + + func testRevertObjectMissingOriginal() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var mutableScore = score + mutableScore.points = 50 + mutableScore.player = "ali" + XCTAssertNotEqual(mutableScore, score) + do { + try mutableScore.revertObject() + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Missing original")) + } + } + + func testRevertObjectDiffObjectId() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var mutableScore = score.mergeable + mutableScore.points = 50 + mutableScore.player = "ali" + mutableScore.objectId = "nolo" + XCTAssertNotEqual(mutableScore, score) + do { + try mutableScore.revertObject() + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("objectId as the original")) + } + } + + func testRevertKeyPath() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var mutableScore = score.mergeable + mutableScore.points = 50 + mutableScore.player = "ali" + XCTAssertNotEqual(mutableScore, score) + try mutableScore.revertKeyPath(\.player) + XCTAssertNotEqual(mutableScore, score) + XCTAssertEqual(mutableScore.objectId, score.objectId) + XCTAssertNotEqual(mutableScore.points, score.points) + XCTAssertEqual(mutableScore.player, score.player) + } + + func testRevertKeyPathMissingOriginal() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var mutableScore = score + mutableScore.points = 50 + mutableScore.player = "ali" + XCTAssertNotEqual(mutableScore, score) + do { + try mutableScore.revertKeyPath(\.player) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Missing original")) + } + } + + func testRevertKeyPathDiffObjectId() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var mutableScore = score.mergeable + mutableScore.points = 50 + mutableScore.player = "ali" + mutableScore.objectId = "nolo" + XCTAssertNotEqual(mutableScore, score) + do { + try mutableScore.revertKeyPath(\.player) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("objectId as the original")) + } + } + func testFetchCommand() { var score = GameScore(points: 10) let className = score.className From 60ae9fdcc2bb5b75b9b980ef5a4b3a8260c0b1cd Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 4 Sep 2022 11:47:13 -0400 Subject: [PATCH 2/3] add tests for nil --- Tests/ParseSwiftTests/ParseObjectTests.swift | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 1abc56fc8..2bd28440e 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -491,6 +491,35 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(mutableScore.player, score.player) } + func testRevertKeyPathUpdatedNil() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + var mutableScore = score.mergeable + mutableScore.points = 50 + mutableScore.player = nil + XCTAssertNotEqual(mutableScore, score) + try mutableScore.revertKeyPath(\.player) + XCTAssertNotEqual(mutableScore, score) + XCTAssertEqual(mutableScore.objectId, score.objectId) + XCTAssertNotEqual(mutableScore.points, score.points) + XCTAssertEqual(mutableScore.player, score.player) + } + + func testRevertKeyPathOriginalNil() throws { + var score = GameScore(points: 19, name: "fire") + score.objectId = "yolo" + score.player = nil + var mutableScore = score.mergeable + mutableScore.points = 50 + mutableScore.player = "ali" + XCTAssertNotEqual(mutableScore, score) + try mutableScore.revertKeyPath(\.player) + XCTAssertNotEqual(mutableScore, score) + XCTAssertEqual(mutableScore.objectId, score.objectId) + XCTAssertNotEqual(mutableScore.points, score.points) + XCTAssertEqual(mutableScore.player, score.player) + } + func testRevertKeyPathMissingOriginal() throws { var score = GameScore(points: 19, name: "fire") score.objectId = "yolo" From b9552e1297e603adcbf3e72ab39b847841b5abcc Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 4 Sep 2022 11:56:48 -0400 Subject: [PATCH 3/3] nit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17088b984..5b0b3b759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * _Contributing to this repo? Add info about your change here to be included in the next release_ __New features__ -- Add revertKeyPath() and revertObject() methods to ParseObject which allow developers to revert to original values of key paths or objects after mutating ParseObjects that were already have an objectId ([#402](https://github.com/parse-community/Parse-Swift/pull/402)), thanks to [Corey Baker](https://github.com/cbaker6). +- Add revertKeyPath() and revertObject() methods to ParseObject which allow developers to revert to original values of key paths or objects after mutating ParseObjects that already have an objectId ([#402](https://github.com/parse-community/Parse-Swift/pull/402)), thanks to [Corey Baker](https://github.com/cbaker6). ### 4.9.3 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.2...4.9.3)