Skip to content

Commit edf7606

Browse files
committed
feat: add revertKeyPath() and revertObject() methods to ParseObject
1 parent 106f97d commit edf7606

File tree

3 files changed

+164
-1
lines changed

3 files changed

+164
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.3...main)
55
* _Contributing to this repo? Add info about your change here to be included in the next release_
66

7+
__New features__
8+
- 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).
9+
710
### 4.9.3
811
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.2...4.9.3)
912

Sources/ParseSwift/Objects/ParseObject.swift

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public protocol ParseObject: ParseTypeable,
8383
/**
8484
Determines if a `KeyPath` of the current `ParseObject` should be restored
8585
by comparing it to another `ParseObject`.
86+
- parameter key: The `KeyPath` to check.
8687
- parameter original: The original `ParseObject`.
8788
- returns: Returns a **true** if the keyPath should be restored or **false** otherwise.
8889
*/
@@ -140,6 +141,23 @@ public protocol ParseObject: ParseTypeable,
140141
use `shouldRestoreKey` to compare key modifications between objects.
141142
*/
142143
func merge(with object: Self) throws -> Self
144+
145+
/**
146+
Reverts the `KeyPath` of the `ParseObject` back to the original `KeyPath`
147+
before mutations began.
148+
- throws: An error of type `ParseError`.
149+
- important: This reverts to the contents in `originalData`. This means `originalData` should have
150+
been populated by calling `mergeable` or some other means.
151+
*/
152+
mutating func revertKeyPath<W>(_ keyPath: WritableKeyPath<Self, W?>) throws where W: Equatable
153+
154+
/**
155+
Reverts the `ParseObject` back to the original object before mutations began.
156+
- throws: An error of type `ParseError`.
157+
- important: This reverts to the contents in `originalData`. This means `originalData` should have
158+
been populated by calling `mergeable` or some other means.
159+
*/
160+
mutating func revertObject() throws
143161
}
144162

145163
// MARK: Default Implementations
@@ -198,7 +216,7 @@ public extension ParseObject {
198216
}
199217
var updated = self
200218
if shouldRestoreKey(\.ACL,
201-
original: object) {
219+
original: object) {
202220
updated.ACL = object.ACL
203221
}
204222
return updated
@@ -207,6 +225,45 @@ public extension ParseObject {
207225
func merge(with object: Self) throws -> Self {
208226
return try mergeParse(with: object)
209227
}
228+
229+
mutating func revertKeyPath<W>(_ keyPath: WritableKeyPath<Self, W?>) throws where W: Equatable {
230+
guard let originalData = originalData else {
231+
throw ParseError(code: .unknownError,
232+
message: "Missing original data to revert to")
233+
}
234+
let original = try ParseCoding.jsonDecoder().decode(Self.self,
235+
from: originalData)
236+
guard hasSameObjectId(as: original) else {
237+
throw ParseError(code: .unknownError,
238+
message: "The current object does not have the same objectId as the original")
239+
}
240+
if shouldRevertKey(keyPath,
241+
original: original) {
242+
self[keyPath: keyPath] = original[keyPath: keyPath]
243+
}
244+
}
245+
246+
mutating func revertObject() throws {
247+
guard let originalData = originalData else {
248+
throw ParseError(code: .unknownError,
249+
message: "Missing original data to revert to")
250+
}
251+
let original = try ParseCoding.jsonDecoder().decode(Self.self,
252+
from: originalData)
253+
guard hasSameObjectId(as: original) else {
254+
throw ParseError(code: .unknownError,
255+
message: "The current object does not have the same objectId as the original")
256+
}
257+
self = original
258+
}
259+
}
260+
261+
// MARK: Default Implementations (Internal)
262+
extension ParseObject {
263+
func shouldRevertKey<W>(_ key: KeyPath<Self, W?>,
264+
original: Self) -> Bool where W: Equatable {
265+
original[keyPath: key] != self[keyPath: key]
266+
}
210267
}
211268

212269
// MARK: Batch Support

Tests/ParseSwiftTests/ParseObjectTests.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,109 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length
427427
XCTAssertThrowsError(try score2.merge(with: score))
428428
}
429429

430+
func testRevertObject() throws {
431+
var score = GameScore(points: 19, name: "fire")
432+
score.objectId = "yolo"
433+
var mutableScore = score.mergeable
434+
mutableScore.points = 50
435+
mutableScore.player = "ali"
436+
XCTAssertNotEqual(mutableScore, score)
437+
try mutableScore.revertObject()
438+
XCTAssertEqual(mutableScore, score)
439+
}
440+
441+
func testRevertObjectMissingOriginal() throws {
442+
var score = GameScore(points: 19, name: "fire")
443+
score.objectId = "yolo"
444+
var mutableScore = score
445+
mutableScore.points = 50
446+
mutableScore.player = "ali"
447+
XCTAssertNotEqual(mutableScore, score)
448+
do {
449+
try mutableScore.revertObject()
450+
XCTFail("Should have thrown error")
451+
} catch {
452+
guard let parseError = error as? ParseError else {
453+
XCTFail("Should have casted")
454+
return
455+
}
456+
XCTAssertTrue(parseError.message.contains("Missing original"))
457+
}
458+
}
459+
460+
func testRevertObjectDiffObjectId() throws {
461+
var score = GameScore(points: 19, name: "fire")
462+
score.objectId = "yolo"
463+
var mutableScore = score.mergeable
464+
mutableScore.points = 50
465+
mutableScore.player = "ali"
466+
mutableScore.objectId = "nolo"
467+
XCTAssertNotEqual(mutableScore, score)
468+
do {
469+
try mutableScore.revertObject()
470+
XCTFail("Should have thrown error")
471+
} catch {
472+
guard let parseError = error as? ParseError else {
473+
XCTFail("Should have casted")
474+
return
475+
}
476+
XCTAssertTrue(parseError.message.contains("objectId as the original"))
477+
}
478+
}
479+
480+
func testRevertKeyPath() throws {
481+
var score = GameScore(points: 19, name: "fire")
482+
score.objectId = "yolo"
483+
var mutableScore = score.mergeable
484+
mutableScore.points = 50
485+
mutableScore.player = "ali"
486+
XCTAssertNotEqual(mutableScore, score)
487+
try mutableScore.revertKeyPath(\.player)
488+
XCTAssertNotEqual(mutableScore, score)
489+
XCTAssertEqual(mutableScore.objectId, score.objectId)
490+
XCTAssertNotEqual(mutableScore.points, score.points)
491+
XCTAssertEqual(mutableScore.player, score.player)
492+
}
493+
494+
func testRevertKeyPathMissingOriginal() throws {
495+
var score = GameScore(points: 19, name: "fire")
496+
score.objectId = "yolo"
497+
var mutableScore = score
498+
mutableScore.points = 50
499+
mutableScore.player = "ali"
500+
XCTAssertNotEqual(mutableScore, score)
501+
do {
502+
try mutableScore.revertKeyPath(\.player)
503+
XCTFail("Should have thrown error")
504+
} catch {
505+
guard let parseError = error as? ParseError else {
506+
XCTFail("Should have casted")
507+
return
508+
}
509+
XCTAssertTrue(parseError.message.contains("Missing original"))
510+
}
511+
}
512+
513+
func testRevertKeyPathDiffObjectId() throws {
514+
var score = GameScore(points: 19, name: "fire")
515+
score.objectId = "yolo"
516+
var mutableScore = score.mergeable
517+
mutableScore.points = 50
518+
mutableScore.player = "ali"
519+
mutableScore.objectId = "nolo"
520+
XCTAssertNotEqual(mutableScore, score)
521+
do {
522+
try mutableScore.revertKeyPath(\.player)
523+
XCTFail("Should have thrown error")
524+
} catch {
525+
guard let parseError = error as? ParseError else {
526+
XCTFail("Should have casted")
527+
return
528+
}
529+
XCTAssertTrue(parseError.message.contains("objectId as the original"))
530+
}
531+
}
532+
430533
func testFetchCommand() {
431534
var score = GameScore(points: 10)
432535
let className = score.className

0 commit comments

Comments
 (0)