Skip to content

Commit

Permalink
feat: add revertKeyPath() and revertObject() methods to ParseObject (#…
Browse files Browse the repository at this point in the history
…402)

* feat: add revertKeyPath() and revertObject() methods to ParseObject

* add tests for nil

* nit
  • Loading branch information
cbaker6 authored Sep 4, 2022
1 parent 106f97d commit 9dac227
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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)

Expand Down
59 changes: 58 additions & 1 deletion Sources/ParseSwift/Objects/ParseObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<W>(_ keyPath: WritableKeyPath<Self, W?>) 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
Expand Down Expand Up @@ -198,7 +216,7 @@ public extension ParseObject {
}
var updated = self
if shouldRestoreKey(\.ACL,
original: object) {
original: object) {
updated.ACL = object.ACL
}
return updated
Expand All @@ -207,6 +225,45 @@ public extension ParseObject {
func merge(with object: Self) throws -> Self {
return try mergeParse(with: object)
}

mutating func revertKeyPath<W>(_ keyPath: WritableKeyPath<Self, W?>) 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<W>(_ key: KeyPath<Self, W?>,
original: Self) -> Bool where W: Equatable {
original[keyPath: key] != self[keyPath: key]
}
}

// MARK: Batch Support
Expand Down
132 changes: 132 additions & 0 deletions Tests/ParseSwiftTests/ParseObjectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,138 @@ 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 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"
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
Expand Down

0 comments on commit 9dac227

Please sign in to comment.