From d94dcad40c6fa4c59049307124506d8ee260087a Mon Sep 17 00:00:00 2001 From: Corey Date: Sun, 4 Sep 2022 23:47:56 -0400 Subject: [PATCH] feat: add set operation based on KeyPath (#403) * wip * switch to using chainable for reverts * add file * feat: add set operation based on KeyPath * add tests and refactor general errors * add back tests for deprecated methods * add get() method * reduce codecov patch --- .codecov.yml | 2 +- CHANGELOG.md | 6 +- ParseSwift.xcodeproj/project.pbxproj | 10 + .../Protocols/ParseAuthentication.swift | 10 +- .../Documentation.docc/ParseSwift.md | 1 + .../ParseSwift/Extensions/URLSession.swift | 9 +- .../Objects/ParseInstallation.swift | 71 +++--- Sources/ParseSwift/Objects/ParseObject.swift | 107 ++++---- Sources/ParseSwift/Objects/ParseUser.swift | 86 +++---- Sources/ParseSwift/Parse.swift | 22 +- Sources/ParseSwift/ParseConstants.swift | 2 +- Sources/ParseSwift/Types/ParseACL.swift | 36 +-- Sources/ParseSwift/Types/ParseConfig.swift | 2 +- .../ParseSwift/Types/ParseConfiguration.swift | 20 +- Sources/ParseSwift/Types/ParseFile.swift | 20 +- .../Types/ParseOperation+async.swift | 2 +- .../Types/ParseOperation+keyPath.swift | 49 ++++ Sources/ParseSwift/Types/ParseOperation.swift | 128 +++++++--- Sources/ParseSwift/Types/ParseVersion.swift | 3 +- Sources/ParseSwift/Types/Query.swift | 8 +- Tests/ParseSwiftTests/ParseObjectTests.swift | 33 ++- .../ParseOperationAsyncTests.swift | 128 ++++++++++ .../ParseSwiftTests/ParseOperationTests.swift | 235 +++++++++++++++--- 23 files changed, 701 insertions(+), 289 deletions(-) create mode 100644 Sources/ParseSwift/Types/ParseOperation+keyPath.swift diff --git a/.codecov.yml b/.codecov.yml index fd2c86e08..5ce6c15b7 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,7 +6,7 @@ coverage: status: patch: default: - target: auto + target: 72 changes: false project: default: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b0b3b759..37eabc22a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.3...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 4.10.0 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.9.3...4.10.0) + __New features__ +- Add a new operation method that allows developers to set a new value to a KeyPath without needing the string version of the key. Also adds the get() method to allow developers to get the unwrapped property of any ParseObject based on its KeyPath ([#403](https://github.com/parse-community/Parse-Swift/pull/403)), thanks to [Corey Baker](https://github.com/cbaker6). - 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 diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 15c6ed524..b7bec1e77 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -451,6 +451,10 @@ 7085DDB326D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7085DDB226D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift */; }; 7085DDB426D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7085DDB226D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift */; }; 7085DDB526D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7085DDB226D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift */; }; + 7087A93C28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7087A93B28C558CA00656E93 /* ParseOperation+keyPath.swift */; }; + 7087A93D28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7087A93B28C558CA00656E93 /* ParseOperation+keyPath.swift */; }; + 7087A93E28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7087A93B28C558CA00656E93 /* ParseOperation+keyPath.swift */; }; + 7087A93F28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7087A93B28C558CA00656E93 /* ParseOperation+keyPath.swift */; }; 708CADCF2872263D0066C279 /* ParseKeychainAccessGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708CADCE2872263D0066C279 /* ParseKeychainAccessGroupTests.swift */; }; 708CADD02872263D0066C279 /* ParseKeychainAccessGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708CADCE2872263D0066C279 /* ParseKeychainAccessGroupTests.swift */; }; 708CADD12872263D0066C279 /* ParseKeychainAccessGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708CADCE2872263D0066C279 /* ParseKeychainAccessGroupTests.swift */; }; @@ -1269,6 +1273,7 @@ 7085DD9326CBF3A70033B977 /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = ""; }; 7085DDA226CC8A470033B977 /* ParseHealth+combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ParseHealth+combine.swift"; sourceTree = ""; }; 7085DDB226D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticationCombineTests.swift; sourceTree = ""; }; + 7087A93B28C558CA00656E93 /* ParseOperation+keyPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ParseOperation+keyPath.swift"; sourceTree = ""; }; 708CADCE2872263D0066C279 /* ParseKeychainAccessGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseKeychainAccessGroupTests.swift; sourceTree = ""; }; 708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = ""; }; 709A147C283949D100BF85E5 /* ParseSchema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSchema.swift; sourceTree = ""; }; @@ -2135,6 +2140,7 @@ F97B464024D9C78B00F4A88B /* ParseOperation.swift */, 703B091026BD992E005A112F /* ParseOperation+async.swift */, 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */, + 7087A93B28C558CA00656E93 /* ParseOperation+keyPath.swift */, 91285B1B26990D7F0051B544 /* ParsePolygon.swift */, 705025BC284C610C008D6624 /* ParsePush.swift */, 705025C1284C7841008D6624 /* ParsePush+async.swift */, @@ -2697,6 +2703,7 @@ 91285B1C26990D7F0051B544 /* ParsePolygon.swift in Sources */, 91BB8FCA2690AC99005A6BA5 /* QueryViewModel.swift in Sources */, 7085DD9426CBF3A70033B977 /* Documentation.docc in Sources */, + 7087A93C28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */, 705025EB285153BC008D6624 /* ParsePushApplePayloadable.swift in Sources */, 705025A928441C96008D6624 /* ParseFieldOptions.swift in Sources */, F97B45D624D9C6F200F4A88B /* ParseEncoder.swift in Sources */, @@ -3006,6 +3013,7 @@ 91285B1D26990D7F0051B544 /* ParsePolygon.swift in Sources */, 91BB8FCB2690AC99005A6BA5 /* QueryViewModel.swift in Sources */, 7085DD9526CBF3A70033B977 /* Documentation.docc in Sources */, + 7087A93D28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */, 705025EC285153BC008D6624 /* ParsePushApplePayloadable.swift in Sources */, 705025AA28441C96008D6624 /* ParseFieldOptions.swift in Sources */, F97B45D724D9C6F200F4A88B /* ParseEncoder.swift in Sources */, @@ -3447,6 +3455,7 @@ 91679D67268E596300F71809 /* ParseVersion.swift in Sources */, 91285B1F26990D7F0051B544 /* ParsePolygon.swift in Sources */, 91BB8FCD2690AC99005A6BA5 /* QueryViewModel.swift in Sources */, + 7087A93F28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */, 705025EE285153BC008D6624 /* ParsePushApplePayloadable.swift in Sources */, 705025AC28441C96008D6624 /* ParseFieldOptions.swift in Sources */, 7085DD9726CBF3A70033B977 /* Documentation.docc in Sources */, @@ -3633,6 +3642,7 @@ 91679D66268E596300F71809 /* ParseVersion.swift in Sources */, 91285B1E26990D7F0051B544 /* ParsePolygon.swift in Sources */, 91BB8FCC2690AC99005A6BA5 /* QueryViewModel.swift in Sources */, + 7087A93E28C558CA00656E93 /* ParseOperation+keyPath.swift in Sources */, 705025ED285153BC008D6624 /* ParsePushApplePayloadable.swift in Sources */, 705025AB28441C96008D6624 /* ParseFieldOptions.swift in Sources */, 7085DD9626CBF3A70033B977 /* Documentation.docc in Sources */, diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index fff2c65bb..231ea49e6 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -290,13 +290,11 @@ public extension ParseUser { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) - } + completion(.failure(parseError)) } } } diff --git a/Sources/ParseSwift/Documentation.docc/ParseSwift.md b/Sources/ParseSwift/Documentation.docc/ParseSwift.md index 9ebec0bb2..7b8833cb9 100644 --- a/Sources/ParseSwift/Documentation.docc/ParseSwift.md +++ b/Sources/ParseSwift/Documentation.docc/ParseSwift.md @@ -13,4 +13,5 @@ To learn how to use or experiment with ParseSwift, you can run and edit the [Par - ``ParseSwift/initialize(configuration:)`` - ``ParseSwift/initialize(applicationId:clientKey:masterKey:serverURL:liveQueryServerURL:allowingCustomObjectIds:usingTransactions:usingEqualQueryConstraint:usingPostForQuery:keyValueStore:requestCachePolicy:cacheMemoryCapacity:cacheDiskCapacity:usingDataProtectionKeychain:deletingKeychainIfNeeded:httpAdditionalHeaders:maxConnectionAttempts:authentication:)`` +- ``ParseSwift/initialize(applicationId:clientKey:masterKey:serverURL:liveQueryServerURL:allowingCustomObjectIds:usingTransactions:usingEqualQueryConstraint:usingPostForQuery:keyValueStore:requestCachePolicy:cacheMemoryCapacity:cacheDiskCapacity:migratingFromObjcSDK:usingDataProtectionKeychain:deletingKeychainIfNeeded:httpAdditionalHeaders:maxConnectionAttempts:authentication:)`` diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 8aa177913..c2095de3b 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -153,11 +153,10 @@ internal extension URLSession { let data = try ParseCoding.jsonEncoder().encode(location) return try .success(mapper(data)) } catch { - guard let parseError = error as? ParseError else { - return .failure(ParseError(code: .unknownError, - // swiftlint:disable:next line_length - message: "Error decoding parse-server response: \(response) with error: \(String(describing: error))")) - } + let defaultError = ParseError(code: .unknownError, + // swiftlint:disable:next line_length + message: "Error decoding parse-server response: \(response) with error: \(String(describing: error))") + let parseError = error as? ParseError ?? defaultError return .failure(parseError) } } diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 2dd5b59eb..f65e10b6e 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -10,7 +10,7 @@ import Foundation /** Objects that conform to the `ParseInstallation` protocol have a local representation of an - installation persisted to the Parse cloud. This protocol inherits from the + installation persisted to the Keychain and Parse Server. This protocol inherits from the `ParseObject` protocol, and retains the same functionality of a `ParseObject`, but also extends it with installation-specific fields and related immutability and validity checks. @@ -21,16 +21,15 @@ import Foundation is automatically updated to match the device's time zone when the `ParseInstallation` is saved, thus these fields might not reflect the latest device state if the installation has not recently been saved. - `ParseInstallation`s which have a valid `deviceToken` and are saved to the Parse Server can be used to target push notifications. Use `setDeviceToken` to set the `deviceToken` properly. - warning: If the use of badge is desired, it should be retrieved by using UIKit, AppKit, etc. and - stored in `ParseInstallation.badge` before saving/updating the installation. - - - warning: Linux developers should set `appName`, `appIdentifier`, and `appVersion` - manually as `ParseSwift` does not have access to Bundle.main. + stored in `ParseInstallation.badge` when saving/updating the installation. + - warning: Linux, Android, and Windows developers should set `appName`, + `appIdentifier`, and `appVersion` manually as `ParseSwift` does not have access + to Bundle.main. */ public protocol ParseInstallation: ParseObject { @@ -479,13 +478,12 @@ extension ParseInstallation { try Self.updateKeychainIfNeeded([foundResult]) completion(.success(foundResult)) } catch { - let returnError: ParseError! - if let parseError = error as? ParseError { - returnError = parseError - } else { - returnError = ParseError(code: .unknownError, message: error.localizedDescription) + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError + callbackQueue.async { + completion(.failure(parseError)) } - completion(.failure(returnError)) } } else { completion(result) @@ -533,6 +531,7 @@ extension ParseInstallation { - returns: Returns saved `ParseInstallation`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ + @discardableResult public func save(options: API.Options = []) throws -> Self { try save(ignoringCustomObjectIdConfig: false, options: options) @@ -560,6 +559,7 @@ extension ParseInstallation { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ + @discardableResult public func save(ignoringCustomObjectIdConfig: Bool, options: API.Options = []) throws -> Self { var options = options @@ -724,12 +724,11 @@ extension ParseInstallation { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } } return @@ -862,13 +861,12 @@ extension ParseInstallation { try Self.updateKeychainIfNeeded([self], deleting: true) completion(.success(())) } catch { - let returnError: ParseError! - if let parseError = error as? ParseError { - returnError = parseError - } else { - returnError = ParseError(code: .unknownError, message: error.localizedDescription) + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError + callbackQueue.async { + completion(.failure(parseError)) } - completion(.failure(returnError)) } case .failure(let error): completion(.failure(error)) @@ -937,6 +935,7 @@ public extension Sequence where Element: ParseInstallation { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ + @discardableResult func saveAll(batchLimit limit: Int? = nil, // swiftlint:disable:this function_body_length transaction: Bool = configuration.isUsingTransactions, ignoringCustomObjectIdConfig: Bool = false, @@ -1232,12 +1231,11 @@ public extension Sequence where Element: ParseInstallation { commands.append(try installation.updateCommand()) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } return } @@ -1275,12 +1273,11 @@ public extension Sequence where Element: ParseInstallation { } } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } } } @@ -1499,12 +1496,10 @@ public extension Sequence where Element: ParseInstallation { } } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - guard let parseError = error as? ParseError else { - completion(.failure(ParseError(code: .unknownError, - message: error.localizedDescription))) - return - } completion(.failure(parseError)) } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 6be1805e0..f94d8ca1e 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -11,35 +11,32 @@ import Foundation // swiftlint:disable line_length /** - Objects that conform to the `ParseObject` protocol have a local representation of data persisted to the Parse cloud. + Objects that conform to the `ParseObject` protocol have a local representation of data persisted to the Parse Server. This is the main protocol that is used to interact with objects in your app. - The Swift SDK is designed for your `ParseObject`s to be "value types" (structs). - If you are using value types the the compiler will assist you with conforming to `ParseObject` protocol. If you - are thinking of using reference types, see the warning. - - After a `ParseObject`is saved/created to a Parse Server. It is recommended to conduct the rest of your updates on a - `mergeable` copy of your `ParseObject`. This allows a subset of the fields to be updated (PATCH) of an object + The Swift SDK is designed for your `ParseObject`s to be **value types (structs)**. + Since you are using value types the compiler will assist you with conforming to the `ParseObject` protocol. + After a `ParseObject`is saved/created to a Parse Server. It is recommended to conduct any updates on your updates + to a `mergeable` copy of your `ParseObject`. This allows a subset of the fields to be updated (PATCH) of an object as oppose to replacing all of the fields of an object (PUT). This reduces the amount of data sent between client and server when using `save`, `saveAll`, `update`, `updateAll`, `replace`, `replaceAll`, to update objects. - - important: It is required that all added properties be optional properties so they can eventually be used as - Parse `Pointer`'s. If a developer really wants to have a required key, they should require it on the server-side or - create methods to check the respective properties on the client-side before saving objects. See + - important: It is required that all of your `ParseObject`'s be **value types(structs)** and all added + properties be optional so they can eventually be used as Parse `Pointer`'s. If a developer really wants to + have a required key, they should require it on the server-side or create methods to check the respective properties + on the client-side before saving objects. See [here](https://github.com/parse-community/Parse-Swift/pull/315#issuecomment-1014701003) for more information on the reasons why. See the [Playgrounds](https://github.com/parse-community/Parse-Swift/blob/c119033f44b91570997ad24f7b4b5af8e4d47b64/ParseSwift.playground/Pages/1%20-%20Your%20first%20Object.xcplaygroundpage/Contents.swift#L32-L66) for an example. - important: To take advantage of `mergeable`, the developer should implement the `merge` method in every `ParseObject`. - - warning: If you plan to use "reference types" (classes), you are using at your risk as this SDK is not designed - for reference types and may have unexpected behavior when it comes to threading. You will also need to implement + - note: If you plan to use custom encoding/decoding, be sure to add `objectId`, `createdAt`, `updatedAt`, and + `ACL` to your `ParseObject`'s `CodingKeys`. + - warning: This SDK is not designed to use **reference types(classes)** for `ParseObject`'s. Doing so is at your + risk and may have unexpected behavior when it comes to threading. You will also need to implement your own `==` method to conform to `Equatable` along with with the `hash` method to conform to `Hashable`. It is important to note that for unsaved `ParseObject`'s, you will not be able to rely on `objectId` for - `Equatable` and `Hashable` as your unsaved objects will not have this value yet and is nil. A possible way to - address this is by creating a `UUID` for your objects locally and relying on that for `Equatable` and `Hashable`, - otherwise it is possible you will get "circular dependency errors" depending on your implementation. - - note: If you plan to use custom encoding/decoding, be sure to add `objectId`, `createdAt`, `updatedAt`, and - `ACL` to your `ParseObject` `CodingKeys`. + `Equatable` and `Hashable` as your unsaved objects will not have this value yet and is nil. */ public protocol ParseObject: ParseTypeable, Objectable, @@ -149,7 +146,7 @@ public protocol ParseObject: ParseTypeable, - 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(_ keyPath: WritableKeyPath) throws where W: Equatable + func revertKeyPath(_ keyPath: WritableKeyPath) throws -> Self where W: Equatable /** Reverts the `ParseObject` back to the original object before mutations began. @@ -157,7 +154,15 @@ public protocol ParseObject: ParseTypeable, - 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 + func revertObject() throws -> Self + + /** + Get the unwrapped property value. + - parameter key: The `KeyPath` of the value to get. + - throws: An error of type `ParseError` when the value is **nil**. + - returns: The unwrapped value. + */ + func get(_ keyPath: KeyPath) throws -> W where W: Equatable } // MARK: Default Implementations @@ -226,7 +231,7 @@ public extension ParseObject { return try mergeParse(with: object) } - mutating func revertKeyPath(_ keyPath: WritableKeyPath) throws where W: Equatable { + func revertKeyPath(_ keyPath: WritableKeyPath) throws -> Self where W: Equatable { guard let originalData = originalData else { throw ParseError(code: .unknownError, message: "Missing original data to revert to") @@ -237,13 +242,15 @@ public extension ParseObject { throw ParseError(code: .unknownError, message: "The current object does not have the same objectId as the original") } + var updated = self if shouldRevertKey(keyPath, original: original) { - self[keyPath: keyPath] = original[keyPath: keyPath] + updated[keyPath: keyPath] = original[keyPath: keyPath] } + return updated } - mutating func revertObject() throws { + func revertObject() throws -> Self { guard let originalData = originalData else { throw ParseError(code: .unknownError, message: "Missing original data to revert to") @@ -254,7 +261,15 @@ public extension ParseObject { throw ParseError(code: .unknownError, message: "The current object does not have the same objectId as the original") } - self = original + return original + } + + @discardableResult + func get(_ keyPath: KeyPath) throws -> W where W: Equatable { + guard let value = self[keyPath: keyPath] else { + throw ParseError(code: .unknownError, message: "Could not unwrap value") + } + return value } } @@ -601,12 +616,11 @@ transactions for this call. commands.append(try object.updateCommand()) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } return } @@ -641,12 +655,11 @@ transactions for this call. } } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } } } @@ -853,12 +866,10 @@ transactions for this call. } } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - guard let parseError = error as? ParseError else { - completion(.failure(ParseError(code: .unknownError, - message: error.localizedDescription))) - return - } completion(.failure(parseError)) } } @@ -959,6 +970,7 @@ extension ParseObject { - returns: Returns saved `ParseObject`. */ + @discardableResult public func save(options: API.Options = []) throws -> Self { try save(ignoringCustomObjectIdConfig: false, options: options) } @@ -984,6 +996,7 @@ extension ParseObject { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ + @discardableResult public func save(ignoringCustomObjectIdConfig: Bool = false, options: API.Options = []) throws -> Self { var childObjects: [String: PointerType]? @@ -1127,13 +1140,10 @@ extension ParseObject { childFiles: savedChildFiles, completion: completion) } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - guard let parseError = error as? ParseError else { - let error = ParseError(code: .unknownError, - message: error.localizedDescription) - completion(.failure(error)) - return - } completion(.failure(parseError)) } } @@ -1255,12 +1265,9 @@ extension ParseObject { } completion(objectsFinishedSaving, filesFinishedSaving, nil) } catch { - guard let parseError = error as? ParseError else { - completion(objectsFinishedSaving, filesFinishedSaving, - ParseError(code: .unknownError, - message: error.localizedDescription)) - return - } + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError completion(objectsFinishedSaving, filesFinishedSaving, parseError) } } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 6b428a653..76356fe29 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -1,9 +1,10 @@ import Foundation /** - Objects that conform to the `ParseUser` protocol have a local representation of a user persisted to the Parse Data. - This protocol inherits from the `ParseObject` protocol, and retains the same functionality of a `ParseObject`, - but also extends it with various user specific methods, like authentication, signing up, and validation uniqueness. + Objects that conform to the `ParseUser` protocol have a local representation of a user persisted to the + Keychain and Parse Server. This protocol inherits from the `ParseObject` protocol, and retains the same + functionality of a `ParseObject`, but also extends it with various user specific methods, like + authentication, signing up, and validation uniqueness. */ public protocol ParseUser: ParseObject { /** @@ -765,14 +766,11 @@ extension ParseUser { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, - message: error.localizedDescription) - completion(.failure(parseError)) - } + completion(.failure(parseError)) } } } else { @@ -783,13 +781,11 @@ extension ParseUser { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) - } + completion(.failure(parseError)) } } } @@ -834,13 +830,11 @@ extension ParseUser { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) - } + completion(.failure(parseError)) } } } @@ -969,13 +963,10 @@ extension ParseUser { try Self.updateKeychainIfNeeded([foundResult]) completion(.success(foundResult)) } catch { - let returnError: ParseError! - if let parseError = error as? ParseError { - returnError = parseError - } else { - returnError = ParseError(code: .unknownError, message: error.localizedDescription) - } - completion(.failure(returnError)) + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError + completion(.failure(parseError)) } } else { completion(result) @@ -1023,6 +1014,7 @@ extension ParseUser { - returns: Returns saved `ParseUser`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ + @discardableResult public func save(options: API.Options = []) throws -> Self { try save(ignoringCustomObjectIdConfig: false, options: options) } @@ -1049,6 +1041,7 @@ extension ParseUser { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ + @discardableResult public func save(ignoringCustomObjectIdConfig: Bool, options: API.Options = []) throws -> Self { var childObjects: [String: PointerType]? @@ -1207,12 +1200,11 @@ extension ParseUser { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } } return @@ -1734,12 +1726,11 @@ public extension Sequence where Element: ParseUser { commands.append(try user.updateCommand()) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } return } @@ -1774,12 +1765,11 @@ public extension Sequence where Element: ParseUser { } } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - completion(.failure(.init(code: .unknownError, message: error.localizedDescription))) - } + completion(.failure(parseError)) } } } @@ -1994,12 +1984,10 @@ public extension Sequence where Element: ParseUser { } } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - guard let parseError = error as? ParseError else { - completion(.failure(ParseError(code: .unknownError, - message: error.localizedDescription))) - return - } completion(.failure(parseError)) } } diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index 0c377ed7c..5f764296b 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -86,9 +86,9 @@ public var configuration: ParseConfiguration { Configure the Parse Swift client. This should only be used when starting your app. Typically in the `application(... didFinishLaunchingWithOptions launchOptions...)`. - parameter configuration: The Parse configuration. - - warning: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - important: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - note: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: `usingTransactions` is experimental. - - warning: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: Setting `usingDataProtectionKeychain` to **true** is known to cause issues in Playgrounds or in situtations when apps do not have credentials to setup a Keychain. */ @@ -191,8 +191,8 @@ public func initialize(configuration: ParseConfiguration) { Defaults to **false**. - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this - this is the only store available since there is no Keychain. Linux users should replace this store with an - encrypted one. + this is the only store available since there is no Keychain. Linux, Android, and Windows users should + replace this store with an encrypted one. - parameter requestCachePolicy: The default caching policy for all http requests that determines when to return a response from the cache. Defaults to `useProtocolCachePolicy`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/url_loading_system/accessing_cached_data) for more info. @@ -210,9 +210,9 @@ public func initialize(configuration: ParseConfiguration) { It should have the following argument signature: `(challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. - - warning: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - important: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - note: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: `usingTransactions` is experimental. - - warning: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: Setting `usingDataProtectionKeychain` to **true** is known to cause issues in Playgrounds or in situtations when apps do not have credentials to setup a Keychain. */ @@ -276,8 +276,8 @@ public func initialize( Defaults to **false**. - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this - this is the only store available since there is no Keychain. Linux users should replace this store with an - encrypted one. + this is the only store available since there is no Keychain. Linux, Android, and Windows users should + replace this store with an encrypted one. - parameter requestCachePolicy: The default caching policy for all http requests that determines when to return a response from the cache. Defaults to `useProtocolCachePolicy`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/url_loading_system/accessing_cached_data) for more info. @@ -297,9 +297,9 @@ public func initialize( It should have the following argument signature: `(challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. - - warning: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - important: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - note: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: `usingTransactions` is experimental. - - warning: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: Setting `usingDataProtectionKeychain` to **true** is known to cause issues in Playgrounds or in situtations when apps do not have credentials to setup a Keychain. */ @@ -397,7 +397,7 @@ public func deleteObjectiveCKeychain() throws { for more information. **false** to disable synchronization. - throws: An error of type `ParseError`. - returns: **true** if the Keychain was moved to the new `accessGroup`, **false** otherwise. - - warning: Setting `synchronizeAcrossDevices == true` requires `accessGroup` to be + - important: Setting `synchronizeAcrossDevices == true` requires `accessGroup` to be set to a valid [keychain group](https://developer.apple.com/documentation/security/ksecattraccessgroup). */ @discardableResult public func setAccessGroup(_ accessGroup: String?, diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 1eb5892b5..11a0903d9 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 = "4.9.3" + static let version = "4.10.0" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Sources/ParseSwift/Types/ParseACL.swift b/Sources/ParseSwift/Types/ParseACL.swift index 07818a4bc..1fb34d44d 100644 --- a/Sources/ParseSwift/Types/ParseACL.swift +++ b/Sources/ParseSwift/Types/ParseACL.swift @@ -12,7 +12,7 @@ import Foundation `ParseACL` is used to control which users can access or modify a particular `ParseObject`. Each `ParseObject` has its own ACL. You can grant read and write permissions separately to specific users, to groups of users that belong to roles, or you can grant permissions to - "the public" so that, for example, any user could read a particular object but only a + **the public** so that, for example, any user could read a particular object but only a particular set of users could write to that object. */ public struct ParseACL: ParseTypeable, @@ -311,8 +311,8 @@ public struct ParseACL: ParseTypeable, extension ParseACL { /** Get the default ACL from the Keychain. - - returns: Returns the default ACL. + - throws: An error of type `ParseError`. */ public static func defaultACL() throws -> Self { @@ -351,32 +351,18 @@ extension ParseACL { } /** - Sets a default ACL that can later be used by `ParseObjects`. - - To apply the default ACL to all instances of a respective `ParseObject` when they are created, - you will need to add `ACL = try? ParseACL.defaultACL()`. You can also at it when - conforming to `ParseObject`: - - struct MyParseObject: ParseObject { - - var objectId: String? - var createdAt: Date? - var updatedAt: Date? - var ACL: ParseACL? = try? ParseACL.defaultACL() - } + Sets a default ACL that can later be used by `ParseObjects`. The default ACL + is persisted to the Keychain. This value will be copied and used as a template when + new `ParseObject`'s are created locally. Any changes to the default ACL will not + have effect on already saved `ParseObject`'s. - parameter acl: The ACL to use as a template for instances of `ParseObject`. - - This value will be copied and used as a template for the creation of new ACLs, so changes to the - instance after this method has been called will not be reflected in new instance of `ParseObject`. - - parameter withAccessForCurrentUser: If **true**, the `ACL` that is applied to - newly-created instance of `ParseObject` will - provide read and write access to the `ParseUser.+currentUser` at the time of creation. - - If **false**, the provided `acl` will be used without modification. - - If `acl` is `nil`, this value is ignored. - - - returns: Updated defaultACL + newly-created instance of `ParseObject` will provide read and write access to the + `ParseUser.currentUser` at the time of creation. If **false**, the provided `acl` + will be used without modification. If `acl` is `nil`, this value is ignored. + - returns: Updated default ACL. + - throws: An error of type `ParseError`. */ public static func setDefaultACL(_ acl: ParseACL, withAccessForCurrentUser: Bool) throws -> ParseACL { diff --git a/Sources/ParseSwift/Types/ParseConfig.swift b/Sources/ParseSwift/Types/ParseConfig.swift index fc124c9e2..bdc4c12da 100644 --- a/Sources/ParseSwift/Types/ParseConfig.swift +++ b/Sources/ParseSwift/Types/ParseConfig.swift @@ -11,7 +11,7 @@ import Foundation /** Objects that conform to the `ParseConfig` protocol are able to access the Config on the Parse Server. When conforming to `ParseConfig`, any properties added can be retrieved by the client or updated on - the server. + the server. The current `ParseConfig` is persisted to the Keychain and Parse Server. */ public protocol ParseConfig: ParseTypeable {} diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift index 02f6a42d9..533a6f9ac 100644 --- a/Sources/ParseSwift/Types/ParseConfiguration.swift +++ b/Sources/ParseSwift/Types/ParseConfiguration.swift @@ -16,9 +16,9 @@ import FoundationNetworking /** The Configuration for a Parse client. - - warning: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - important: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - note: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: `usingTransactions` is experimental. - - warning: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: Setting `usingDataProtectionKeychain` to **true** is known to cause issues in Playgrounds or in situtations when apps do not have credentials to setup a Keychain. */ @@ -119,8 +119,8 @@ public struct ParseConfiguration { Defaults to **false**. - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this - this is the only store available since there is no Keychain. Linux users should replace this store with an - encrypted one. + this is the only store available since there is no Keychain. Linux, Android, and Windows users should + replace this store with an encrypted one. - parameter requestCachePolicy: The default caching policy for all http requests that determines when to return a response from the cache. Defaults to `useProtocolCachePolicy`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/url_loading_system/accessing_cached_data) for more info. @@ -142,9 +142,9 @@ public struct ParseConfiguration { It should have the following argument signature: `(challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. - - warning: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - important: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - note: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: `usingTransactions` is experimental. - - warning: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: Setting `usingDataProtectionKeychain` to **true** is known to cause issues in Playgrounds or in situtations when apps do not have credentials to setup a Keychain. */ @@ -208,8 +208,8 @@ public struct ParseConfiguration { Defaults to **false**. - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this - this is the only store available since there is no Keychain. Linux users should replace this store with an - encrypted one. + this is the only store available since there is no Keychain. Linux, Android, and Windows users should + replace this store with an encrypted one. - parameter requestCachePolicy: The default caching policy for all http requests that determines when to return a response from the cache. Defaults to `useProtocolCachePolicy`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/url_loading_system/accessing_cached_data) for more info. @@ -231,9 +231,9 @@ public struct ParseConfiguration { It should have the following argument signature: `(challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details. - - warning: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - important: It is recomended to only specify `masterKey` when using the SDK on a server. Do not use this key on the client. + - note: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: `usingTransactions` is experimental. - - warning: Setting `usingPostForQuery` to **true** will require all queries to access the server instead of following the `requestCachePolicy`. - warning: Setting `usingDataProtectionKeychain` to **true** is known to cause issues in Playgrounds or in situtations when apps do not have credentials to setup a Keychain. */ diff --git a/Sources/ParseSwift/Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift index fdc97f726..9a089e093 100644 --- a/Sources/ParseSwift/Types/ParseFile.swift +++ b/Sources/ParseSwift/Types/ParseFile.swift @@ -467,13 +467,11 @@ extension ParseFile { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) - } + completion(.failure(parseError)) } } case .failure(let error): @@ -491,13 +489,11 @@ extension ParseFile { completion(result) } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) - } + completion(.failure(parseError)) } } } diff --git a/Sources/ParseSwift/Types/ParseOperation+async.swift b/Sources/ParseSwift/Types/ParseOperation+async.swift index baf2dd719..9eaa3e3f7 100644 --- a/Sources/ParseSwift/Types/ParseOperation+async.swift +++ b/Sources/ParseSwift/Types/ParseOperation+async.swift @@ -20,7 +20,7 @@ public extension ParseOperation { - returns: A saved `ParseFile`. - throws: An error of type `ParseError`. */ - func save(options: API.Options = []) async throws -> T { + @discardableResult func save(options: API.Options = []) async throws -> T { try await withCheckedThrowingContinuation { continuation in self.save(options: options, completion: continuation.resume) diff --git a/Sources/ParseSwift/Types/ParseOperation+keyPath.swift b/Sources/ParseSwift/Types/ParseOperation+keyPath.swift new file mode 100644 index 000000000..279d0d812 --- /dev/null +++ b/Sources/ParseSwift/Types/ParseOperation+keyPath.swift @@ -0,0 +1,49 @@ +// +// ParseOperation+keyPath.swift +// ParseSwift +// +// Created by Corey Baker on 9/4/22. +// Copyright © 2022 Parse Community. All rights reserved. +// + +import Foundation + +extension ParseOperation { + + func setOriginalDataIfNeeded(_ operation: Self) -> Self { + var mutableOperation = operation + if mutableOperation.target.originalData == nil { + mutableOperation.target = mutableOperation.target.mergeable + } + return mutableOperation + } + + /** + An operation that sets a field's value. + - Parameters: + - keyPath: The respective `KeyPath` of the object. + - value: The value to set the `KeyPath` to. + - returns: The updated operations. + - warning: Do not combine operations using this method with other operations that + do not use this method to **set** all operations. If you need to combine multiple types + of operations such as: add, increment, forceSet, etc., use + `func set(_ key: (String, WritableKeyPath), value: W?)` + instead. + */ + public func set(_ keyPath: WritableKeyPath, + value: W) throws -> Self where W: Encodable & Equatable { + guard operations.isEmpty, + keysToNull.isEmpty else { + throw ParseError(code: .unknownError, + message: """ + Cannot combine other operations such as: add, increment, + forceSet, etc., with this method. Use the \"set\" method that takes + the (String, WritableKeyPath) tuple as an argument instead to + combine multiple types of operations. + """) + } + var mutableOperation = setOriginalDataIfNeeded(self) + mutableOperation.target[keyPath: keyPath] = value + return mutableOperation + } +} diff --git a/Sources/ParseSwift/Types/ParseOperation.swift b/Sources/ParseSwift/Types/ParseOperation.swift index 6735a00b1..5be8e2de5 100644 --- a/Sources/ParseSwift/Types/ParseOperation.swift +++ b/Sources/ParseSwift/Types/ParseOperation.swift @@ -29,18 +29,18 @@ public struct ParseOperation: Savable where T: ParseObject { /** 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. + - key: A tuple consisting of the key and the respective `KeyPath` of the object. + - value: The value to set the `KeyPath` to. - returns: The updated operations. - Note: Set the value to "nil" if you want it to be "null" on the Parse Server. */ public func set(_ key: (String, WritableKeyPath), - value: W?) -> Self where W: Encodable { + value: W?) -> Self where W: Encodable & Equatable { var mutableOperation = self if value == nil && target[keyPath: key.1] != nil { mutableOperation.keysToNull.insert(key.0) mutableOperation.target[keyPath: key.1] = value - } else if !target[keyPath: key.1].isEqual(value) { + } else if target[keyPath: key.1] != value { mutableOperation.operations[key.0] = value mutableOperation.target[keyPath: key.1] = value } @@ -50,8 +50,8 @@ public struct ParseOperation: Savable where T: ParseObject { /** 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. + - key: A tuple consisting of the key and the respective `KeyPath` of the object. + - value: The value to set the `KeyPath` to. - returns: The updated operations. - Note: Set the value to "nil" if you want it to be "null" on the Parse Server. */ @@ -98,10 +98,17 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that adds a new element to an array field, only if it was not already present. - Parameters: - - key: A tuple consisting of the key and the respective 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. */ + @available(*, deprecated, + message: """ + The KeyPath of a ParseObject should always point to an optional value. + This means that all properties of your ParseObject's should be optional. + Please read the important notes and warnings in the documentation for + details. + """) public func addUnique(_ key: (String, WritableKeyPath), objects: [V]) -> Self where V: Encodable, V: Hashable { var mutableOperation = self @@ -116,7 +123,7 @@ public struct ParseOperation: Savable where T: ParseObject { An operation that adds a new element to an array field, only if it was not already present. - Parameters: - - key: A tuple consisting of the key and the respective 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. */ @@ -146,10 +153,17 @@ 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 the respective 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. */ + @available(*, deprecated, + message: """ + The KeyPath of a ParseObject should always point to an optional value. + This means that all properties of your ParseObject's should be optional. + Please read the important notes and warnings in the documentation for + details. + """) public func add(_ key: (String, WritableKeyPath), objects: [V]) -> Self where V: Encodable { var mutableOperation = self @@ -163,7 +177,7 @@ 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 the respective 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. */ @@ -193,10 +207,17 @@ 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 the respective 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. */ + @available(*, deprecated, + message: """ + The KeyPath of a ParseObject should always point to an optional value. + This means that all properties of your ParseObject's should be optional. + Please read the important notes and warnings in the documentation for + details. + """) public func addRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { var mutableOperation = self @@ -210,7 +231,7 @@ 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 the respective 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. */ @@ -242,10 +263,17 @@ 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 the respective 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. */ + @available(*, deprecated, + message: """ + The KeyPath of a ParseObject should always point to an optional value. + This means that all properties of your ParseObject's should be optional. + Please read the important notes and warnings in the documentation for + details. + """) public func remove(_ key: (String, WritableKeyPath), objects: [V]) -> Self where V: Encodable, V: Hashable { var mutableOperation = self @@ -263,7 +291,7 @@ 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 the respective 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. */ @@ -298,10 +326,17 @@ 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 the respective 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. */ + @available(*, deprecated, + message: """ + The KeyPath of a ParseObject should always point to an optional value. + This means that all properties of your ParseObject's should be optional. + Please read the important notes and warnings in the documentation for + details. + """) public func removeRelation(_ key: (String, WritableKeyPath), objects: [V]) throws -> Self where V: ParseObject { var mutableOperation = self @@ -319,7 +354,7 @@ 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 the respective 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. */ @@ -350,7 +385,7 @@ 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 the respective 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 { @@ -362,8 +397,7 @@ public struct ParseOperation: Savable where T: ParseObject { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: RawCodingKey.self) - try operations.forEach { pair in - let (key, value) = pair + try operations.forEach { key, value in let encoder = container.superEncoder(forKey: .key(key)) try value.encode(to: encoder) } @@ -382,12 +416,26 @@ extension ParseOperation { - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An error of type `ParseError`. - - returns: Returns saved `ParseObject`. */ - public func save(options: API.Options = []) throws -> T { + @discardableResult public func save(options: API.Options = []) throws -> T { guard target.objectId != nil else { - throw ParseError(code: .missingObjectId, message: "ParseObject is not saved.") + throw ParseError(code: .missingObjectId, + message: "ParseObject is not saved.") + } + guard target.originalData == nil else { + guard operations.isEmpty, + keysToNull.isEmpty else { + throw ParseError(code: .unknownError, + message: """ + Cannot combine operations with the \"set\" method that uses + just the KeyPath with other operations such as: add, increment, + forceSet, etc., that use the KeyPath and/or key String. Use the + \"set\" method that takes the (String, WritableKeyPath) tuple + as an argument instead to combine multiple types of operations. + """) + } + return try target.save(options: options) } return try saveCommand() .execute(options: options) @@ -407,26 +455,40 @@ extension ParseOperation { completion: @escaping (Result) -> Void ) { guard target.objectId != nil else { + let error = ParseError(code: .missingObjectId, + message: "ParseObject is not saved.") callbackQueue.async { - let error = ParseError(code: .missingObjectId, message: "ParseObject is not saved.") completion(.failure(error)) } return } - do { - try self.saveCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) - } - } catch { - callbackQueue.async { - let error = ParseError(code: .missingObjectId, message: "ParseObject is not saved.") - completion(.failure(error)) + guard target.originalData == nil else { + guard operations.isEmpty, + keysToNull.isEmpty else { + let error = ParseError(code: .unknownError, + message: """ + Cannot combine operations with the \"set\" method that uses + just the KeyPath with other operations such as: add, increment, + forceSet, etc., that use the KeyPath and/or key String. Use the + \"set\" method that takes the (String, WritableKeyPath) tuple + as an argument instead to combine multiple types of operations. + """) + callbackQueue.async { + completion(.failure(error)) + } + return } + target.save(options: options, + callbackQueue: callbackQueue, + completion: completion) + return } + self.saveCommand().executeAsync(options: options, + callbackQueue: callbackQueue, + completion: completion) } - func saveCommand() throws -> API.NonParseBodyCommand, T> { + func saveCommand() -> API.NonParseBodyCommand, T> { // MARK: Should be switched to ".PATCH" when server supports PATCH. API.NonParseBodyCommand(method: .PUT, path: target.endpoint, body: self) { try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: $0).apply(to: self.target) diff --git a/Sources/ParseSwift/Types/ParseVersion.swift b/Sources/ParseSwift/Types/ParseVersion.swift index 1cbddda40..34c2e574e 100644 --- a/Sources/ParseSwift/Types/ParseVersion.swift +++ b/Sources/ParseSwift/Types/ParseVersion.swift @@ -8,7 +8,8 @@ import Foundation -/// `ParseVersion` is used to determine the version of the SDK. +/// `ParseVersion` is used to determine the version of the SDK. The current +/// version of the SDK is persisted to the Keychain. public struct ParseVersion: ParseTypeable, Comparable { var string: String diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 9fdc761b4..acb1e6c79 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -669,12 +669,10 @@ extension Query: Queryable { finished = true } } catch { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError callbackQueue.async { - guard let parseError = error as? ParseError else { - completion(.failure(ParseError(code: .unknownError, - message: error.localizedDescription))) - return - } completion(.failure(parseError)) } return diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 2bd28440e..cc3271324 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -434,7 +434,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.points = 50 mutableScore.player = "ali" XCTAssertNotEqual(mutableScore, score) - try mutableScore.revertObject() + mutableScore = try mutableScore.revertObject() XCTAssertEqual(mutableScore, score) } @@ -446,7 +446,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.player = "ali" XCTAssertNotEqual(mutableScore, score) do { - try mutableScore.revertObject() + mutableScore = try mutableScore.revertObject() XCTFail("Should have thrown error") } catch { guard let parseError = error as? ParseError else { @@ -466,7 +466,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.objectId = "nolo" XCTAssertNotEqual(mutableScore, score) do { - try mutableScore.revertObject() + mutableScore = try mutableScore.revertObject() XCTFail("Should have thrown error") } catch { guard let parseError = error as? ParseError else { @@ -484,7 +484,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.points = 50 mutableScore.player = "ali" XCTAssertNotEqual(mutableScore, score) - try mutableScore.revertKeyPath(\.player) + mutableScore = try mutableScore.revertKeyPath(\.player) XCTAssertNotEqual(mutableScore, score) XCTAssertEqual(mutableScore.objectId, score.objectId) XCTAssertNotEqual(mutableScore.points, score.points) @@ -498,7 +498,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.points = 50 mutableScore.player = nil XCTAssertNotEqual(mutableScore, score) - try mutableScore.revertKeyPath(\.player) + mutableScore = try mutableScore.revertKeyPath(\.player) XCTAssertNotEqual(mutableScore, score) XCTAssertEqual(mutableScore.objectId, score.objectId) XCTAssertNotEqual(mutableScore.points, score.points) @@ -513,7 +513,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.points = 50 mutableScore.player = "ali" XCTAssertNotEqual(mutableScore, score) - try mutableScore.revertKeyPath(\.player) + mutableScore = try mutableScore.revertKeyPath(\.player) XCTAssertNotEqual(mutableScore, score) XCTAssertEqual(mutableScore.objectId, score.objectId) XCTAssertNotEqual(mutableScore.points, score.points) @@ -528,7 +528,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.player = "ali" XCTAssertNotEqual(mutableScore, score) do { - try mutableScore.revertKeyPath(\.player) + mutableScore = try mutableScore.revertKeyPath(\.player) XCTFail("Should have thrown error") } catch { guard let parseError = error as? ParseError else { @@ -548,7 +548,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length mutableScore.objectId = "nolo" XCTAssertNotEqual(mutableScore, score) do { - try mutableScore.revertKeyPath(\.player) + mutableScore = try mutableScore.revertKeyPath(\.player) XCTFail("Should have thrown error") } catch { guard let parseError = error as? ParseError else { @@ -559,6 +559,23 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testGet() throws { + let originalPoints = 10 + let score = GameScore(points: originalPoints) + let points = try score.get(\.points) + XCTAssertEqual(points, originalPoints) + do { + try score.get(\.ACL) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("unwrap")) + } + } + func testFetchCommand() { var score = GameScore(points: 10) let className = score.className diff --git a/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift b/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift index 4f33f2343..efed502db 100644 --- a/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationAsyncTests.swift @@ -104,5 +104,133 @@ class ParseOperationAsyncTests: XCTestCase { // swiftlint:disable:this type_body XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) XCTAssertNil(saved.ACL) } + + @MainActor + func testSaveServerError() async throws { + + var score = GameScore(points: 10) + score.objectId = "yarr" + let operations = score.operation + .increment("points", by: 1) + + let serverError = ParseError(code: .operationForbidden, message: "Test error") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverError) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + try await operations.save() + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(parseError, serverError) + } + } + + @MainActor + func testSaveNoObjectId() async throws { + var score = GameScore() + score.points = 10 + let operations = score.operation + .increment("points", by: 1) + + do { + try await operations.save() + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(parseError.code, .missingObjectId) + } + } + + @MainActor + func testSaveKeyPath() async throws { // swiftlint:disable:this function_body_length + var score = GameScore() + score.objectId = "yarr" + let operations = try score.operation + .set(\.points, value: 15) + .set(\.player, value: "hello") + + var scoreOnServer = score + scoreOnServer.points = 15 + scoreOnServer.player = "hello" + 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 await operations.save() + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + XCTAssertEqual(saved, scoreOnServer) + } catch { + XCTFail(error.localizedDescription) + } + } + + @MainActor + func testSaveKeyPathOtherTypeOperationsExist() async throws { // swiftlint:disable:this function_body_length + var score = GameScore() + score.objectId = "yarr" + let operations = try score.operation + .set(\.points, value: 15) + .set(("player", \.player), value: "hello") + + do { + try await operations.save() + XCTFail("Should have failed") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Cannot combine")) + } + } + + @MainActor + func testSaveKeyPathNilOperationsExist() async throws { // swiftlint:disable:this function_body_length + var score = GameScore() + score.objectId = "yarr" + let operations = try score.operation + .set(\.points, value: 15) + .set(("points", \.points), value: nil) + + do { + try await operations.save() + XCTFail("Should have failed") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Cannot combine")) + } + } } #endif diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift index 2ca6b1ce1..907bf8a08 100644 --- a/Tests/ParseSwiftTests/ParseOperationTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -21,20 +21,48 @@ class ParseOperationTests: XCTestCase { //: Your own properties var points: Int? - var members: [String] = [String]() + var members: [String]? var levels: [String]? var previous: [Level]? - var next: [Level] + var next: [Level]? + + init() { + } + + // custom initializers + init(points: Int) { + self.points = points + self.next = [Level(level: 5)] + self.members = [String]() + } + } + + // Used for deprecated operations + struct GameScoreDeprecated: ParseObject { + //: These are required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var originalData: Data? + + //: Your own properties + var points: Int + var members: [String] + var levels: [String]? + var previous: [Level]? + var next: [Level] = [Level(level: 3)] - //custom initializers init() { self.points = 5 - self.next = [Level()] + self.members = ["hello"] } + // custom initializers init(points: Int) { self.points = points - self.next = [Level()] + self.next = [Level(level: 5)] + self.members = [String]() } } @@ -47,15 +75,16 @@ class ParseOperationTests: XCTestCase { var originalData: Data? //: Your own properties - var level: Int - var members = [String]() + var level: Int? + var members: [String]? - //custom initializers init() { - self.level = 5 } + + //custom initializers init(level: Int) { self.level = level + self.members = [String]() } } @@ -82,14 +111,15 @@ class ParseOperationTests: XCTestCase { } func testSaveCommand() throws { - var score = GameScore(points: 10) + var score = GameScore() + score.points = 10 let objectId = "hello" score.objectId = objectId let operations = score.operation .increment("points", by: 1) let className = score.className - let command = try operations.saveCommand() + let command = operations.saveCommand() XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") XCTAssertEqual(command.method, API.Method.PUT) @@ -107,7 +137,8 @@ class ParseOperationTests: XCTestCase { } func testSave() { // swiftlint:disable:this function_body_length - var score = GameScore(points: 10) + var score = GameScore() + score.points = 10 score.objectId = "yarr" let operations = score.operation .increment("points", by: 1) @@ -149,8 +180,99 @@ class ParseOperationTests: XCTestCase { } } + func testSaveNoObjectId() { + var score = GameScore() + score.points = 10 + let operations = score.operation + .increment("points", by: 1) + + do { + try operations.save() + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(parseError.code, .missingObjectId) + } + } + + func testSaveKeyPath() throws { // swiftlint:disable:this function_body_length + var score = GameScore() + score.objectId = "yarr" + let operations = try score.operation + .set(\.points, value: 15) + .set(\.levels, value: ["hello"]) + + var scoreOnServer = score + scoreOnServer.points = 15 + scoreOnServer.levels = ["hello"] + 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)) + XCTAssertEqual(saved, scoreOnServer) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveKeyPathOtherTypeOperationsExist() throws { // swiftlint:disable:this function_body_length + var score = GameScore() + score.objectId = "yarr" + let operations = try score.operation + .set(\.points, value: 15) + .set(("levels", \.levels), value: ["hello"]) + + do { + try operations.save() + XCTFail("Should have failed") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Cannot combine")) + } + } + + func testSaveKeyPathNilOperationsExist() throws { // swiftlint:disable:this function_body_length + var score = GameScore() + score.objectId = "yarr" + let operations = try score.operation + .set(\.points, value: 15) + .set(("points", \.points), value: nil) + + do { + try operations.save() + XCTFail("Should have failed") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Cannot combine")) + } + } + func testSaveAsyncMainQueue() { - var score = GameScore(points: 10) + var score = GameScore() + score.points = 10 score.objectId = "yarr" let operations = score.operation .increment("points", by: 1) @@ -204,7 +326,8 @@ class ParseOperationTests: XCTestCase { } func testSaveSet() throws { // swiftlint:disable:this function_body_length - var score = GameScore(points: 10) + var score = GameScore() + score.points = 10 score.objectId = "yarr" let operations = score.operation .set(("points", \.points), value: 15) @@ -246,7 +369,8 @@ class ParseOperationTests: XCTestCase { } func testSaveSetToNull() throws { // swiftlint:disable:this function_body_length - var score = GameScore(points: 10) + var score = GameScore() + score.points = 10 score.objectId = "yarr" let operations = score.operation .set(("points", \.points), value: nil) @@ -288,7 +412,8 @@ class ParseOperationTests: XCTestCase { } func testSaveSetAsyncMainQueue() throws { - var score = GameScore(points: 10) + var score = GameScore() + score.points = 10 score.objectId = "yarr" let operations = score.operation .set(("points", \.points), value: 15) @@ -362,8 +487,8 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testAddKeypath() throws { - let score = GameScore(points: 10) + func testAddKeypathDeprecated() throws { + let score = GameScoreDeprecated() let operations = score.operation .add(("test", \.members), objects: ["hello"]) let expected = "{\"test\":{\"__op\":\"Add\",\"objects\":[\"hello\"]}}" @@ -373,7 +498,7 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testAddOptionalKeypath() throws { + func testAddKeypath() throws { let score = GameScore(points: 10) let operations = score.operation .add(("test", \.levels), objects: ["hello"]) @@ -395,8 +520,8 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testAddUniqueKeypath() throws { - let score = GameScore(points: 10) + func testAddUniqueKeypathDeprecated() throws { + let score = GameScoreDeprecated() let operations = score.operation .addUnique(("test", \.members), objects: ["hello"]) let expected = "{\"test\":{\"__op\":\"AddUnique\",\"objects\":[\"hello\"]}}" @@ -406,7 +531,7 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testAddUniqueOptionalKeypath() throws { + func testAddUniqueKeypath() throws { let score = GameScore(points: 10) let operations = score.operation .addUnique(("test", \.levels), objects: ["hello"]) @@ -431,8 +556,8 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testAddRelationKeypath() throws { - let score = GameScore(points: 10) + func testAddRelationKeypathDeprecated() throws { + let score = GameScoreDeprecated() var level = Level(level: 2) level.objectId = "yolo" let operations = try score.operation @@ -445,7 +570,7 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testAddRelationOptionalKeypath() throws { + func testAddRelationKeypath() throws { let score = GameScore(points: 10) var level = Level(level: 2) level.objectId = "yolo" @@ -470,8 +595,8 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testRemoveKeypath() throws { - let score = GameScore(points: 10) + func testRemoveKeypathDeprecated() throws { + let score = GameScoreDeprecated() let operations = score.operation .remove(("test", \.members), objects: ["hello"]) let expected = "{\"test\":{\"__op\":\"Remove\",\"objects\":[\"hello\"]}}" @@ -481,7 +606,7 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testRemoveOptionalKeypath() throws { + func testRemoveKeypath() throws { let score = GameScore(points: 10) let operations = score.operation .remove(("test", \.levels), objects: ["hello"]) @@ -506,8 +631,8 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testRemoveRelationKeypath() throws { - let score = GameScore(points: 10) + func testRemoveRelationKeypathDeprecated() throws { + let score = GameScoreDeprecated() var level = Level(level: 2) level.objectId = "yolo" let operations = try score.operation @@ -520,7 +645,7 @@ class ParseOperationTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testRemoveRelationOptionalKeypath() throws { + func testRemoveRelationKeypath() throws { let score = GameScore(points: 10) var level = Level(level: 2) level.objectId = "yolo" @@ -563,6 +688,54 @@ class ParseOperationTests: XCTestCase { XCTAssertNil(operations3.target.points) } + func testSetKeyPath() throws { + var score = GameScore() + score.points = 10 + var operations = try score.operation.set(\.points, value: 15) + .set(\.levels, value: ["hello"]) + var expected = GameScore() + expected.points = 15 + expected.levels = ["hello"] + XCTAssertNotNil(operations.target.originalData) + XCTAssertNotEqual(operations.target, expected) + operations.target.originalData = nil + XCTAssertEqual(operations.target, expected) + } + + func testSetKeyPathOtherTypeOperationsExist() throws { + var score = GameScore() + score.points = 10 + var operations = score.operation + .set(("levels", \.levels), value: ["hello"]) + do { + operations = try operations.set(\.points, value: 15) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Cannot combine")) + } + } + + func testSetKeyPathNilOperationsExist() throws { + var score = GameScore() + score.points = 10 + var operations = score.operation + .set(("points", \.points), value: nil) + do { + operations = try operations.set(\.points, value: 15) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("Cannot combine")) + } + } + func testObjectIdSet() throws { var score = GameScore() score.objectId = "test"