Skip to content

Commit 9dac227

Browse files
authored
feat: add revertKeyPath() and revertObject() methods to ParseObject (#402)
* feat: add revertKeyPath() and revertObject() methods to ParseObject * add tests for nil * nit
1 parent 106f97d commit 9dac227

File tree

3 files changed

+193
-1
lines changed

3 files changed

+193
-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 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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,138 @@ 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 testRevertKeyPathUpdatedNil() throws {
495+
var score = GameScore(points: 19, name: "fire")
496+
score.objectId = "yolo"
497+
var mutableScore = score.mergeable
498+
mutableScore.points = 50
499+
mutableScore.player = nil
500+
XCTAssertNotEqual(mutableScore, score)
501+
try mutableScore.revertKeyPath(\.player)
502+
XCTAssertNotEqual(mutableScore, score)
503+
XCTAssertEqual(mutableScore.objectId, score.objectId)
504+
XCTAssertNotEqual(mutableScore.points, score.points)
505+
XCTAssertEqual(mutableScore.player, score.player)
506+
}
507+
508+
func testRevertKeyPathOriginalNil() throws {
509+
var score = GameScore(points: 19, name: "fire")
510+
score.objectId = "yolo"
511+
score.player = nil
512+
var mutableScore = score.mergeable
513+
mutableScore.points = 50
514+
mutableScore.player = "ali"
515+
XCTAssertNotEqual(mutableScore, score)
516+
try mutableScore.revertKeyPath(\.player)
517+
XCTAssertNotEqual(mutableScore, score)
518+
XCTAssertEqual(mutableScore.objectId, score.objectId)
519+
XCTAssertNotEqual(mutableScore.points, score.points)
520+
XCTAssertEqual(mutableScore.player, score.player)
521+
}
522+
523+
func testRevertKeyPathMissingOriginal() throws {
524+
var score = GameScore(points: 19, name: "fire")
525+
score.objectId = "yolo"
526+
var mutableScore = score
527+
mutableScore.points = 50
528+
mutableScore.player = "ali"
529+
XCTAssertNotEqual(mutableScore, score)
530+
do {
531+
try mutableScore.revertKeyPath(\.player)
532+
XCTFail("Should have thrown error")
533+
} catch {
534+
guard let parseError = error as? ParseError else {
535+
XCTFail("Should have casted")
536+
return
537+
}
538+
XCTAssertTrue(parseError.message.contains("Missing original"))
539+
}
540+
}
541+
542+
func testRevertKeyPathDiffObjectId() throws {
543+
var score = GameScore(points: 19, name: "fire")
544+
score.objectId = "yolo"
545+
var mutableScore = score.mergeable
546+
mutableScore.points = 50
547+
mutableScore.player = "ali"
548+
mutableScore.objectId = "nolo"
549+
XCTAssertNotEqual(mutableScore, score)
550+
do {
551+
try mutableScore.revertKeyPath(\.player)
552+
XCTFail("Should have thrown error")
553+
} catch {
554+
guard let parseError = error as? ParseError else {
555+
XCTFail("Should have casted")
556+
return
557+
}
558+
XCTAssertTrue(parseError.message.contains("objectId as the original"))
559+
}
560+
}
561+
430562
func testFetchCommand() {
431563
var score = GameScore(points: 10)
432564
let className = score.className

0 commit comments

Comments
 (0)