Skip to content
81 changes: 70 additions & 11 deletions Sources/ParseSwift/Operations/ParseOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,53 @@ public struct ParseOperation<T>: 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<W>(_ key: (String, WritableKeyPath<T, W>),
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<W>(_ key: (String, WritableKeyPath<T, W>),
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
Expand All @@ -43,6 +85,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- Parameters:
- key: The key of the object.
- objects: The field of objects.
- returns: The updated operations.
*/
public func addUnique<W>(_ key: String, objects: [W]) -> Self where W: Encodable, W: Hashable {
var mutableOperation = self
Expand All @@ -54,8 +97,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
Expand All @@ -74,8 +118,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
Expand All @@ -95,6 +140,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- Parameters:
- key: The key of the object.
- objects: The field of objects.
- returns: The updated operations.
*/
public func add<W>(_ key: String, objects: [W]) -> Self where W: Encodable {
var mutableOperation = self
Expand All @@ -105,8 +151,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: Encodable {
Expand All @@ -124,8 +171,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: Encodable {
Expand All @@ -145,6 +193,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- Parameters:
- key: The key of the object.
- objects: The field of objects.
- returns: The updated operations.
*/
public func addRelation<W>(_ key: String, objects: [W]) throws -> Self where W: ParseObject {
var mutableOperation = self
Expand All @@ -155,8 +204,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: ParseObject {
Expand All @@ -174,8 +224,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: ParseObject {
Expand All @@ -196,6 +247,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- Parameters:
- key: The key of the object.
- objects: The field of objects.
- returns: The updated operations.
*/
public func remove<W>(_ key: String, objects: [W]) -> Self where W: Encodable {
var mutableOperation = self
Expand All @@ -207,8 +259,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
Expand All @@ -230,8 +283,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: Encodable, V: Hashable {
Expand All @@ -255,6 +309,7 @@ public struct ParseOperation<T>: Savable where T: ParseObject {
- Parameters:
- key: The key of the object.
- objects: The field of objects.
- returns: The updated operations.
*/
public func removeRelation<W>(_ key: String, objects: [W]) throws -> Self where W: ParseObject {
var mutableOperation = self
Expand All @@ -266,8 +321,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]>),
objects: [V]) throws -> Self where V: ParseObject {
Expand All @@ -289,8 +345,9 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, [V]?>),
objects: [V]) throws -> Self where V: ParseObject {
Expand All @@ -311,6 +368,7 @@ public struct ParseOperation<T>: 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
Expand All @@ -321,7 +379,8 @@ public struct ParseOperation<T>: 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<V>(_ key: (String, WritableKeyPath<T, V?>)) -> Self where V: Encodable {
var mutableOperation = self
Expand Down
141 changes: 141 additions & 0 deletions Tests/ParseSwiftTests/ParseOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -394,6 +491,50 @@ class ParseOperationTests: XCTestCase {
}
#endif

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)
}

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
Expand Down