diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5313464..3dcedc664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.10.4...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.11.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 1.11.0 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.10.4...1.11.0) + +__Improvements__ +- Added `operation` for `set` and `forceSet`, used for single key updates ([#248](https://github.com/parse-community/Parse-Swift/pull/248)), thanks to [Daniel Blyth](https://github.com/dblythy) and [Corey Baker](https://github.com/cbaker6). + ### 1.10.4 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.10.3...1.10.4) diff --git a/Sources/ParseSwift/Operations/ParseOperation.swift b/Sources/ParseSwift/Operations/ParseOperation.swift index 643cc0ca7..659ed042a 100644 --- a/Sources/ParseSwift/Operations/ParseOperation.swift +++ b/Sources/ParseSwift/Operations/ParseOperation.swift @@ -25,11 +25,53 @@ public struct ParseOperation: Savable where T: ParseObject { self.target = target } + /** + An operation that sets a field's value if it has changed from its previous value. + - Parameters: + - key: A tuple consisting of the key and the respective KeyPath of the object. + - value: The value to set it to. + - returns: The updated operations. + */ + public func set(_ key: (String, WritableKeyPath), + value: W) throws -> Self where W: Encodable { + var mutableOperation = self + guard let target = self.target else { + throw ParseError(code: .unknownError, message: "Target shouldn't be nil") + } + if let currentValue = target[keyPath: key.1] as? NSObject, + let updatedValue = value as? NSObject { + if currentValue != updatedValue { + mutableOperation.operations[key.0] = value + mutableOperation.target?[keyPath: key.1] = value + } + } else { + mutableOperation.operations[key.0] = value + mutableOperation.target?[keyPath: key.1] = value + } + return mutableOperation + } + + /** + An operation that force sets a field's value. + - Parameters: + - key: A tuple consisting of the key and the respective KeyPath of the object. + - value: The value to set it to. + - returns: The updated operations. + */ + public func forceSet(_ key: (String, WritableKeyPath), + value: W) throws -> Self where W: Encodable { + var mutableOperation = self + mutableOperation.operations[key.0] = value + mutableOperation.target?[keyPath: key.1] = value + return mutableOperation + } + /** An operation that increases a numeric field's value by a given amount. - Parameters: - key: The key of the object. - amount: How much to increment by. + - returns: The updated operations. */ public func increment(_ key: String, by amount: Int) -> Self { var mutableOperation = self @@ -43,6 +85,7 @@ public struct ParseOperation: Savable where T: ParseObject { - Parameters: - key: The key of the object. - objects: The field of objects. + - returns: The updated operations. */ public func addUnique(_ key: String, objects: [W]) -> Self where W: Encodable, W: Hashable { var mutableOperation = self @@ -54,8 +97,9 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that adds a new element to an array field, only if it wasn't already present. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func addUnique(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { @@ -74,8 +118,9 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that adds a new element to an array field, only if it wasn't already present. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func addUnique(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { @@ -95,6 +140,7 @@ public struct ParseOperation: Savable where T: ParseObject { - Parameters: - key: The key of the object. - objects: The field of objects. + - returns: The updated operations. */ public func add(_ key: String, objects: [W]) -> Self where W: Encodable { var mutableOperation = self @@ -105,8 +151,9 @@ public struct ParseOperation: Savable where T: ParseObject { /** An operation that adds a new element to an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func add(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable { @@ -124,8 +171,9 @@ public struct ParseOperation: Savable where T: ParseObject { /** An operation that adds a new element to an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func add(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable { @@ -145,6 +193,7 @@ public struct ParseOperation: Savable where T: ParseObject { - Parameters: - key: The key of the object. - objects: The field of objects. + - returns: The updated operations. */ public func addRelation(_ key: String, objects: [W]) throws -> Self where W: ParseObject { var mutableOperation = self @@ -155,8 +204,9 @@ public struct ParseOperation: Savable where T: ParseObject { /** An operation that adds a new relation to an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func addRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { @@ -174,8 +224,9 @@ public struct ParseOperation: Savable where T: ParseObject { /** An operation that adds a new relation to an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func addRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { @@ -196,6 +247,7 @@ public struct ParseOperation: Savable where T: ParseObject { - Parameters: - key: The key of the object. - objects: The field of objects. + - returns: The updated operations. */ public func remove(_ key: String, objects: [W]) -> Self where W: Encodable { var mutableOperation = self @@ -207,8 +259,9 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that removes every instance of an element from an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func remove(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { @@ -230,8 +283,9 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that removes every instance of an element from an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func remove(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: Encodable, V: Hashable { @@ -255,6 +309,7 @@ public struct ParseOperation: Savable where T: ParseObject { - Parameters: - key: The key of the object. - objects: The field of objects. + - returns: The updated operations. */ public func removeRelation(_ key: String, objects: [W]) throws -> Self where W: ParseObject { var mutableOperation = self @@ -266,8 +321,9 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that removes every instance of a relation from an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func removeRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { @@ -289,8 +345,9 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that removes every instance of a relation from an array field. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. - objects: The field of objects. + - returns: The updated operations. */ public func removeRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { @@ -311,6 +368,7 @@ public struct ParseOperation: Savable where T: ParseObject { /** An operation where a field is deleted from the object. - parameter key: The key of the object. + - returns: The updated operations. */ public func unset(_ key: String) -> Self { var mutableOperation = self @@ -321,7 +379,8 @@ public struct ParseOperation: Savable where T: ParseObject { /** An operation where a field is deleted from the object. - Parameters: - - key: A tuple consisting of the key and KeyPath of the object. + - key: A tuple consisting of the key and the respective KeyPath of the object. + - returns: The updated operations. */ public func unset(_ key: (String, WritableKeyPath)) -> Self where V: Encodable { var mutableOperation = self diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 81e6f99eb..d9327a9dd 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 = "1.10.4" + static let version = "1.11.0" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift index f4fb13cfd..4103a9717 100644 --- a/Tests/ParseSwiftTests/ParseOperationTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -139,6 +139,7 @@ class ParseOperationTests: XCTestCase { } XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) XCTAssertEqual(saved.ACL, scoreOnServer.ACL) + XCTAssertEqual(saved.score+1, scoreOnServer.score) } catch { XCTFail(error.localizedDescription) } @@ -188,6 +189,102 @@ class ParseOperationTests: XCTestCase { } XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) XCTAssertEqual(saved.ACL, scoreOnServer.ACL) + XCTAssertEqual(saved.score+1, scoreOnServer.score) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveSet() throws { // swiftlint:disable:this function_body_length + var score = GameScore(score: 10) + score.objectId = "yarr" + let operations = try score.operation + .set(("score", \.score), value: 15) + + var scoreOnServer = score + scoreOnServer.score = 15 + scoreOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.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) + } + do { + let saved = try operations.save() + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(saved.ACL, scoreOnServer.ACL) + XCTAssertEqual(saved.score, scoreOnServer.score) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveSetAsyncMainQueue() throws { + var score = GameScore(score: 10) + score.objectId = "yarr" + let operations = try score.operation + .set(("score", \.score), value: 15) + + var scoreOnServer = score + scoreOnServer.score = 15 + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let expectation1 = XCTestExpectation(description: "Save object1") + + operations.save(options: [], callbackQueue: .main) { result in + + switch result { + + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(saved.ACL, scoreOnServer.ACL) + XCTAssertEqual(saved.score, scoreOnServer.score) case .failure(let error): XCTFail(error.localizedDescription) } @@ -392,8 +489,52 @@ class ParseOperationTests: XCTestCase { let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertEqual(decoded, expected) } + + func testSet() throws { + let score = GameScore(score: 10) + let operations = try score.operation.set(("score", \.score), value: 15) + .set(("levels", \.levels), value: ["hello"]) + let expected = "{\"score\":15,\"levels\":[\"hello\"]}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + XCTAssertEqual(operations.target?.score, 15) + } #endif + func testObjectIdSet() throws { + var score = GameScore() + score.objectId = "test" + score.levels = nil + let operations = try score.operation.set(("objectId", \.objectId), value: "test") + let expected = "{}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testUnchangedSet() throws { + let score = GameScore(score: 10) + let operations = try score.operation.set(("score", \.score), value: 10) + let expected = "{}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testForceSet() throws { + let score = GameScore(score: 10) + let operations = try score.operation.forceSet(("score", \.score), value: 10) + let expected = "{\"score\":10}" + let encoded = try ParseCoding.parseEncoder() + .encode(operations) + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + func testUnset() throws { let score = GameScore(score: 10) let operations = score.operation