From 8251c9fa5717cebd93d6d499a67d17dc604ca33a Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Thu, 28 Jan 2021 23:03:22 -0500 Subject: [PATCH 01/15] wip --- ParseSwift.xcodeproj/project.pbxproj | 10 ++++ .../Objects/ParseUser+publisher.swift | 55 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 Sources/ParseSwift/Objects/ParseUser+publisher.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 502d489d1..9fdcb72c3 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -75,6 +75,10 @@ 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D562506CE890091CC1D /* BaseParseInstallation.swift */; }; 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D562506CE890091CC1D /* BaseParseInstallation.swift */; }; 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */; }; + 7016ED3225C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; + 7016ED3325C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; + 7016ED3425C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; + 7016ED3525C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; 7033ECB325584A83009770F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB225584A83009770F3 /* AppDelegate.swift */; }; 7033ECB525584A83009770F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB425584A83009770F3 /* ViewController.swift */; }; 7033ECB825584A83009770F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB625584A83009770F3 /* Main.storyboard */; }; @@ -459,6 +463,7 @@ 70110D51250680140091CC1D /* ParseConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConstants.swift; sourceTree = ""; }; 70110D562506CE890091CC1D /* BaseParseInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseParseInstallation.swift; sourceTree = ""; }; 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationTests.swift; sourceTree = ""; }; + 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseUser+publisher.swift"; sourceTree = ""; }; 7033ECB025584A83009770F3 /* TestHostTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHostTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7033ECB225584A83009770F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7033ECB425584A83009770F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -968,6 +973,7 @@ 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */, 70C5503725B406B800B5DBC2 /* ParseSession.swift */, F97B45C424D9C6F200F4A88B /* ParseUser.swift */, + 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */, ); path = Objects; sourceTree = ""; @@ -1476,6 +1482,7 @@ 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DE24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507725B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7016ED3225C3BA2000038648 /* ParseUser+publisher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1588,6 +1595,7 @@ 70110D582506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DF24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507825B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7016ED3325C3BA2000038648 /* ParseUser+publisher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1746,6 +1754,7 @@ 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */, F97B461924D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507A25B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7016ED3525C3BA2000038648 /* ParseUser+publisher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1821,6 +1830,7 @@ 912C9BE024D302B0009947C3 /* Parse.swift in Sources */, F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507925B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7016ED3425C3BA2000038648 /* ParseUser+publisher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/ParseSwift/Objects/ParseUser+publisher.swift b/Sources/ParseSwift/Objects/ParseUser+publisher.swift new file mode 100644 index 000000000..4ba58fcaa --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseUser+publisher.swift @@ -0,0 +1,55 @@ +// +// ParseUser+publisher.swift +// ParseSwift +// +// Created by Corey Baker on 1/28/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseUser { + + /** + Signs up the user *asynchronously*. + + This will also enforce that the username isn't already taken. + + - warning: Make sure that password and username are set before calling this method. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: Future. + */ + static func signup(username: String, + password: String, + options: API.Options = []) -> Future { + Future { promise in + Self.signup(username: username, + password: password, + options: options, + completion: promise) + } + } + + static func login(username: String, + password: String, + options: API.Options = []) -> Future { + Future { promise in + Self.login(username: username, + password: password, + options: options, + completion: promise) + } + } + + func become(sessionToken: String, options: API.Options = []) -> Future { + Future { promise in + become(sessionToken: sessionToken, options: options, completion: promise) + } + } +} + +#endif From 8936f5d5104ab168bd3da2f8a0fda9383d0a1f3c Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 29 Jan 2021 17:36:38 -0500 Subject: [PATCH 02/15] Update --- .../Contents.swift | 2 +- ParseSwift.playground/contents.xcplayground | 8 +- ParseSwift.xcodeproj/project.pbxproj | 48 ++++- Sources/ParseSwift/API/API.swift | 4 +- .../Objects/ParseInstallation+combine.swift | 62 ++++++ .../Objects/ParseInstallation.swift | 14 +- .../Objects/ParseObject+combine.swift | 62 ++++++ Sources/ParseSwift/Objects/ParseObject.swift | 14 +- .../Objects/ParseUser+combine.swift | 179 ++++++++++++++++++ .../Objects/ParseUser+publisher.swift | 55 ------ Sources/ParseSwift/Objects/ParseUser.swift | 61 +++--- .../ParseUserCombineTests.swift | 143 ++++++++++++++ Tests/ParseSwiftTests/ParseUserTests.swift | 18 +- 13 files changed, 543 insertions(+), 127 deletions(-) create mode 100644 Sources/ParseSwift/Objects/ParseInstallation+combine.swift create mode 100644 Sources/ParseSwift/Objects/ParseObject+combine.swift create mode 100644 Sources/ParseSwift/Objects/ParseUser+combine.swift delete mode 100644 Sources/ParseSwift/Objects/ParseUser+publisher.swift create mode 100644 Tests/ParseSwiftTests/ParseUserCombineTests.swift diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 284427846..68a4f6372 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -103,7 +103,7 @@ User.current?.signup { result in //: Password Reset Request - synchronously. do { - try User.verificationEmailRequest(email: "hello@parse.org") + try User.verificationEmail(email: "hello@parse.org") print("Successfully requested verification email be sent") } catch let error { print("Error requesting verification email be sent: \(error)") diff --git a/ParseSwift.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground index 992108bbe..ab9557970 100644 --- a/ParseSwift.playground/contents.xcplayground +++ b/ParseSwift.playground/contents.xcplayground @@ -11,9 +11,9 @@ - - - + + + - + \ No newline at end of file diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 9fdcb72c3..c177fcdb1 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -75,10 +75,21 @@ 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D562506CE890091CC1D /* BaseParseInstallation.swift */; }; 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D562506CE890091CC1D /* BaseParseInstallation.swift */; }; 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */; }; - 7016ED3225C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; - 7016ED3325C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; - 7016ED3425C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; - 7016ED3525C3BA2000038648 /* ParseUser+publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */; }; + 7016ED3225C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED3325C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED3425C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED3525C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED4025C4A25A00038648 /* ParseUserCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */; }; + 7016ED4125C4A25A00038648 /* ParseUserCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */; }; + 7016ED4225C4A25A00038648 /* ParseUserCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */; }; + 7016ED5625C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED5725C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED5825C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED5925C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED6425C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; + 7016ED6525C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; + 7016ED6625C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; + 7016ED6725C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; 7033ECB325584A83009770F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB225584A83009770F3 /* AppDelegate.swift */; }; 7033ECB525584A83009770F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB425584A83009770F3 /* ViewController.swift */; }; 7033ECB825584A83009770F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB625584A83009770F3 /* Main.storyboard */; }; @@ -463,7 +474,10 @@ 70110D51250680140091CC1D /* ParseConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConstants.swift; sourceTree = ""; }; 70110D562506CE890091CC1D /* BaseParseInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseParseInstallation.swift; sourceTree = ""; }; 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationTests.swift; sourceTree = ""; }; - 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseUser+publisher.swift"; sourceTree = ""; }; + 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseUser+combine.swift"; sourceTree = ""; }; + 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseUserCombineTests.swift; sourceTree = ""; }; + 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseInstallation+combine.swift"; sourceTree = ""; }; + 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseObject+combine.swift"; sourceTree = ""; }; 7033ECB025584A83009770F3 /* TestHostTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHostTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7033ECB225584A83009770F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7033ECB425584A83009770F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -703,6 +717,7 @@ 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */, 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, + 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, 911DB12A24C3F7260027F3C7 /* NetworkMocking */, ); @@ -969,11 +984,13 @@ isa = PBXGroup; children = ( 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */, + 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */, F97B45C624D9C6F200F4A88B /* ParseObject.swift */, + 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */, 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */, 70C5503725B406B800B5DBC2 /* ParseSession.swift */, F97B45C424D9C6F200F4A88B /* ParseUser.swift */, - 7016ED3125C3BA2000038648 /* ParseUser+publisher.swift */, + 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */, ); path = Objects; sourceTree = ""; @@ -1461,6 +1478,7 @@ F97B45EA24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460224D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BA25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5625C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972A25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D125A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B45F624D9C6F200F4A88B /* ParseError.swift in Sources */, @@ -1475,6 +1493,7 @@ F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */, F97B465624D9C78C00F4A88B /* Remove.swift in Sources */, F97B45FA24D9C6F200F4A88B /* ParseACL.swift in Sources */, + 7016ED6425C4C46B00038648 /* ParseObject+combine.swift in Sources */, 70BDA2B3250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462724D9C72700F4A88B /* API.swift in Sources */, 70647E9C259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1482,7 +1501,7 @@ 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DE24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507725B49D3A00B5DBC2 /* ParseRole.swift in Sources */, - 7016ED3225C3BA2000038648 /* ParseUser+publisher.swift in Sources */, + 7016ED3225C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1496,6 +1515,7 @@ 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */, + 7016ED4025C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, 705727B12593FF8800F0ADD5 /* ParseFileTests.swift in Sources */, 70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */, 7003957625A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, @@ -1574,6 +1594,7 @@ F97B45EB24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460324D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BB25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5725C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972B25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D225A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B45F724D9C6F200F4A88B /* ParseError.swift in Sources */, @@ -1588,6 +1609,7 @@ F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */, F97B465724D9C78C00F4A88B /* Remove.swift in Sources */, F97B45FB24D9C6F200F4A88B /* ParseACL.swift in Sources */, + 7016ED6525C4C46B00038648 /* ParseObject+combine.swift in Sources */, 70BDA2B4250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462824D9C72700F4A88B /* API.swift in Sources */, 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1595,7 +1617,7 @@ 70110D582506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DF24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507825B49D3A00B5DBC2 /* ParseRole.swift in Sources */, - 7016ED3325C3BA2000038648 /* ParseUser+publisher.swift in Sources */, + 7016ED3325C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1618,6 +1640,7 @@ 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */, + 7016ED4225C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, 705727BC2593FF8C00F0ADD5 /* ParseFileTests.swift in Sources */, 709B984F2556ECAA00507778 /* AnyCodableTests.swift in Sources */, 7003957825A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, @@ -1655,6 +1678,7 @@ 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */, + 7016ED4125C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, 705727BB2593FF8B00F0ADD5 /* ParseFileTests.swift in Sources */, 70F2E2BD254F283000B2EA5C /* AnyDecodableTests.swift in Sources */, 7003957725A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, @@ -1733,6 +1757,7 @@ 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F924D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BD25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5925C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972D25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D425A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B460124D9C6F200F4A88B /* ParseFile.swift in Sources */, @@ -1747,6 +1772,7 @@ F97B463E24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B462A24D9C72700F4A88B /* API.swift in Sources */, F97B463224D9C74400F4A88B /* BatchUtils.swift in Sources */, + 7016ED6725C4C46B00038648 /* ParseObject+combine.swift in Sources */, F97B45F124D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D924D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1754,7 +1780,7 @@ 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */, F97B461924D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507A25B49D3A00B5DBC2 /* ParseRole.swift in Sources */, - 7016ED3525C3BA2000038648 /* ParseUser+publisher.swift in Sources */, + 7016ED3525C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1809,6 +1835,7 @@ 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F824D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BC25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5825C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972C25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D325A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B460024D9C6F200F4A88B /* ParseFile.swift in Sources */, @@ -1823,6 +1850,7 @@ F97B463D24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B462924D9C72700F4A88B /* API.swift in Sources */, F97B463124D9C74400F4A88B /* BatchUtils.swift in Sources */, + 7016ED6625C4C46B00038648 /* ParseObject+combine.swift in Sources */, F97B45F024D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D824D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1830,7 +1858,7 @@ 912C9BE024D302B0009947C3 /* Parse.swift in Sources */, F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507925B49D3A00B5DBC2 /* ParseRole.swift in Sources */, - 7016ED3425C3BA2000038648 /* ParseUser+publisher.swift in Sources */, + 7016ED3425C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index 34fec5f91..f6088e118 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -31,7 +31,7 @@ public struct API { case logout case file(fileName: String) case passwordReset - case verificationEmailRequest + case verificationEmail case functions(name: String) case jobs(name: String) case aggregate(className: String) @@ -70,7 +70,7 @@ public struct API { return "/files/\(fileName)" case .passwordReset: return "/requestPasswordReset" - case .verificationEmailRequest: + case .verificationEmail: return "/verificationEmailRequest" case .functions(name: let name): return "/functions/\(name)" diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift new file mode 100644 index 000000000..d45844cf3 --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -0,0 +1,62 @@ +// +// ParseInstallation+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseInstallation { + + /** + Fetches the `ParseUser` *aynchronously* with the current data from the server and sets an error if one occurs. + Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + fetch(options: options, + completion: promise) + } + } + + /** + Saves the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + It should have the following argument signature: `(Result)`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + save(options: options, + completion: promise) + } + } + + /** + Deletes the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + It should have the following argument signature: `(Result)`. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deletePublisher(email: String, options: API.Options = []) -> Future { + Future { promise in + delete(options: options, completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 412e5113e..eb40df916 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -525,13 +525,13 @@ extension ParseInstallation { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. - It should have the following argument signature: `(ParseError?)`. + It should have the following argument signature: `(Result)`. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ public func delete( options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void + completion: @escaping (Result) -> Void ) { do { try deleteCommand() @@ -539,21 +539,21 @@ extension ParseInstallation { callbackQueue.async { switch result { - case .success(let error): + case .success: Self.updateKeychainIfNeeded([self], deleting: true) - completion(error) + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } } catch let error as ParseError { callbackQueue.async { - completion(error) + completion(.failure(error)) } } catch { callbackQueue.async { - completion(ParseError(code: .unknownError, message: error.localizedDescription)) + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } } diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift new file mode 100644 index 000000000..85a34987f --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -0,0 +1,62 @@ +// +// ParseObject+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseObject { + + /** + Fetches the `ParseUser` *aynchronously* with the current data from the server and sets an error if one occurs. + Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + fetch(options: options, + completion: promise) + } + } + + /** + Saves the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + It should have the following argument signature: `(Result)`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + save(options: options, + completion: promise) + } + } + + /** + Deletes the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + It should have the following argument signature: `(Result)`. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deletePublisher(email: String, options: API.Options = []) -> Future { + Future { promise in + delete(options: options, completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 3df6eece9..93d421e05 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -715,32 +715,32 @@ extension ParseObject { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. - It should have the following argument signature: `(ParseError?)`. + It should have the following argument signature: `(Result)`. */ public func delete( options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void + completion: @escaping (Result) -> Void ) { do { try deleteCommand().executeAsync(options: options) { result in callbackQueue.async { switch result { - case .success(let error): - completion(error) + case .success: + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } } catch let error as ParseError { callbackQueue.async { - completion(error) + completion(.failure(error)) } } catch { callbackQueue.async { - completion(ParseError(code: .unknownError, message: error.localizedDescription)) + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } } diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift new file mode 100644 index 000000000..99031e315 --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -0,0 +1,179 @@ +// +// ParseUser+publisher.swift +// ParseSwift +// +// Created by Corey Baker on 1/28/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseUser { + + /** + Signs up the user *asynchronously* and publishes value. + + This will also enforce that the username isn't already taken. + + - warning: Make sure that password and username are set before calling this method. + - parameter username: The username of the user. + - parameter password: The password of the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func signupPublisher(username: String, + password: String, + options: API.Options = []) -> Future { + Future { promise in + Self.signup(username: username, + password: password, + options: options, + completion: promise) + } + } + + /** + Signs up the user *asynchronously* and publishes value. + + This will also enforce that the username isn't already taken. + + - warning: Make sure that password and username are set before calling this method. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func signupPublisher(options: API.Options = []) -> Future { + Future { promise in + signup(options: options, + completion: promise) + } + } + + /** + Makes an *asynchronous* request to log in a user with specified credentials. + Publishes an instance of the successfully logged in `ParseUser`. + + This also caches the user locally so that calls to *current* will use the latest logged in user. + - parameter username: The username of the user. + - parameter password: The password of the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func loginPublisher(username: String, + password: String, + options: API.Options = []) -> Future { + Future { promise in + Self.login(username: username, + password: password, + options: options, + completion: promise) + } + } + + /** + Logs in a `ParseUser` *asynchronously* with a session token. + Publishes an instance of the successfully logged in `ParseUser`. + If successful, this saves the session to the keychain, so you can retrieve the currently logged in user + using *current*. + + - parameter sessionToken: The sessionToken of the user to login. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func becomePublisher(sessionToken: String, options: API.Options = []) -> Future { + Future { promise in + become(sessionToken: sessionToken, options: options, completion: promise) + } + } + + /** + Logs out the currently logged in user *asynchronously*. Publishes when complete. + + This will also remove the session from the Keychain, log out of linked services + and all future calls to `current` will return `nil`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func logoutPublisher(options: API.Options = []) -> Future { + Future { promise in + Self.logout(options: options, completion: promise) + } + } + + /** + Requests *asynchronously* a password reset email to be sent to the specified email address + associated with the user account. This email allows the user to securely reset their password on the web. + Publishes when complete. + - parameter email: The email address associated with the user that forgot their password. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func passwordResetPublisher(email: String, options: API.Options = []) -> Future { + Future { promise in + Self.passwordReset(email: email, options: options, completion: promise) + } + } + + /** + Requests *asynchronously* a verification email be sent to the specified email address + associated with the user account. Publishes when complete. + - parameter email: The email address associated with the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func verificationEmailPublisher(email: String, + options: API.Options = []) -> Future { + Future { promise in + Self.verificationEmail(email: email, options: options, completion: promise) + } + } + + /** + Fetches the `ParseUser` *aynchronously* with the current data from the server and sets an error if one occurs. + Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + fetch(options: options, + completion: promise) + } + } + + /** + Saves the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + It should have the following argument signature: `(Result)`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + save(options: options, + completion: promise) + } + } + + /** + Deletes the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + It should have the following argument signature: `(Result)`. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deletePublisher(email: String, options: API.Options = []) -> Future { + Future { promise in + delete(options: options, completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Objects/ParseUser+publisher.swift b/Sources/ParseSwift/Objects/ParseUser+publisher.swift deleted file mode 100644 index 4ba58fcaa..000000000 --- a/Sources/ParseSwift/Objects/ParseUser+publisher.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// ParseUser+publisher.swift -// ParseSwift -// -// Created by Corey Baker on 1/28/21. -// Copyright © 2021 Parse Community. All rights reserved. -// - -#if !os(Linux) -import Foundation -import Combine - -// MARK: Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -public extension ParseUser { - - /** - Signs up the user *asynchronously*. - - This will also enforce that the username isn't already taken. - - - warning: Make sure that password and username are set before calling this method. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - returns: Future. - */ - static func signup(username: String, - password: String, - options: API.Options = []) -> Future { - Future { promise in - Self.signup(username: username, - password: password, - options: options, - completion: promise) - } - } - - static func login(username: String, - password: String, - options: API.Options = []) -> Future { - Future { promise in - Self.login(username: username, - password: password, - options: options, - completion: promise) - } - } - - func become(sessionToken: String, options: API.Options = []) -> Future { - Future { promise in - become(sessionToken: sessionToken, options: options, completion: promise) - } - } -} - -#endif diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 8a47102d5..0f6daa27b 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -235,7 +235,6 @@ extension ParseUser { value of .main. - parameter completion: The block to execute when completed. It should have the following argument signature: `(Result)`. - - important: If an object fetched has the same objectId as current, it will automatically update the current. */ public func become(sessionToken: String, options: API.Options = [], @@ -313,16 +312,15 @@ extension ParseUser { - parameter completion: A block that will be called when logging out, completes or fails. */ public static func logout(options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { - callbackQueue.async { - logoutCommand().executeAsync(options: options) { result in - + completion: @escaping (Result) -> Void) { + logoutCommand().executeAsync(options: options) { result in + callbackQueue.async { switch result { case .success: - completion(nil) + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } @@ -379,15 +377,15 @@ extension ParseUser { */ public static func passwordReset(email: String, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { + completion: @escaping (Result) -> Void) { passwordResetCommand(email: email).executeAsync(options: options) { result in callbackQueue.async { switch result { - case .success(let error): - completion(error) + case .success: + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } @@ -410,9 +408,9 @@ extension ParseUser { - parameter email: The email address associated with the user. - parameter options: A set of header options sent to the server. Defaults to an empty set. */ - public static func verificationEmailRequest(email: String, - options: API.Options = []) throws { - if let error = try verificationEmailRequestCommand(email: email).execute(options: options) { + public static func verificationEmail(email: String, + options: API.Options = []) throws { + if let error = try verificationEmailCommand(email: email).execute(options: options) { throw error } } @@ -425,30 +423,29 @@ extension ParseUser { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: A block that will be called when the verification request completes or fails. */ - public static func verificationEmailRequest(email: String, - options: API.Options = [], - callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { - verificationEmailRequestCommand(email: email) + public static func verificationEmail(email: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + verificationEmailCommand(email: email) .executeAsync(options: options) { result in callbackQueue.async { switch result { - case .success(let error): - completion(error) + case .success: + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } } - // swiftlint:disable:next line_length - internal static func verificationEmailRequestCommand(email: String) -> API.NonParseBodyCommand { + internal static func verificationEmailCommand(email: String) -> API.NonParseBodyCommand { let emailBody = EmailBody(email: email) return API.NonParseBodyCommand(method: .POST, - path: .verificationEmailRequest, + path: .verificationEmail, body: emailBody) { (data) -> ParseError? in try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) } @@ -838,36 +835,36 @@ extension ParseUser { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. - It should have the following argument signature: `(ParseError?)`. + It should have the following argument signature: `Result`. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ public func delete( options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void + completion: @escaping (Result) -> Void ) { do { try deleteCommand().executeAsync(options: options) { result in switch result { - case .success(let error): + case .success: callbackQueue.async { try? Self.updateKeychainIfNeeded([self], deleting: true) - completion(error) + completion(.success(())) } case .failure(let error): callbackQueue.async { - completion(error) + completion(.failure(error)) } } } } catch let error as ParseError { callbackQueue.async { - completion(error) + completion(.failure(error)) } } catch { callbackQueue.async { - completion(ParseError(code: .unknownError, message: error.localizedDescription)) + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } } diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift new file mode 100644 index 000000000..b04d62eb3 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -0,0 +1,143 @@ +// +// ParseUserCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + self.createdAt = Date() + self.updatedAt = Date() + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + let loginUserName = "hello10" + let loginPassword = "world" + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testSignin() { + let loginResponse = LoginSignupResponse() + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Signup user1") + let publisher = User.signupPublisher(username: loginUserName, password: loginUserName) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { signedUp in + XCTAssertNotNil(signedUp) + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNotNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.customKey) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNotNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.ACL) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index e7c690df0..4f2505cc6 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -1127,7 +1127,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testVerificationEmailRequestCommand() throws { let body = EmailBody(email: "hello@parse.org") - let command = User.verificationEmailRequestCommand(email: body.email) + let command = User.verificationEmailCommand(email: body.email) XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/verificationEmailRequest") XCTAssertEqual(command.method, API.Method.POST) @@ -1147,7 +1147,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } do { - try User.verificationEmailRequest(email: "hello@parse.org") + try User.verificationEmail(email: "hello@parse.org") } catch { XCTFail(error.localizedDescription) } @@ -1169,7 +1169,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } do { - try User.verificationEmailRequest(email: "hello@parse.org") + try User.verificationEmail(email: "hello@parse.org") XCTFail("Should have thrown ParseError") } catch { if let error = error as? ParseError { @@ -1180,10 +1180,10 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func verificationEmailRequestAsync(callbackQueue: DispatchQueue) { + func verificationEmailAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.verificationEmailRequest(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { error in guard let error = error else { expectation1.fulfill() @@ -1207,13 +1207,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - self.verificationEmailRequestAsync(callbackQueue: .main) + self.verificationEmailAsync(callbackQueue: .main) } - func verificationEmailRequestAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { + func verificationEmailAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.verificationEmailRequest(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { error in guard let error = error else { XCTFail("Should have thrown ParseError") @@ -1238,7 +1238,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - self.verificationEmailRequestAsyncError(parseError: parseError, callbackQueue: .main) + self.verificationEmailAsyncError(parseError: parseError, callbackQueue: .main) } func testUserCustomValuesNotSavedToKeychain() { From 901683cf43aff411de8d5c94bd11b273e645fa0b Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 29 Jan 2021 20:18:06 -0500 Subject: [PATCH 03/15] Update --- Sources/ParseSwift/Objects/ParseInstallation+combine.swift | 6 +++--- Sources/ParseSwift/Objects/ParseObject+combine.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index d45844cf3..dbd377b7e 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -15,7 +15,7 @@ import Combine public extension ParseInstallation { /** - Fetches the `ParseUser` *aynchronously* with the current data from the server and sets an error if one occurs. + Fetches the `ParseInstallation` *aynchronously* with the current data from the server and sets an error if one occurs. Publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. @@ -30,7 +30,7 @@ public extension ParseInstallation { } /** - Saves the `ParseUser` *asynchronously* and executes the given callback block. + Saves the `ParseInstallation` *asynchronously* and executes the given callback block. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -45,7 +45,7 @@ public extension ParseInstallation { } /** - Deletes the `ParseUser` *asynchronously* and executes the given callback block. + Deletes the `ParseInstallation` *asynchronously* and executes the given callback block. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index 85a34987f..9c2789911 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -15,7 +15,7 @@ import Combine public extension ParseObject { /** - Fetches the `ParseUser` *aynchronously* with the current data from the server and sets an error if one occurs. + Fetches the `ParseObject` *aynchronously* with the current data from the server and sets an error if one occurs. Publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. @@ -30,7 +30,7 @@ public extension ParseObject { } /** - Saves the `ParseUser` *asynchronously* and executes the given callback block. + Saves the `ParseObject` *asynchronously* and executes the given callback block. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -45,7 +45,7 @@ public extension ParseObject { } /** - Deletes the `ParseUser` *asynchronously* and executes the given callback block. + Deletes the `ParseObject` *asynchronously* and executes the given callback block. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. From 07610f9b055c24123e9adc02f7c613f0dd79baec Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 29 Jan 2021 23:34:10 -0500 Subject: [PATCH 04/15] Need test cases --- ParseSwift.xcodeproj/project.pbxproj | 50 +++++++ .../Objects/ParseInstallation+combine.swift | 8 +- .../Objects/ParseObject+combine.swift | 4 +- Sources/ParseSwift/Objects/ParseObject.swift | 8 +- .../Objects/ParseUser+combine.swift | 4 +- Sources/ParseSwift/Objects/ParseUser.swift | 74 +++++++---- .../Operations/ParseOperation+combine.swift | 31 +++++ .../ParseSwift/Types/ParseCloud+combine.swift | 44 +++++++ .../Types/ParseConfig+combine.swift | 42 ++++++ .../ParseSwift/Types/ParseFile+command.swift | 94 +++++++++++++ Sources/ParseSwift/Types/ParseFile.swift | 14 +- Sources/ParseSwift/Types/Query+combine.swift | 124 ++++++++++++++++++ Sources/ParseSwift/Types/Query.swift | 2 +- Tests/ParseSwiftTests/ParseFileTests.swift | 14 +- .../ParseInstallationTests.swift | 6 +- Tests/ParseSwiftTests/ParseObjectTests.swift | 34 ++--- Tests/ParseSwiftTests/ParseUserTests.swift | 68 +++++----- 17 files changed, 509 insertions(+), 112 deletions(-) create mode 100644 Sources/ParseSwift/Operations/ParseOperation+combine.swift create mode 100644 Sources/ParseSwift/Types/ParseCloud+combine.swift create mode 100644 Sources/ParseSwift/Types/ParseConfig+combine.swift create mode 100644 Sources/ParseSwift/Types/ParseFile+command.swift create mode 100644 Sources/ParseSwift/Types/Query+combine.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index c177fcdb1..d6cf35e4f 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -95,6 +95,26 @@ 7033ECB825584A83009770F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB625584A83009770F3 /* Main.storyboard */; }; 7033ECBA25584A85009770F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB925584A85009770F3 /* Assets.xcassets */; }; 7033ECBD25584A85009770F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECBB25584A85009770F3 /* LaunchScreen.storyboard */; }; + 7044C17525C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C17625C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C17725C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C17825C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C18325C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C18425C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C18525C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C18625C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C19125C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; + 7044C19225C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; + 7044C19325C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; + 7044C19425C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; + 7044C19F25C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1A025C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1A125C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1A225C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1AD25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1AE25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1AF25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1B025C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; @@ -485,6 +505,11 @@ 7033ECB925584A85009770F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7033ECBC25584A85009770F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7033ECBE25584A85009770F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseCloud+combine.swift"; sourceTree = ""; }; + 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseConfig+combine.swift"; sourceTree = ""; }; + 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseFile+command.swift"; sourceTree = ""; }; + 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseOperation+combine.swift"; sourceTree = ""; }; + 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+combine.swift"; sourceTree = ""; }; 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveQuerySocket.swift; sourceTree = ""; }; 70572670259033A700F0ADD5 /* ParseFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileManager.swift; sourceTree = ""; }; 705726DF2592C2A800F0ADD5 /* ParseHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseHash.swift; sourceTree = ""; }; @@ -969,13 +994,17 @@ children = ( F97B45C024D9C6F200F4A88B /* ParseACL.swift */, 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */, + 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */, 70D1BDB925BB17A600A42E7C /* ParseConfig.swift */, + 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */, F97B45BF24D9C6F200F4A88B /* ParseError.swift */, F97B45C124D9C6F200F4A88B /* ParseFile.swift */, + 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */, F97B45BC24D9C6F200F4A88B /* ParseGeoPoint.swift */, 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */, F97B45BE24D9C6F200F4A88B /* Pointer.swift */, F97B45BB24D9C6F200F4A88B /* Query.swift */, + 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */, ); path = Types; sourceTree = ""; @@ -1029,6 +1058,7 @@ F97B464124D9C78B00F4A88B /* Delete.swift */, F97B464524D9C78B00F4A88B /* Increment.swift */, F97B464024D9C78B00F4A88B /* ParseOperation.swift */, + 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */, F97B464424D9C78B00F4A88B /* Remove.swift */, 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */, ); @@ -1448,7 +1478,10 @@ 700395F225A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45F224D9C6F200F4A88B /* Pointer.swift in Sources */, 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19125C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C19F25C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B461E24D9C6F200F4A88B /* ParseStorage.swift in Sources */, + 7044C1AD25C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B45D224D9C6F200F4A88B /* AnyDecodable.swift in Sources */, 70C550A025B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463B24D9C74400F4A88B /* API+Commands.swift in Sources */, @@ -1460,6 +1493,7 @@ 700396F825A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B465A24D9C78C00F4A88B /* Increment.swift in Sources */, 7003960925A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17525C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45E224D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EA25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572671259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1501,6 +1535,7 @@ 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DE24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507725B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18325C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, 7016ED3225C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1564,7 +1599,10 @@ 700395F325A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45F324D9C6F200F4A88B /* Pointer.swift in Sources */, 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19225C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C1A025C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B461F24D9C6F200F4A88B /* ParseStorage.swift in Sources */, + 7044C1AE25C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B45D324D9C6F200F4A88B /* AnyDecodable.swift in Sources */, 70C550A125B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463C24D9C74400F4A88B /* API+Commands.swift in Sources */, @@ -1576,6 +1614,7 @@ 700396F925A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B465B24D9C78C00F4A88B /* Increment.swift in Sources */, 7003960A25A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17625C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45E324D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EB25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572672259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1617,6 +1656,7 @@ 70110D582506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DF24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507825B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18425C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, 7016ED3325C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1727,7 +1767,10 @@ 700395F525A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FD24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAF259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19425C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C1A225C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B465124D9C78C00F4A88B /* Add.swift in Sources */, + 7044C1B025C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B461124D9C6F200F4A88B /* ParseObject.swift in Sources */, 70C550A325B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460D24D9C6F200F4A88B /* Fetchable.swift in Sources */, @@ -1739,6 +1782,7 @@ 700396FB25A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B463A24D9C74400F4A88B /* Responses.swift in Sources */, 7003960C25A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17825C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45DD24D9C6F200F4A88B /* Extensions.swift in Sources */, 700396ED25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572674259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1780,6 +1824,7 @@ 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */, F97B461924D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507A25B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18625C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, 7016ED3525C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1805,7 +1850,10 @@ 700395F425A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FC24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19325C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C1A125C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B465024D9C78B00F4A88B /* Add.swift in Sources */, + 7044C1AF25C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B461024D9C6F200F4A88B /* ParseObject.swift in Sources */, 70C550A225B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460C24D9C6F200F4A88B /* Fetchable.swift in Sources */, @@ -1817,6 +1865,7 @@ 700396FA25A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B463924D9C74400F4A88B /* Responses.swift in Sources */, 7003960B25A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17725C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45DC24D9C6F200F4A88B /* Extensions.swift in Sources */, 700396EC25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572673259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1858,6 +1907,7 @@ 912C9BE024D302B0009947C3 /* Parse.swift in Sources */, F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507925B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18525C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, 7016ED3425C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index dbd377b7e..07b48695d 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -15,8 +15,8 @@ import Combine public extension ParseInstallation { /** - Fetches the `ParseInstallation` *aynchronously* with the current data from the server and sets an error if one occurs. - Publishes when complete. + Fetches the `ParseInstallation` *aynchronously* with the current data from the server + and sets an error if one occurs. Publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -34,7 +34,6 @@ public extension ParseInstallation { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. - It should have the following argument signature: `(Result)`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ func savePublisher(options: API.Options = []) -> Future { @@ -49,10 +48,9 @@ public extension ParseInstallation { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. - It should have the following argument signature: `(Result)`. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ - func deletePublisher(email: String, options: API.Options = []) -> Future { + func deletePublisher(options: API.Options = []) -> Future { Future { promise in delete(options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index 9c2789911..7f3b00982 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -34,7 +34,6 @@ public extension ParseObject { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. - It should have the following argument signature: `(Result)`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ func savePublisher(options: API.Options = []) -> Future { @@ -49,10 +48,9 @@ public extension ParseObject { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. - It should have the following argument signature: `(Result)`. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ - func deletePublisher(email: String, options: API.Options = []) -> Future { + func deletePublisher(options: API.Options = []) -> Future { Future { promise in delete(options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 93d421e05..d2331af0b 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -727,8 +727,12 @@ extension ParseObject { callbackQueue.async { switch result { - case .success: - completion(.success(())) + case .success(let error): + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } case .failure(let error): completion(.failure(error)) } diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 99031e315..4ba146e4f 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -151,7 +151,6 @@ public extension ParseUser { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. - It should have the following argument signature: `(Result)`. - important: If an object saved has the same objectId as current, it will automatically update the current. */ func savePublisher(options: API.Options = []) -> Future { @@ -166,10 +165,9 @@ public extension ParseUser { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. - It should have the following argument signature: `(Result)`. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ - func deletePublisher(email: String, options: API.Options = []) -> Future { + func deletePublisher(options: API.Options = []) -> Future { Future { promise in delete(options: options, completion: promise) } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 0f6daa27b..91a4aefde 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -298,7 +298,15 @@ extension ParseUser { Logs out the currently logged in user in Keychain *synchronously*. */ public static func logout(options: API.Options = []) throws { - _ = try logoutCommand().execute(options: options) + let error = try? logoutCommand().execute(options: options) + //Always let user logout locally, no matter the error. + deleteCurrentContainerFromKeychain() + BaseParseInstallation.deleteCurrentContainerFromKeychain() + BaseConfig.deleteCurrentContainerFromKeychain() + //Wait to throw error + if let parseError = error { + throw parseError + } } /** @@ -315,10 +323,20 @@ extension ParseUser { completion: @escaping (Result) -> Void) { logoutCommand().executeAsync(options: options) { result in callbackQueue.async { + + //Always let user logout locally, no matter the error. + deleteCurrentContainerFromKeychain() + BaseParseInstallation.deleteCurrentContainerFromKeychain() + BaseConfig.deleteCurrentContainerFromKeychain() + switch result { - case .success: - completion(.success(())) + case .success(let error): + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } case .failure(let error): completion(.failure(error)) } @@ -326,28 +344,14 @@ extension ParseUser { } } - internal static func logoutCommand() -> API.NonParseBodyCommand { - return API.NonParseBodyCommand(method: .POST, path: .logout) { (data) -> NoBody in - var parseError: ParseError? - var serverResponse = NoBody() - //Always let user logout locally, no matter the error. - deleteCurrentContainerFromKeychain() - BaseParseInstallation.deleteCurrentContainerFromKeychain() - BaseConfig.deleteCurrentContainerFromKeychain() - + internal static func logoutCommand() -> API.NonParseBodyCommand { + return API.NonParseBodyCommand(method: .POST, path: .logout) { (data) -> ParseError? in do { - serverResponse = try ParseCoding.jsonDecoder().decode(NoBody.self, from: data) + let parseError = try ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + throw parseError } catch { - if let foundError = error as? ParseError { - parseError = foundError - } else { - parseError = ParseError(code: .unknownError, message: error.localizedDescription) - } - } - guard let error = parseError else { - return serverResponse + return nil } - throw error } } } @@ -382,8 +386,12 @@ extension ParseUser { callbackQueue.async { switch result { - case .success: - completion(.success(())) + case .success(let error): + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } case .failure(let error): completion(.failure(error)) } @@ -433,8 +441,12 @@ extension ParseUser { switch result { - case .success: - completion(.success(())) + case .success(let error): + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } case .failure(let error): completion(.failure(error)) } @@ -835,7 +847,7 @@ extension ParseUser { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. - It should have the following argument signature: `Result`. + It should have the following argument signature: `(Result)`. - important: If an object deleted has the same objectId as current, it will automatically update the current. */ public func delete( @@ -847,10 +859,14 @@ extension ParseUser { try deleteCommand().executeAsync(options: options) { result in switch result { - case .success: + case .success(let error): callbackQueue.async { try? Self.updateKeychainIfNeeded([self], deleting: true) - completion(.success(())) + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } } case .failure(let error): callbackQueue.async { diff --git a/Sources/ParseSwift/Operations/ParseOperation+combine.swift b/Sources/ParseSwift/Operations/ParseOperation+combine.swift new file mode 100644 index 000000000..3c3d24c83 --- /dev/null +++ b/Sources/ParseSwift/Operations/ParseOperation+combine.swift @@ -0,0 +1,31 @@ +// +// ParseOperation+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseOperation { + + /** + Saves the operations on the `ParseObject` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + save(options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseCloud+combine.swift b/Sources/ParseSwift/Types/ParseCloud+combine.swift new file mode 100644 index 000000000..42df6a860 --- /dev/null +++ b/Sources/ParseSwift/Types/ParseCloud+combine.swift @@ -0,0 +1,44 @@ +// +// ParseCloud+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseCloud { + + /** + Calls a Cloud Code function *asynchronously* and returns a result of it's execution. + Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func runFunctionPublisher(options: API.Options = []) -> Future { + Future { promise in + runFunction(options: options, + completion: promise) + } + } + + /** + Starts a Cloud Code job *asynchronously* and returns a result with the jobStatusId of the job. + Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func startJobPublisher(options: API.Options = []) -> Future { + Future { promise in + startJob(options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseConfig+combine.swift b/Sources/ParseSwift/Types/ParseConfig+combine.swift new file mode 100644 index 000000000..c8a4bc4ad --- /dev/null +++ b/Sources/ParseSwift/Types/ParseConfig+combine.swift @@ -0,0 +1,42 @@ +// +// ParseConfig+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseConfig { + + /** + Fetch the Config *asynchronously*. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + fetch(options: options, + completion: promise) + } + } + + /** + Update the Config *asynchronously*. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + save(options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseFile+command.swift b/Sources/ParseSwift/Types/ParseFile+command.swift new file mode 100644 index 000000000..d9ba76617 --- /dev/null +++ b/Sources/ParseSwift/Types/ParseFile+command.swift @@ -0,0 +1,94 @@ +// +// ParseFile+command.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseFile { + + /** + Fetches a file with given url *synchronously*. Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + fetch(options: options, + completion: promise) + } + } + + /** + Fetches a file with given url *synchronously*. Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter progress: A block that will be called when file updates it's progress. + It should have the following argument signature: `(task: URLSessionDownloadTask, + bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func fetchPublisher(options: API.Options = [], + progress: @escaping ((URLSessionDownloadTask, + Int64, Int64, Int64) -> Void)) -> Future { + Future { promise in + fetch(options: options, + progress: progress, + completion: promise) + } + } + + /** + Creates a file with given data *asynchronously* and executes the given callback block. + Publishes when complete. + A name will be assigned to it by the server. If the file hasn't been downloaded, it will automatically + be downloaded before saved. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + save(options: options, + completion: promise) + } + } + + /** + Creates a file with given data *asynchronously* and executes the given callback block. + A name will be assigned to it by the server. If the file hasn't been downloaded, it will automatically + be downloaded before saved. Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter progress: A block that will be called when file updates it's progress. + It should have the following argument signature: `(task: URLSessionDownloadTask, + bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = [], + progress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil) -> Future { + Future { promise in + save(options: options, + progress: progress, + completion: promise) + } + } + + /** + Deletes the file from the Parse cloud. Publishes when complete. + - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func deletePublisher(options: API.Options = []) -> Future { + Future { promise in + delete(options: options, completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift index 8d56bf9e1..6679eaba0 100644 --- a/Sources/ParseSwift/Types/ParseFile.swift +++ b/Sources/ParseSwift/Types/ParseFile.swift @@ -179,19 +179,19 @@ extension ParseFile { - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: A block that will be called when file deletes or fails. - It should have the following argument signature: `(ParseError?)` + It should have the following argument signature: `(Result)` */ public func delete(options: API.Options, callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { + completion: @escaping (Result) -> Void) { var options = options options = options.union(self.options) if !options.contains(.useMasterKey) { callbackQueue.async { - completion(ParseError(code: .unknownError, + completion(.failure(ParseError(code: .unknownError, // swiftlint:disable:next line_length - message: "You must specify \"useMasterKey\" in \"options\" in order to delete a file.")) + message: "You must specify \"useMasterKey\" in \"options\" in order to delete a file."))) } return } @@ -200,9 +200,9 @@ extension ParseFile { switch result { case .success: - completion(nil) + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } @@ -413,7 +413,7 @@ extension ParseFile { It should have the following argument signature: `(task: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. - parameter completion: A block that will be called when file saves or fails. - It should have the following argument signature: `(Result)` + It should have the following argument signature: `(Result)`. */ public func save(options: API.Options = [], callbackQueue: DispatchQueue = .main, diff --git a/Sources/ParseSwift/Types/Query+combine.swift b/Sources/ParseSwift/Types/Query+combine.swift new file mode 100644 index 000000000..1e02f5510 --- /dev/null +++ b/Sources/ParseSwift/Types/Query+combine.swift @@ -0,0 +1,124 @@ +// +// Query+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Query { + + /** + Finds objects *asynchronously* and publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func findPublisher(options: API.Options = []) -> Future<[ResultType], ParseError> { + Future { promise in + find(options: options, + completion: promise) + } + } + + /** + Finds objects *asynchronously* and publishes when complete. + - parameter explain: Used to toggle the information on the query plan. + - parameter hint: String or Object of index that should be used when executing query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func findPublisher(explain: Bool, + hint: String? = nil, + options: API.Options = []) -> Future { + Future { promise in + find(explain: explain, + hint: hint, + options: options, + completion: promise) + } + } + + /** + Gets an object *asynchronously* and publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func firstPublisher(options: API.Options = []) -> Future { + Future { promise in + first(options: options, + completion: promise) + } + } + + /** + Gets an object *asynchronously* and publishes when complete. + - parameter explain: Used to toggle the information on the query plan. + - parameter hint: String or Object of index that should be used when executing query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func firstPublisher(explain: Bool, + hint: String? = nil, + options: API.Options = []) -> Future { + Future { promise in + first(explain: explain, + hint: hint, + options: options, + completion: promise) + } + } + + /** + Count objects *asynchronously* and publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func countPublisher(options: API.Options = []) -> Future { + Future { promise in + count(options: options, + completion: promise) + } + } + + /** + Count objects *asynchronously* and publishes when complete. + - parameter explain: Used to toggle the information on the query plan. + - parameter hint: String or Object of index that should be used when executing query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func countPublisher(explain: Bool, + hint: String? = nil, + options: API.Options = []) -> Future { + Future { promise in + count(explain: explain, + hint: hint, + options: options, + completion: promise) + } + } + + /** + Executes an aggregate query *asynchronously* and publishes when complete. + - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. + - parameter pipeline: A pipeline of stages to process query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func aggregatePublisher(_ pipeline: AggregateType, + options: API.Options = []) -> Future<[ResultType], ParseError> { + Future { promise in + aggregate(pipeline, + options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 219ff4757..cfd2c8cc2 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -872,7 +872,7 @@ extension Query: Queryable { - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. - It should have the following argument signature: `(Result<[AnyResultType], ParseError>)`. + It should have the following argument signature: `(Result)`. */ public func find(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, diff --git a/Tests/ParseSwiftTests/ParseFileTests.swift b/Tests/ParseSwiftTests/ParseFileTests.swift index dfe60ed60..51d271894 100644 --- a/Tests/ParseSwiftTests/ParseFileTests.swift +++ b/Tests/ParseSwiftTests/ParseFileTests.swift @@ -1068,13 +1068,11 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } let expectation1 = XCTestExpectation(description: "ParseFile async") - parseFile.delete(options: [.useMasterKey]) { error in + parseFile.delete(options: [.useMasterKey]) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 20.0) @@ -1104,12 +1102,10 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } let expectation1 = XCTestExpectation(description: "ParseFile async") - parseFile.delete(options: [.removeMimeType]) { error in + parseFile.delete(options: [.removeMimeType]) { result in - guard error != nil else { + if case .success = result { XCTFail("Should have thrown error") - expectation1.fulfill() - return } expectation1.fulfill() } diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index 53380d9e2..290e91a06 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -690,8 +690,10 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - installation.delete { error in - XCTAssertNil(error) + installation.delete { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } expectation1.fulfill() } } diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 5062e7e77..584b6ca78 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -947,24 +947,20 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length func deleteAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Delete object1") - score.delete(options: [], callbackQueue: callbackQueue) { error in + score.delete(options: [], callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } let expectation2 = XCTestExpectation(description: "Delete object2") - score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { error in + score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation2.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation2.fulfill() } wait(for: [expectation1, expectation2], timeout: 20.0) @@ -1027,26 +1023,24 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length func deleteAsyncError(score: GameScore, parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Delete object1") - score.delete(options: [], callbackQueue: callbackQueue) { error in + score.delete(options: [], callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation1.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation1.fulfill() } let expectation2 = XCTestExpectation(description: "Delete object2") - score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { error in + score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation2.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation2.fulfill() } wait(for: [expectation1, expectation2], timeout: 20.0) diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 4f2505cc6..da4b85652 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -75,13 +75,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length testing: true) } - override func tearDown() { + override func tearDownWithError() throws { super.tearDown() MockURLProtocol.removeAll() #if !os(Linux) - try? KeychainStore.shared.deleteAll() + try KeychainStore.shared.deleteAll() #endif - try? ParseStorage.shared.deleteAll() + try ParseStorage.shared.deleteAll() } func testFetchCommand() { @@ -972,20 +972,30 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func logoutAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.logout(callbackQueue: callbackQueue) { error in + User.logout(callbackQueue: callbackQueue) { result in - guard let error = error else { + switch result { + + case .success: if let userFromKeychain = BaseParseUser.current { XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") } - if let installationFromKeychain = BaseParseInstallation.current { + if let installationFromMemory: CurrentInstallationContainer + = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") + } + + #if !os(Linux) + if let installationFromKeychain: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") } - expectation1.fulfill() - return + #endif + + case .failure(let error): + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 20.0) @@ -1067,13 +1077,11 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func passwordResetAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1097,14 +1105,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func passwordResetAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation1.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1183,13 +1190,11 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func verificationEmailAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1213,14 +1218,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func verificationEmailAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation1.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1330,8 +1334,10 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - user.delete { error in - XCTAssertNil(error) + user.delete { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } expectation1.fulfill() } } From be9e1a427a8c55fd323cfc9b4521ad4e97d44da6 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 01:53:17 -0500 Subject: [PATCH 05/15] updates --- ParseSwift.xcodeproj/project.pbxproj | 8 + .../Objects/ParseInstallation+combine.swift | 2 +- Sources/ParseSwift/Objects/ParseUser.swift | 2 +- .../ParseUserCombineTests.swift | 490 +++++++++++++++++- Tests/ParseSwiftTests/ParseUserTests.swift | 1 - 5 files changed, 499 insertions(+), 4 deletions(-) diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index d6cf35e4f..1499738e2 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -115,6 +115,9 @@ 7044C1AE25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; 7044C1AF25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; 7044C1B025C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1BB25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; + 7044C1BC25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; + 7044C1BD25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; @@ -510,6 +513,7 @@ 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseFile+command.swift"; sourceTree = ""; }; 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseOperation+combine.swift"; sourceTree = ""; }; 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+combine.swift"; sourceTree = ""; }; + 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationCombineTests.swift; sourceTree = ""; }; 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveQuerySocket.swift; sourceTree = ""; }; 70572670259033A700F0ADD5 /* ParseFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileManager.swift; sourceTree = ""; }; 705726DF2592C2A800F0ADD5 /* ParseHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseHash.swift; sourceTree = ""; }; @@ -732,6 +736,7 @@ 705727882593FF8000F0ADD5 /* ParseFileTests.swift */, 70BC0B32251903D1001556DB /* ParseGeoPointTests.swift */, 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */, + 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */, 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, @@ -1563,6 +1568,7 @@ 7FFF552E2217E72A007C3B4E /* AnyEncodableTests.swift in Sources */, 7FFF55302217E72A007C3B4E /* AnyDecodableTests.swift in Sources */, 70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */, + 7044C1BB25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 7FFF552F2217E72A007C3B4E /* AnyCodableTests.swift in Sources */, 4AA807701F794C31008CD551 /* KeychainStoreTests.swift in Sources */, 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, @@ -1693,6 +1699,7 @@ 709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */, 709B98522556ECAA00507778 /* ParseUserTests.swift in Sources */, 709B984E2556ECAA00507778 /* ParseGeoPointTests.swift in Sources */, + 7044C1BD25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 709B984B2556ECAA00507778 /* MockURLProtocol.swift in Sources */, 709B98552556ECAA00507778 /* ParseQueryTests.swift in Sources */, 70C5502425B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, @@ -1731,6 +1738,7 @@ 70F2E2C1254F283000B2EA5C /* AnyCodableTests.swift in Sources */, 70F2E2B3254F283000B2EA5C /* ParseUserTests.swift in Sources */, 70F2E2C0254F283000B2EA5C /* MockURLResponse.swift in Sources */, + 7044C1BC25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 70F2E2BE254F283000B2EA5C /* ParseObjectBatchTests.swift in Sources */, 70F2E2BF254F283000B2EA5C /* MockURLProtocol.swift in Sources */, 70C5502325B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 07b48695d..6293c3ad9 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -39,7 +39,7 @@ public extension ParseInstallation { func savePublisher(options: API.Options = []) -> Future { Future { promise in save(options: options, - completion: promise) + completion: promise) } } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 91a4aefde..e6c6a3ab1 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -348,7 +348,7 @@ extension ParseUser { return API.NonParseBodyCommand(method: .POST, path: .logout) { (data) -> ParseError? in do { let parseError = try ParseCoding.jsonDecoder().decode(ParseError.self, from: data) - throw parseError + return parseError } catch { return nil } diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index b04d62eb3..07c09bc1a 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -88,7 +88,7 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le try ParseStorage.shared.deleteAll() } - func testSignin() { + func testSignup() { let loginResponse = LoginSignupResponse() var subscriptions = Set() MockURLProtocol.mockRequests { _ in @@ -138,6 +138,494 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le publisher.store(in: &subscriptions) wait(for: [expectation1], timeout: 20.0) } + + func testLogin() { + let loginResponse = LoginSignupResponse() + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Login user1") + let publisher = User.loginPublisher(username: loginUserName, password: loginUserName) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { signedUp in + XCTAssertNotNil(signedUp) + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNotNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.customKey) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNotNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.ACL) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func login() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + _ = try User.login(username: loginUserName, password: loginPassword) + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBecome() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.sessionToken = "newValue" + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.becomePublisher(sessionToken: serverResponse.sessionToken) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { signedUp in + XCTAssertNotNil(signedUp) + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNotNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.customKey) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNotNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.ACL) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testLogout() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Logout user1") + let publisher = User.logoutPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + if let userFromKeychain = BaseParseUser.current { + XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") + } + + if let installationFromMemory: CurrentInstallationContainer + = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") + } + + #if !os(Linux) + if let installationFromKeychain: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") + } + #endif + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testLogoutError() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + let serverResponse = ParseError(code: .internalServer, message: "Object not found") + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Logout user1") + let publisher = User.logoutPublisher() + .sink(receiveCompletion: { result in + + if case .finished = result { + XCTFail("Should have thrown ParseError") + } + + if let userFromKeychain = BaseParseUser.current { + XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") + } + + if let installationFromMemory: CurrentInstallationContainer + = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") + } + + #if !os(Linux) + if let installationFromKeychain: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") + } + #endif + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have thrown ParseError") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testPasswordReset() { + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Password user1") + let publisher = User.passwordResetPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testPasswordResetError() { + let parseError = ParseError(code: .internalServer, message: "Object not found") + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Password user1") + let publisher = User.passwordResetPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case .finished = result { + XCTFail("Should have thrown ParseError") + } + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have thrown ParseError") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testVerificationEmail() { + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Verification user1") + let publisher = User.verificationEmailPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testVerificationEmailError() { + let parseError = ParseError(code: .internalServer, message: "Object not found") + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Verification user1") + let publisher = User.verificationEmailPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case .finished = result { + XCTFail("Should have thrown ParseError") + } + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have thrown ParseError") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testFetch() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.sessionToken = "newValue" + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.objectId, serverResponse.objectId) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertEqual(userFromKeychain.objectId, serverResponse.objectId) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.sessionToken = "newValue" + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssertEqual(saved.objectId, serverResponse.objectId) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertEqual(userFromKeychain.objectId, serverResponse.objectId) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.deletePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + if BaseParseUser.current != nil { + XCTFail("Couldn't get CurrentUser from Keychain") + } + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } } #endif diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index da4b85652..35802bc54 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -1822,7 +1822,6 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) serverResponse.sessionToken = "newValue" serverResponse.username = "stop" - serverResponse.password = "this" var userOnServer: User! From 06ccc80f84d2dc3ccaa67fd8e50c795171094c9f Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 10:27:50 -0500 Subject: [PATCH 06/15] Update --- .../Protocols/ParseAuthentication.swift | 6 + .../Objects/ParseInstallation+combine.swift | 3 + .../Objects/ParseUser+combine.swift | 10 +- .../Operations/ParseOperation+combine.swift | 3 +- .../ParseSwift/Types/ParseCloud+combine.swift | 4 + .../Types/ParseConfig+combine.swift | 4 + Sources/ParseSwift/Types/Query+combine.swift | 2 + .../ParseInstallationCombineTests.swift | 315 ++++++++++++++++++ 8 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 Tests/ParseSwiftTests/ParseInstallationCombineTests.swift diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index 412d17734..eaac12f6f 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -327,3 +327,9 @@ public extension ParseUser { } } } + +public extension ParseUser { + + // MARK: 3rd Party Authentication - Login Combine + +} diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 6293c3ad9..750b09c0d 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -14,6 +14,7 @@ import Combine @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseInstallation { + // MARK: Fetchable - Combine /** Fetches the `ParseInstallation` *aynchronously* with the current data from the server and sets an error if one occurs. Publishes when complete. @@ -29,6 +30,7 @@ public extension ParseInstallation { } } + // MARK: Savable - Combine /** Saves the `ParseInstallation` *asynchronously* and executes the given callback block. @@ -43,6 +45,7 @@ public extension ParseInstallation { } } + // MARK: Deletable - Combine /** Deletes the `ParseInstallation` *asynchronously* and executes the given callback block. diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 4ba146e4f..1043c8d7d 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -10,10 +10,11 @@ import Foundation import Combine -// MARK: Combine + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseUser { + // MARK: Signing Out - Combine /** Signs up the user *asynchronously* and publishes value. @@ -52,6 +53,7 @@ public extension ParseUser { } } + // MARK: Logging In - Combine /** Makes an *asynchronous* request to log in a user with specified credentials. Publishes an instance of the successfully logged in `ParseUser`. @@ -89,6 +91,7 @@ public extension ParseUser { } } + // MARK: Logging Out - Combine /** Logs out the currently logged in user *asynchronously*. Publishes when complete. @@ -103,6 +106,7 @@ public extension ParseUser { } } + // MARK: Password Reset - Combine /** Requests *asynchronously* a password reset email to be sent to the specified email address associated with the user account. This email allows the user to securely reset their password on the web. @@ -117,6 +121,7 @@ public extension ParseUser { } } + // MARK: Verification Email Request - Combine /** Requests *asynchronously* a verification email be sent to the specified email address associated with the user account. Publishes when complete. @@ -131,6 +136,7 @@ public extension ParseUser { } } + // MARK: Fetchable - Combine /** Fetches the `ParseUser` *aynchronously* with the current data from the server and sets an error if one occurs. Publishes when complete. @@ -146,6 +152,7 @@ public extension ParseUser { } } + // MARK: Savable - Combine /** Saves the `ParseUser` *asynchronously* and executes the given callback block. @@ -160,6 +167,7 @@ public extension ParseUser { } } + // MARK: Deletable - Combine /** Deletes the `ParseUser` *asynchronously* and executes the given callback block. diff --git a/Sources/ParseSwift/Operations/ParseOperation+combine.swift b/Sources/ParseSwift/Operations/ParseOperation+combine.swift index 3c3d24c83..474c003bb 100644 --- a/Sources/ParseSwift/Operations/ParseOperation+combine.swift +++ b/Sources/ParseSwift/Operations/ParseOperation+combine.swift @@ -10,10 +10,11 @@ import Foundation import Combine -// MARK: Combine @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseOperation { + // MARK: Savable - Combine + /** Saves the operations on the `ParseObject` *asynchronously* and executes the given callback block. diff --git a/Sources/ParseSwift/Types/ParseCloud+combine.swift b/Sources/ParseSwift/Types/ParseCloud+combine.swift index 42df6a860..417de1f73 100644 --- a/Sources/ParseSwift/Types/ParseCloud+combine.swift +++ b/Sources/ParseSwift/Types/ParseCloud+combine.swift @@ -14,6 +14,8 @@ import Combine @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseCloud { + // MARK: Functions - Combine + /** Calls a Cloud Code function *asynchronously* and returns a result of it's execution. Publishes when complete. @@ -27,6 +29,8 @@ public extension ParseCloud { } } + // MARK: Jobs - Combine + /** Starts a Cloud Code job *asynchronously* and returns a result with the jobStatusId of the job. Publishes when complete. diff --git a/Sources/ParseSwift/Types/ParseConfig+combine.swift b/Sources/ParseSwift/Types/ParseConfig+combine.swift index c8a4bc4ad..73c2fc1fe 100644 --- a/Sources/ParseSwift/Types/ParseConfig+combine.swift +++ b/Sources/ParseSwift/Types/ParseConfig+combine.swift @@ -14,6 +14,8 @@ import Combine @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseConfig { + // MARK: Fetchable - Combine + /** Fetch the Config *asynchronously*. - parameter options: A set of header options sent to the server. Defaults to an empty set. @@ -26,6 +28,8 @@ public extension ParseConfig { } } + // MARK: Savable - Combine + /** Update the Config *asynchronously*. - parameter options: A set of header options sent to the server. Defaults to an empty set. diff --git a/Sources/ParseSwift/Types/Query+combine.swift b/Sources/ParseSwift/Types/Query+combine.swift index 1e02f5510..2bf782b42 100644 --- a/Sources/ParseSwift/Types/Query+combine.swift +++ b/Sources/ParseSwift/Types/Query+combine.swift @@ -14,6 +14,8 @@ import Combine @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { + // MARK: Queryable - Combine + /** Finds objects *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift new file mode 100644 index 000000000..de3020cb3 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -0,0 +1,315 @@ +// +// ParseInstallationCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + self.createdAt = Date() + self.updatedAt = Date() + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + struct Installation: ParseInstallation { + var installationId: String? + var deviceType: String? + var deviceToken: String? + var badge: Int? + var timeZone: String? + var channels: [String]? + var appName: String? + var appIdentifier: String? + var appVersion: String? + var parseVersion: String? + var localeIdentifier: String? + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var customKey: String? + } + + let testInstallationObjectId = "yarr" + + let loginUserName = "hello10" + let loginPassword = "world" + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + login() + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func login() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + _ = try User.login(username: loginUserName, password: loginPassword) + + } catch { + XCTFail(error.localizedDescription) + } + } + + func update() { + var installation = Installation() + installation.objectId = testInstallationObjectId + installation.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + do { + let saved = try installation.save() + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + } catch { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testFetch() { + update() + MockURLProtocol.removeAll() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var serverResponse = installation + serverResponse.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + serverResponse.customKey = "newValue" + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let publisher = installation.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssert(fetched.hasSameObjectId(as: serverResponse)) + XCTAssertEqual(Installation.current?.customKey, serverResponse.customKey) + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + update() + MockURLProtocol.removeAll() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + guard var installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + installation.customKey = "newValue" + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var serverResponse = installation + serverResponse.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let publisher = installation.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssert(fetched.hasSameObjectId(as: serverResponse)) + XCTAssertEqual(Installation.current?.customKey, serverResponse.customKey) + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() { + update() + MockURLProtocol.removeAll() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var serverResponse = installation + serverResponse.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + serverResponse.customKey = "newValue" + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let publisher = installation.deletePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif From 7fceac7a15588682412c67a5bbf6e308edc54888 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 11:35:31 -0500 Subject: [PATCH 07/15] Update authentication --- ParseSwift.xcodeproj/project.pbxproj | 30 ++++-- .../Authentication/3rd Party/ParseApple.swift | 73 ++++++++++++++ .../Internal/ParseAnonymous.swift | 27 ++++++ .../ParseAuthentication+combine.swift | 97 +++++++++++++++++++ .../Protocols/ParseAuthentication.swift | 82 +++++++++++++--- .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 2 +- .../ParseSwift/LiveQuery/Subscription.swift | 2 +- .../Objects/ParseInstallation+combine.swift | 2 +- .../Objects/ParseObject+combine.swift | 2 +- .../Objects/ParseUser+combine.swift | 3 +- .../Operations/ParseOperation+combine.swift | 2 +- .../ParseSwift/Types/ParseCloud+combine.swift | 2 +- .../Types/ParseConfig+combine.swift | 2 +- ...+command.swift => ParseFile+combine.swift} | 4 +- Sources/ParseSwift/Types/Query+combine.swift | 2 +- .../ParseInstallationCombineTests.swift | 2 +- .../ParseUserCombineTests.swift | 2 +- 17 files changed, 300 insertions(+), 36 deletions(-) create mode 100644 Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift rename Sources/ParseSwift/Types/{ParseFile+command.swift => ParseFile+combine.swift} (98%) diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 1499738e2..554f5f800 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -103,10 +103,10 @@ 7044C18425C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; 7044C18525C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; 7044C18625C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; - 7044C19125C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; - 7044C19225C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; - 7044C19325C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; - 7044C19425C4F5B60011F6E7 /* ParseFile+command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */; }; + 7044C19125C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; + 7044C19225C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; + 7044C19325C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; + 7044C19425C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; 7044C19F25C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; 7044C1A025C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; 7044C1A125C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; @@ -118,6 +118,10 @@ 7044C1BB25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; 7044C1BC25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; 7044C1BD25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; + 7044C1C825C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1C925C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1CA25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1CB25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; @@ -510,10 +514,11 @@ 7033ECBE25584A85009770F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseCloud+combine.swift"; sourceTree = ""; }; 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseConfig+combine.swift"; sourceTree = ""; }; - 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseFile+command.swift"; sourceTree = ""; }; + 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseFile+combine.swift"; sourceTree = ""; }; 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseOperation+combine.swift"; sourceTree = ""; }; 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+combine.swift"; sourceTree = ""; }; 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationCombineTests.swift; sourceTree = ""; }; + 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseAuthentication+combine.swift"; sourceTree = ""; }; 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveQuerySocket.swift; sourceTree = ""; }; 70572670259033A700F0ADD5 /* ParseFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileManager.swift; sourceTree = ""; }; 705726DF2592C2A800F0ADD5 /* ParseHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseHash.swift; sourceTree = ""; }; @@ -900,6 +905,7 @@ isa = PBXGroup; children = ( 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */, + 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */, ); path = Protocols; sourceTree = ""; @@ -1004,7 +1010,7 @@ 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */, F97B45BF24D9C6F200F4A88B /* ParseError.swift */, F97B45C124D9C6F200F4A88B /* ParseFile.swift */, - 7044C19025C4F5B60011F6E7 /* ParseFile+command.swift */, + 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */, F97B45BC24D9C6F200F4A88B /* ParseGeoPoint.swift */, 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */, F97B45BE24D9C6F200F4A88B /* Pointer.swift */, @@ -1471,6 +1477,7 @@ F97B461624D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DA24D9C6F200F4A88B /* Extensions.swift in Sources */, 70C5503825B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1C825C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF125B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7325BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */, @@ -1483,7 +1490,7 @@ 700395F225A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45F224D9C6F200F4A88B /* Pointer.swift in Sources */, 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, - 7044C19125C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C19125C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, 7044C19F25C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B461E24D9C6F200F4A88B /* ParseStorage.swift in Sources */, 7044C1AD25C4FC080011F6E7 /* Query+combine.swift in Sources */, @@ -1593,6 +1600,7 @@ F97B461724D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DB24D9C6F200F4A88B /* Extensions.swift in Sources */, 70C5503925B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1C925C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF225B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7425BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */, @@ -1605,7 +1613,7 @@ 700395F325A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45F324D9C6F200F4A88B /* Pointer.swift in Sources */, 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, - 7044C19225C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C19225C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, 7044C1A025C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B461F24D9C6F200F4A88B /* ParseStorage.swift in Sources */, 7044C1AE25C4FC080011F6E7 /* Query+combine.swift in Sources */, @@ -1763,6 +1771,7 @@ F97B45E924D9C6F200F4A88B /* Query.swift in Sources */, F97B463624D9C74400F4A88B /* URLSession+extensions.swift in Sources */, 70C5503B25B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1CB25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF425B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7625BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */, @@ -1775,7 +1784,7 @@ 700395F525A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FD24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAF259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, - 7044C19425C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C19425C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, 7044C1A225C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B465124D9C78C00F4A88B /* Add.swift in Sources */, 7044C1B025C4FC080011F6E7 /* Query+combine.swift in Sources */, @@ -1846,6 +1855,7 @@ F97B45E824D9C6F200F4A88B /* Query.swift in Sources */, F97B463524D9C74400F4A88B /* URLSession+extensions.swift in Sources */, 70C5503A25B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1CA25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF325B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7525BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */, @@ -1858,7 +1868,7 @@ 700395F425A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FC24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, - 7044C19325C4F5B60011F6E7 /* ParseFile+command.swift in Sources */, + 7044C19325C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, 7044C1A125C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B465024D9C78B00F4A88B /* Add.swift in Sources */, 7044C1AF25C4FC080011F6E7 /* Query+combine.swift in Sources */, diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift index bf9c3289f..d7c395480 100644 --- a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(Combine) +import Combine +#endif // swiftlint:disable line_length @@ -90,6 +93,41 @@ public extension ParseApple { callbackQueue: callbackQueue, completion: completion) } + + #if canImport(Combine) + + /** + Login a `ParseUser` *asynchronously* using Apple authentication. Publishes when complete. + - parameter user: The `user` from `ASAuthorizationAppleIDCredential`. + - parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(user: String, + identityToken: String, + options: API.Options) -> Future { + loginPublisher(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), + options: options) + } + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]?, + options: API.Options) -> Future { + guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), + let authData = authData else { + let error = ParseError(code: .unknownError, + message: "Should have authData in consisting of keys \"id\" and \"token\".") + return Future { promise in + promise(.failure(error)) + } + } + return AuthenticatedUser.loginPublisher(Self.__type, + authData: authData, + options: options) + } + + #endif } // MARK: Link @@ -133,6 +171,41 @@ public extension ParseApple { callbackQueue: callbackQueue, completion: completion) } + + #if canImport(Combine) + + /** + Link the *current* `ParseUser` *asynchronously* using Apple authentication. Publishes when complete. + - parameter user: The `user` from `ASAuthorizationAppleIDCredential`. + - parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(user: String, + identityToken: String, + options: API.Options) -> Future { + linkPublisher(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), + options: options) + } + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, + options: API.Options) -> Future { + guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), + let authData = authData else { + let error = ParseError(code: .unknownError, + message: "Should have authData in consisting of keys \"id\" and \"token\".") + return Future { promise in + promise(.failure(error)) + } + } + return AuthenticatedUser.linkPublisher(Self.__type, + authData: authData, + options: options) + } + + #endif } // MARK: 3rd Party Authentication - ParseApple diff --git a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift index 523c47969..7f7678035 100644 --- a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift +++ b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(Combine) +import Combine +#endif /** Provides utility functions for working with Anonymously logged-in users. @@ -68,6 +71,18 @@ public extension ParseAnonymous { callbackQueue: callbackQueue, completion: completion) } + + #if canImport(Combine) + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]?, + options: API.Options) -> Future { + AuthenticatedUser.loginPublisher(__type, + authData: AuthenticationKeys.id.makeDictionary(), + options: options) + } + + #endif } // MARK: Link @@ -81,6 +96,18 @@ public extension ParseAnonymous { completion(.failure(ParseError(code: .unknownError, message: "Not supported"))) } } + + #if canImport(Combine) + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, + options: API.Options) -> Future { + Future { promise in + promise(.failure(ParseError(code: .unknownError, message: "Not supported"))) + } + } + + #endif } // MARK: ParseAnonymous diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift new file mode 100644 index 000000000..4f4e73de6 --- /dev/null +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift @@ -0,0 +1,97 @@ +// +// ParseAuthentication+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Convenience Implementations - Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseAuthentication { + + func unlinkPublisher(_ user: AuthenticatedUser, + options: API.Options) -> Future { + user.unlinkPublisher(__type, options: options) + } + + func unlinkPublisher(options: API.Options) -> Future { + guard let current = AuthenticatedUser.current else { + let error = ParseError(code: .invalidLinkedSession, message: "No current ParseUser.") + return Future { promise in + promise(.failure(error)) + } + } + return unlinkPublisher(current, options: options) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseUser { + + // MARK: 3rd Party Authentication - Login Combine + + /** + Makes an *asynchronous* request to log in a user with specified credentials. + Publishes an instance of the successfully logged in `ParseUser`. + + This also caches the user locally so that calls to *current* will use the latest logged in user. + - parameter type: The authentication type. + - parameter authData: The data that represents the authentication. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func loginPublisher(_ type: String, + authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + login(type, + authData: authData, + options: options, + completion: promise) + } + } + + /** + Unlink the authentication type *asynchronously*. Publishes when complete. + - parameter type: The type to unlink. The user must be logged in on this device. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func unlinkPublisher(_ type: String, + options: API.Options = []) -> Future { + Future { promise in + unlink(type, + options: options, + completion: promise) + } + } + + /** + Makes an *asynchronous* request to link a user with specified credentials. The user should already be logged in. + Publishes an instance of the successfully linked `ParseUser`. + + This also caches the user locally so that calls to *current* will use the latest logged in user. + - parameter type: The authentication type. + - parameter authData: The data that represents the authentication. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func linkPublisher(_ type: String, + authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + link(type, + authData: authData, + options: options, + completion: promise) + } + } + +} + +#endif diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index eaac12f6f..7eae38766 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(Combine) +import Combine +#endif /** Objects that conform to the `ParseAuthentication` protocol provide @@ -74,7 +77,7 @@ public protocol ParseAuthentication: Codable { - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. - It should have the following argument signature: `(Result)`. + It should have the following argument signature: `(Result)`. */ func unlink(options: API.Options, callbackQueue: DispatchQueue, @@ -93,6 +96,53 @@ public protocol ParseAuthentication: Codable { - returns: The user whose autentication type was stripped. This modified user has not been saved. */ func strip(_ user: AuthenticatedUser) -> AuthenticatedUser + + #if canImport(Combine) + /** + Login a `ParseUser` *asynchronously* using the respective authentication type. + - parameter authData: The authData for the respective authentication type. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + */ + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]?, + options: API.Options) -> Future + + /** + Link the *current* `ParseUser` *asynchronously* using the respective authentication type. + - parameter authData: The authData for the respective authentication type. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + */ + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, + options: API.Options) -> Future + + /** + Unlink the `ParseUser` *asynchronously* from the respective authentication type. + - parameter user: The `ParseUser` to unlink. The user must be logged in on this device. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + It should have the following argument signature: `(Result)`. + */ + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func unlinkPublisher(_ user: AuthenticatedUser, + options: API.Options) -> Future + + /** + Unlink the *current* `ParseUser` *asynchronously* from the respective authentication type. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + It should have the following argument signature: `(Result)`. + */ + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func unlinkPublisher(options: API.Options) -> Future + + #endif } // MARK: Convenience Implementations @@ -188,7 +238,7 @@ public extension ParseUser { static func login(_ type: String, authData: [String: String], options: API.Options, - callbackQueue: DispatchQueue, + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { let body = SignupLoginBody(authData: [type: authData]) @@ -201,6 +251,12 @@ public extension ParseUser { } // MARK: 3rd Party Authentication - Link + /** + Whether the `ParseUser` is logged in with the respective authentication string type. + - parameter type: The authentication type to check. The user must be logged in on this device. + - returns: `true` if the `ParseUser` is logged in via the repective + authentication type. `false` if the user is not. + */ func isLinked(with type: String) -> Bool { guard let authData = self.authData?[type] else { return false @@ -208,9 +264,17 @@ public extension ParseUser { return authData != nil } + /** + Unlink the authentication type *asynchronously*. + - parameter type: The type to unlink. The user must be logged in on this device. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + It should have the following argument signature: `(Result)`. + */ func unlink(_ type: String, - options: API.Options, - callbackQueue: DispatchQueue, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { guard let current = Self.current, @@ -281,8 +345,8 @@ public extension ParseUser { */ static func link(_ type: String, authData: [String: String], - options: API.Options, - callbackQueue: DispatchQueue, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { guard let current = Self.current else { let error = ParseError(code: .unknownError, message: "Must be logged in to link user") @@ -327,9 +391,3 @@ public extension ParseUser { } } } - -public extension ParseUser { - - // MARK: 3rd Party Authentication - Login Combine - -} diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 7e7361c80..4116df49d 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -693,7 +693,7 @@ extension ParseLiveQuery { // MARK: ParseLiveQuery - Subscribe @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { - #if !os(Linux) + #if canImport(Combine) /** Registers the query for live updates, using the default subscription handler, and the default `ParseLiveQuery` client. Suitable for `ObjectObserved` diff --git a/Sources/ParseSwift/LiveQuery/Subscription.swift b/Sources/ParseSwift/LiveQuery/Subscription.swift index 6e69c9bb6..dfbf069f3 100644 --- a/Sources/ParseSwift/LiveQuery/Subscription.swift +++ b/Sources/ParseSwift/LiveQuery/Subscription.swift @@ -56,7 +56,7 @@ private func == (lhs: Event, rhs: Event) -> Bool { } } -#if !os(Linux) +#if canImport(Combine) /** A default implementation of the `ParseSubscription` protocol. Suitable for `ObjectObserved` as the subscription can be used as a SwiftUI publisher. Meaning it can serve diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 750b09c0d..2c0be78ef 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index 7f3b00982..f8ab1196d 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 1043c8d7d..953509b69 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -6,11 +6,10 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseUser { diff --git a/Sources/ParseSwift/Operations/ParseOperation+combine.swift b/Sources/ParseSwift/Operations/ParseOperation+combine.swift index 474c003bb..33cc2d91a 100644 --- a/Sources/ParseSwift/Operations/ParseOperation+combine.swift +++ b/Sources/ParseSwift/Operations/ParseOperation+combine.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine diff --git a/Sources/ParseSwift/Types/ParseCloud+combine.swift b/Sources/ParseSwift/Types/ParseCloud+combine.swift index 417de1f73..5c9c2a7a4 100644 --- a/Sources/ParseSwift/Types/ParseCloud+combine.swift +++ b/Sources/ParseSwift/Types/ParseCloud+combine.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine diff --git a/Sources/ParseSwift/Types/ParseConfig+combine.swift b/Sources/ParseSwift/Types/ParseConfig+combine.swift index 73c2fc1fe..fcfe63fcc 100644 --- a/Sources/ParseSwift/Types/ParseConfig+combine.swift +++ b/Sources/ParseSwift/Types/ParseConfig+combine.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine diff --git a/Sources/ParseSwift/Types/ParseFile+command.swift b/Sources/ParseSwift/Types/ParseFile+combine.swift similarity index 98% rename from Sources/ParseSwift/Types/ParseFile+command.swift rename to Sources/ParseSwift/Types/ParseFile+combine.swift index d9ba76617..c4ead77e7 100644 --- a/Sources/ParseSwift/Types/ParseFile+command.swift +++ b/Sources/ParseSwift/Types/ParseFile+combine.swift @@ -1,12 +1,12 @@ // -// ParseFile+command.swift +// ParseFile+combine.swift // ParseSwift // // Created by Corey Baker on 1/29/21. // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine diff --git a/Sources/ParseSwift/Types/Query+combine.swift b/Sources/ParseSwift/Types/Query+combine.swift index 2bf782b42..1c8109f26 100644 --- a/Sources/ParseSwift/Types/Query+combine.swift +++ b/Sources/ParseSwift/Types/Query+combine.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import Combine diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index de3020cb3..453468802 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import XCTest diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 07c09bc1a..9345619d3 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -6,7 +6,7 @@ // Copyright © 2021 Parse Community. All rights reserved. // -#if !os(Linux) +#if canImport(Combine) import Foundation import XCTest From 5f1aec0568d99d095f91a53cc67d48b3c99f0ea3 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 14:53:37 -0500 Subject: [PATCH 08/15] Update --- ParseSwift.xcodeproj/project.pbxproj | 68 +- .../Authentication/3rd Party/ParseApple.swift | 8 +- .../Internal/ParseAnonymous.swift | 4 +- .../ParseAuthentication+combine.swift | 4 +- .../ParseSwift/LiveQuery/Subscription.swift | 199 ------ .../ParseAnonymousCombineTests.swift | 136 ++++ .../ParseAppleCombineTests.swift | 249 +++++++ .../ParseAuthenticationTests.swift | 21 + .../ParseCloudCombineTests.swift | 113 +++ .../ParseConfigCombineTests.swift | 216 ++++++ .../ParseFileCombineTests.swift | 296 ++++++++ .../ParseSwiftTests/ParseLiveQueryTests.swift | 669 ------------------ .../ParseSwiftTests/ParseObjectCombine.swift | 217 ++++++ Tests/ParseSwiftTests/ParseObjectTests.swift | 2 +- .../ParseSwiftTests/ParseOperationTests.swift | 3 +- .../ParseQueryCombineTests.swift | 360 ++++++++++ 16 files changed, 1684 insertions(+), 881 deletions(-) create mode 100644 Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift create mode 100644 Tests/ParseSwiftTests/ParseAppleCombineTests.swift create mode 100644 Tests/ParseSwiftTests/ParseCloudCombineTests.swift create mode 100644 Tests/ParseSwiftTests/ParseConfigCombineTests.swift create mode 100644 Tests/ParseSwiftTests/ParseFileCombineTests.swift create mode 100644 Tests/ParseSwiftTests/ParseObjectCombine.swift create mode 100644 Tests/ParseSwiftTests/ParseQueryCombineTests.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 554f5f800..8f837af54 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -122,6 +122,30 @@ 7044C1C925C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; 7044C1CA25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; 7044C1CB25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */; }; + 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */; }; + 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */; }; + 7044C1EC25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */; }; + 7044C1ED25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */; }; + 7044C1EE25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */; }; + 7044C1F925C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */; }; + 7044C1FA25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */; }; + 7044C1FB25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */; }; + 7044C20625C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */; }; + 7044C20725C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */; }; + 7044C20825C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */; }; + 7044C21325C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */; }; + 7044C21425C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */; }; + 7044C21525C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */; }; + 7044C22025C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */; }; + 7044C22125C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */; }; + 7044C22225C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */; }; + 7044C22D25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */; }; + 7044C22E25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */; }; + 7044C22F25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */; }; + 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */; }; + 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */; }; + 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */; }; 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; @@ -519,6 +543,14 @@ 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+combine.swift"; sourceTree = ""; }; 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationCombineTests.swift; sourceTree = ""; }; 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseAuthentication+combine.swift"; sourceTree = ""; }; + 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseObjectCombine.swift; sourceTree = ""; }; + 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOperationCombineTests.swift; sourceTree = ""; }; + 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileCombineTests.swift; sourceTree = ""; }; + 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseQueryCombineTests.swift; sourceTree = ""; }; + 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudCombineTests.swift; sourceTree = ""; }; + 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConfigCombineTests.swift; sourceTree = ""; }; + 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymousCombineTests.swift; sourceTree = ""; }; + 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAppleCombineTests.swift; sourceTree = ""; }; 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveQuerySocket.swift; sourceTree = ""; }; 70572670259033A700F0ADD5 /* ParseFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileManager.swift; sourceTree = ""; }; 705726DF2592C2A800F0ADD5 /* ParseHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseHash.swift; sourceTree = ""; }; @@ -731,28 +763,36 @@ 705726ED2592C91C00F0ADD5 /* HashTests.swift */, 4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */, 9194657724F16E330070296B /* ParseACLTests.swift */, + 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */, 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */, + 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */, 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */, 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */, + 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */, 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */, + 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */, 70D1BE0625BB2BF400A42E7C /* ParseConfigTests.swift */, F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */, + 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */, 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */, 705727882593FF8000F0ADD5 /* ParseFileTests.swift */, 70BC0B32251903D1001556DB /* ParseGeoPointTests.swift */, - 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */, 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */, + 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */, 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, + 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, + 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */, 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */, 70CE1D882545BF730018D572 /* ParsePointerTests.swift */, + 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */, 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */, 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */, 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */, 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */, - 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */, + 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, 911DB12A24C3F7260027F3C7 /* NetworkMocking */, ); @@ -1560,6 +1600,8 @@ 70CE1D892545BF730018D572 /* ParsePointerTests.swift in Sources */, 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */, 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, + 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, + 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */, 7016ED4025C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, @@ -1567,22 +1609,28 @@ 70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */, 7003957625A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99F9259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 7044C20625C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508525B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 7004C24D25B69207005E0AD9 /* ParseRoleTests.swift in Sources */, 91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */, 70D1BD8725B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963B25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 7FFF552E2217E72A007C3B4E /* AnyEncodableTests.swift in Sources */, + 7044C22025C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */, 7FFF55302217E72A007C3B4E /* AnyDecodableTests.swift in Sources */, 70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */, 7044C1BB25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 7FFF552F2217E72A007C3B4E /* AnyCodableTests.swift in Sources */, 4AA807701F794C31008CD551 /* KeychainStoreTests.swift in Sources */, + 7044C1F925C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, F971F4F624DE381A006CB79B /* ParseEncoderTests.swift in Sources */, 70C7DC2124D20F190050419B /* ParseQueryTests.swift in Sources */, + 7044C22D25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, 9194657824F16E330070296B /* ParseACLTests.swift in Sources */, + 7044C21325C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */, 70A2D86B25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, + 7044C1EC25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 70C7DC1E24D20E530050419B /* ParseUserTests.swift in Sources */, 70A2D81F25B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE4B25BB312700A42E7C /* ParseConfigTests.swift in Sources */, @@ -1692,6 +1740,8 @@ 709B98532556ECAA00507778 /* ParsePointerTests.swift in Sources */, 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, + 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, + 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */, 7016ED4225C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, @@ -1699,22 +1749,28 @@ 709B984F2556ECAA00507778 /* AnyCodableTests.swift in Sources */, 7003957825A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FB259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 7044C20825C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508725B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 7004C26125B6920B005E0AD9 /* ParseRoleTests.swift in Sources */, 9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, 70D1BD8925B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963D25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */, + 7044C22225C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */, 709B98522556ECAA00507778 /* ParseUserTests.swift in Sources */, 709B984E2556ECAA00507778 /* ParseGeoPointTests.swift in Sources */, 7044C1BD25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 709B984B2556ECAA00507778 /* MockURLProtocol.swift in Sources */, 709B98552556ECAA00507778 /* ParseQueryTests.swift in Sources */, + 7044C1FB25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502425B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 709B98502556ECAA00507778 /* KeychainStoreTests.swift in Sources */, 709B98562556ECAA00507778 /* ParseObjectTests.swift in Sources */, + 7044C22F25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, 709B985A2556ECAA00507778 /* ParseObjectBatchTests.swift in Sources */, + 7044C21525C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */, 70A2D86D25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, + 7044C1EE25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 709B98582556ECAA00507778 /* AnyEncodableTests.swift in Sources */, 70A2D82125B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE5F25BB312A00A42E7C /* ParseConfigTests.swift in Sources */, @@ -1731,6 +1787,8 @@ 70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, + 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, + 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */, 7016ED4125C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, @@ -1738,22 +1796,28 @@ 70F2E2BD254F283000B2EA5C /* AnyDecodableTests.swift in Sources */, 7003957725A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FA259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 7044C20725C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508625B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 7004C25725B6920A005E0AD9 /* ParseRoleTests.swift in Sources */, 91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, 70D1BD8825B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963C25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 70F2E2C1254F283000B2EA5C /* AnyCodableTests.swift in Sources */, + 7044C22125C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */, 70F2E2B3254F283000B2EA5C /* ParseUserTests.swift in Sources */, 70F2E2C0254F283000B2EA5C /* MockURLResponse.swift in Sources */, 7044C1BC25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 70F2E2BE254F283000B2EA5C /* ParseObjectBatchTests.swift in Sources */, 70F2E2BF254F283000B2EA5C /* MockURLProtocol.swift in Sources */, + 7044C1FA25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502325B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 70F2E2BB254F283000B2EA5C /* ParseGeoPointTests.swift in Sources */, 70F2E2B8254F283000B2EA5C /* AnyEncodableTests.swift in Sources */, + 7044C22E25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, 70F2E2B4254F283000B2EA5C /* ParseQueryTests.swift in Sources */, + 7044C21425C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */, 70A2D86C25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, + 7044C1ED25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 70F2E2BA254F283000B2EA5C /* ParseInstallationTests.swift in Sources */, 70A2D82025B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE5525BB312900A42E7C /* ParseConfigTests.swift in Sources */, diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift index d7c395480..4d14ed6c2 100644 --- a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift @@ -106,14 +106,14 @@ public extension ParseApple { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) func loginPublisher(user: String, identityToken: String, - options: API.Options) -> Future { + options: API.Options = []) -> Future { loginPublisher(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), options: options) } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) func loginPublisher(authData: [String: String]?, - options: API.Options) -> Future { + options: API.Options = []) -> Future { guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), let authData = authData else { let error = ParseError(code: .unknownError, @@ -184,14 +184,14 @@ public extension ParseApple { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) func linkPublisher(user: String, identityToken: String, - options: API.Options) -> Future { + options: API.Options = []) -> Future { linkPublisher(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), options: options) } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) func linkPublisher(authData: [String: String]?, - options: API.Options) -> Future { + options: API.Options = []) -> Future { guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), let authData = authData else { let error = ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift index 7f7678035..a927c374c 100644 --- a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift +++ b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift @@ -75,8 +75,8 @@ public extension ParseAnonymous { #if canImport(Combine) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - func loginPublisher(authData: [String: String]?, - options: API.Options) -> Future { + func loginPublisher(authData: [String: String]? = nil, + options: API.Options = []) -> Future { AuthenticatedUser.loginPublisher(__type, authData: AuthenticationKeys.id.makeDictionary(), options: options) diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift index 4f4e73de6..7043f8217 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift @@ -15,11 +15,11 @@ import Combine public extension ParseAuthentication { func unlinkPublisher(_ user: AuthenticatedUser, - options: API.Options) -> Future { + options: API.Options = []) -> Future { user.unlinkPublisher(__type, options: options) } - func unlinkPublisher(options: API.Options) -> Future { + func unlinkPublisher(options: API.Options = []) -> Future { guard let current = AuthenticatedUser.current else { let error = ParseError(code: .invalidLinkedSession, message: "No current ParseUser.") return Future { promise in diff --git a/Sources/ParseSwift/LiveQuery/Subscription.swift b/Sources/ParseSwift/LiveQuery/Subscription.swift index dfbf069f3..c5eb4160a 100644 --- a/Sources/ParseSwift/LiveQuery/Subscription.swift +++ b/Sources/ParseSwift/LiveQuery/Subscription.swift @@ -103,57 +103,6 @@ open class Subscription: ParseSubscription, ObservableObject { } } - /// The objects found in a `find`, `first`, or `aggregate` - /// query. - /// - note: this will only countain one item for `first`. - public internal(set) var results: [T]? { - willSet { - if newValue != nil { - resultsCodable = nil - count = nil - error = nil - objectWillChange.send() - } - } - } - - /// The number of items found in a `count` query. - public internal(set) var count: Int? { - willSet { - if newValue != nil { - results = nil - resultsCodable = nil - error = nil - objectWillChange.send() - } - } - } - - /// Results of a `explain` or `hint` query. - public internal(set) var resultsCodable: AnyCodable? { - willSet { - if newValue != nil { - results = nil - count = nil - error = nil - objectWillChange.send() - } - } - } - - /// If an error occured during a `find`, `first`, `count`, or `aggregate` - /// query. - public internal(set) var error: ParseError? { - willSet { - if newValue != nil { - count = nil - results = nil - resultsCodable = nil - objectWillChange.send() - } - } - } - /** Creates a new subscription that can be used to handle updates. */ @@ -180,154 +129,6 @@ open class Subscription: ParseSubscription, ObservableObject { open func didUnsubscribe() { self.unsubscribed = query } - - /** - Finds objects and publishes them as `results` afterwards. - - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func find(options: API.Options = [], callbackQueue: DispatchQueue = .main) { - query.find(options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.results = results - case .failure(let error): - self.error = error - } - } - } - - /** - Finds objects and publishes them as `resultsCodable` afterwards. - - - parameter explain: Used to toggle the information on the query plan. - - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func find(explain: Bool, - hint: String? = nil, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.find(explain: explain, hint: hint, options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.resultsCodable = results - case .failure(let error): - self.error = error - } - } - } - - /** - Gets an object and publishes them as `results` afterwards. - - - warning: This method mutates the query. It will reset the limit to `1`. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func first(options: API.Options = [], callbackQueue: DispatchQueue = .main) { - query.first(options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.results = [results] - case .failure(let error): - self.error = error - } - } - } - - /** - Gets an object and publishes them as `resultsCodable` afterwards. - - - warning: This method mutates the query. It will reset the limit to `1`. - - parameter explain: Used to toggle the information on the query plan. - - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func first(explain: Bool, - hint: String? = nil, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.first(explain: explain, hint: hint, options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.resultsCodable = results - case .failure(let error): - self.error = error - } - } - } - - /** - Counts objects and publishes them as `count` afterwards. - - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func count(options: API.Options = [], callbackQueue: DispatchQueue = .main) { - query.count(options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.count = results - case .failure(let error): - self.error = error - } - } - } - - /** - Counts objects and publishes them as `resultsCodable` afterwards. - - - parameter explain: Used to toggle the information on the query plan. - - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func count(explain: Bool, - hint: String? = nil, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.count(explain: explain, hint: hint, options: options) { result in - switch result { - - case .success(let results): - self.resultsCodable = results - case .failure(let error): - self.error = error - } - } - } - - /** - Executes an aggregate query and publishes the results as `results` afterwards. - - - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. - - parameter pipeline: A pipeline of stages to process query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - - warning: This hasn't been tested thoroughly. - */ - open func aggregate(_ pipeline: Query.AggregateType, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.aggregate(pipeline, options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.results = results - case .failure(let error): - self.error = error - } - } - } } #endif diff --git a/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift new file mode 100644 index 000000000..91160ccc8 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift @@ -0,0 +1,136 @@ +// +// ParseAnonymousCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseAuthenticationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + self.createdAt = Date() + self.updatedAt = Date() + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testLogin() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.anonymous.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.anonymous.loginPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseAppleCombineTests.swift b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift new file mode 100644 index 000000000..cb5b3059a --- /dev/null +++ b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift @@ -0,0 +1,249 @@ +// +// ParseAppleCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseAppleCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + self.createdAt = Date() + self.updatedAt = Date() + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testLogin() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.apple.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.apple.loginPublisher(user: "testing", identityToken: "this") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.apple.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func loginNormally() throws -> User { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + return try User.login(username: "parse", password: "user") + } + + func testLink() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + _ = try loginNormally() + MockURLProtocol.removeAll() + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.apple.linkPublisher(user: "testing", identityToken: "this") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.apple.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlink() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + _ = try loginNormally() + MockURLProtocol.removeAll() + + let authData = ParseApple + .AuthenticationKeys.id.makeDictionary(user: "testing", + identityToken: "this") + User.current?.authData = [User.apple.__type: authData] + XCTAssertTrue(User.apple.isLinked) + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.apple.unlinkPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertFalse(user.apple.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift index 827eb358b..59b0f3fc8 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -9,6 +9,9 @@ import Foundation import XCTest @testable import ParseSwift +#if canImport(Combine) +import Combine +#endif class ParseAuthenticationTests: XCTestCase { @@ -46,6 +49,24 @@ class ParseAuthenticationTests: XCTestCase { let error = ParseError(code: .unknownError, message: "Not implemented") completion(.failure(error)) } + + #if canImport(Combine) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]?, + options: API.Options) -> Future { + let error = ParseError(code: .unknownError, message: "Not implemented") + return Future { promise in + promise(.failure(error)) + } + } + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, options: API.Options) -> Future { + let error = ParseError(code: .unknownError, message: "Not implemented") + return Future { promise in + promise(.failure(error)) + } + } + #endif } override func setUpWithError() throws { diff --git a/Tests/ParseSwiftTests/ParseCloudCombineTests.swift b/Tests/ParseSwiftTests/ParseCloudCombineTests.swift new file mode 100644 index 000000000..be1ae445e --- /dev/null +++ b/Tests/ParseSwiftTests/ParseCloudCombineTests.swift @@ -0,0 +1,113 @@ +// +// ParseCloudCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseCloudCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct Cloud: ParseCloud { + // Those are required for Object + var functionJobName: String + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testFunction() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let cloud = Cloud(functionJobName: "test") + let publisher = cloud.runFunctionPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { functionResponse in + + XCTAssertEqual(functionResponse, AnyCodable()) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testJob() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let cloud = Cloud(functionJobName: "test") + let publisher = cloud.startJobPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { functionResponse in + + XCTAssertEqual(functionResponse, AnyCodable()) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseConfigCombineTests.swift b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift new file mode 100644 index 000000000..5c7b84673 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift @@ -0,0 +1,216 @@ +// +// ParseConfigCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseConfigCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct Config: ParseConfig { + var welcomeMessage: String? + var winningNumber: Int? + } + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + self.createdAt = Date() + self.updatedAt = Date() + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func userLogin() { + let loginResponse = LoginSignupResponse() + let loginUserName = "hello10" + let loginPassword = "world" + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + _ = try User.login(username: loginUserName, password: loginPassword) + MockURLProtocol.removeAll() + } catch { + XCTFail("Should login") + } + } + + func testFetch() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + userLogin() + let config = Config() + + var configOnServer = config + configOnServer.welcomeMessage = "Hello" + let serverResponse = ConfigFetchResponse(params: configOnServer) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = config.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.welcomeMessage, configOnServer.welcomeMessage) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainConfig: CurrentConfigContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainConfig.currentConfig?.welcomeMessage, configOnServer.welcomeMessage) + #endif + + XCTAssertEqual(Config.current?.welcomeMessage, configOnServer.welcomeMessage) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + userLogin() + var config = Config() + config.welcomeMessage = "Hello" + + let serverResponse = ConfigUpdateResponse(result: true) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = config.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssertTrue(saved) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainConfig: CurrentConfigContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainConfig.currentConfig?.welcomeMessage, config.welcomeMessage) + #endif + + XCTAssertEqual(Config.current?.welcomeMessage, config.welcomeMessage) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseFileCombineTests.swift b/Tests/ParseSwiftTests/ParseFileCombineTests.swift new file mode 100644 index 000000000..a97e7f02d --- /dev/null +++ b/Tests/ParseSwiftTests/ParseFileCombineTests.swift @@ -0,0 +1,296 @@ +// +// ParseFileCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseFileCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + let temporaryDirectory = "\(NSTemporaryDirectory())test/" + + struct FileUploadResponse: Codable { + let name: String + let url: URL + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + guard let fileManager = ParseFileManager() else { + throw ParseError(code: .unknownError, message: "Should have initialized file manage") + } + try fileManager.createDirectoryIfNeeded(temporaryDirectory) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + + guard let fileManager = ParseFileManager(), + let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else { + throw ParseError(code: .unknownError, message: "Should have initialized file manage") + } + let directory = URL(fileURLWithPath: temporaryDirectory, isDirectory: true) + let expectation1 = XCTestExpectation(description: "Delete files1") + fileManager.removeDirectoryContents(directory) { error in + guard let error = error else { + expectation1.fulfill() + return + } + XCTFail(error.localizedDescription) + expectation1.fulfill() + } + let directory2 = defaultDirectoryPath + .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true) + let expectation2 = XCTestExpectation(description: "Delete files2") + fileManager.removeDirectoryContents(directory2) { _ in + expectation2.fulfill() + } + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testFetch() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + // swiftlint:disable:next line_length + guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else { + XCTFail("Should create URL") + return + } + var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL) + parseFile.url = parseFileURL + + let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg", + url: parseFileURL) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + XCTAssertNotNil(fetched.localURL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFetchFileProgress() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + // swiftlint:disable:next line_length + guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else { + XCTFail("Should create URL") + return + } + var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL) + parseFile.url = parseFileURL + + let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg", + url: parseFileURL) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.fetchPublisher(progress: { (_, _, totalDownloaded, totalExpected) in + let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100 + XCTAssertGreaterThan(currentProgess, -1) + }).sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + XCTAssertNotNil(fetched.localURL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + let parseFile = ParseFile(name: "sampleData.txt", data: sampleData) + + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveFileProgress() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + let parseFile = ParseFile(name: "sampleData.txt", data: sampleData) + + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.savePublisher(progress: { (_, _, totalDownloaded, totalExpected) in + let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100 + XCTAssertGreaterThan(currentProgess, -1) + }).sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + // swiftlint:disable:next line_length + guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else { + XCTFail("Should create URL") + return + } + var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL) + parseFile.url = parseFileURL + + let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg", + url: parseFileURL) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.deletePublisher(options: [.useMasterKey]) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 2d38712a1..d9c693f3d 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -1426,674 +1426,5 @@ class ParseLiveQueryTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: 20.0) } - - func testFind() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.count = 5 - subscription.resultsCodable = AnyCodable() - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.find(options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let score = subscription.results?.first else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.count) - XCTAssert(score.hasSameObjectId(as: scoreOnServer)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirst() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.count = 5 - subscription.resultsCodable = AnyCodable() - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.first(options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let score = subscription.results?.first else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.count) - XCTAssert(score.hasSameObjectId(as: scoreOnServer)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testCount() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.count(options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let count = subscription.count else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(count, 1) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testAggregate() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.count = 5 - subscription.resultsCodable = AnyCodable() - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.aggregate([["hello": "world"]], options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let score = subscription.results?.first else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.count) - XCTAssert(score.hasSameObjectId(as: scoreOnServer)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFindExplain() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.count = 5 - - let json = AnyResultsResponse(results: ["yolo": "yarr"]) - - let encoded: Data! - do { - encoded = try JSONEncoder().encode(json) - } catch { - XCTFail("Should encode. Error \(error)") - return - } - MockURLProtocol.mockRequests { _ in - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } - - subscription.find(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let response = subscription.resultsCodable?.value as? [String: String], - let expected = json.results?.value as? [String: String] else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(response, expected) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirstExplain() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.count = 5 - - let json = AnyResultsResponse(results: ["yolo": "yarr"]) - - let encoded: Data! - do { - encoded = try JSONEncoder().encode(json) - } catch { - XCTFail("Should encode. Error \(error)") - return - } - MockURLProtocol.mockRequests { _ in - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } - - subscription.first(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let response = subscription.resultsCodable?.value as? [String: String], - let expected = json.results?.value as? [String: String] else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(response, expected) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testCountExplain() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.count = 5 - - let json = AnyResultsResponse(results: ["yolo": "yarr"]) - - let encoded: Data! - do { - encoded = try JSONEncoder().encode(json) - } catch { - XCTFail("Should encode. Error \(error)") - return - } - MockURLProtocol.mockRequests { _ in - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } - - subscription.count(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let response = subscription.resultsCodable?.value as? [String: String], - let expected = json.results?.value as? [String: String] else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(response, expected) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFindError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.find() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirstError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.first() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testCountError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.count() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testAggregateError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.aggregate([["hello": "world"]]) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFindExplainError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.find(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirstExplainError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.first(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - func testCountExplainError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.count(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } } #endif diff --git a/Tests/ParseSwiftTests/ParseObjectCombine.swift b/Tests/ParseSwiftTests/ParseObjectCombine.swift new file mode 100644 index 000000000..da9f5db5e --- /dev/null +++ b/Tests/ParseSwiftTests/ParseObjectCombine.swift @@ -0,0 +1,217 @@ +// +// ParseObjectCombine.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseObjectCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int? + var player: String? + + //custom initializers + init (objectId: String?) { + self.objectId = objectId + } + init(score: Int) { + self.score = score + self.player = "Jen" + } + init(score: Int, name: String) { + self.score = score + self.player = name + } + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testFetch() { + var score = GameScore(score: 10) + let objectId = "yarr" + score.objectId = objectId + + var scoreOnServer = score + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + 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) + } + + let publisher = score.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssert(fetched.hasSameObjectId(as: scoreOnServer)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertNil(fetched.ACL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + let score = GameScore(score: 10) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + 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) + } + + let publisher = score.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() { + var score = GameScore(score: 10) + score.objectId = "yarr" + + let scoreOnServer = NoBody() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = score.deletePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 584b6ca78..5665a9e21 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -247,7 +247,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var scoreOnServer = score scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt scoreOnServer.ACL = nil let encoded: Data! do { diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift index b8ae8abde..f784d9305 100644 --- a/Tests/ParseSwiftTests/ParseOperationTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -103,8 +103,7 @@ class ParseOperationTests: XCTestCase { var scoreOnServer = score scoreOnServer.score = 11 - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.updatedAt = Date() let encoded: Data! do { diff --git a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift new file mode 100644 index 000000000..85e67cc97 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift @@ -0,0 +1,360 @@ +// +// ParseQueryCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseQueryCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int? + var player: String? + + //custom initializers + init (objectId: String?) { + self.objectId = objectId + } + init(score: Int) { + self.score = score + self.player = "Jen" + } + init(score: Int, name: String) { + self.score = score + self.player = name + } + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testFind() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.score = 11 + scoreOnServer.objectId = "yolo" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + + let publisher = query.findPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + guard let object = found.first else { + XCTFail("Should have unwrapped") + return + } + XCTAssert(object.hasSameObjectId(as: scoreOnServer)) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFindExplain() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let json = AnyResultsResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + + let publisher = query.findPublisher(explain: true) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { queryResult in + + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { + XCTFail("Error: Should cast to string") + return + } + XCTAssertEqual(response, expected) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFirst() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.score = 11 + scoreOnServer.objectId = "yolo" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + + let publisher = query.firstPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + XCTAssert(found.hasSameObjectId(as: scoreOnServer)) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFirstExplain() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let json = AnyResultsResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + + let publisher = query.firstPublisher(explain: true) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { queryResult in + + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { + XCTFail("Error: Should cast to string") + return + } + XCTAssertEqual(response, expected) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testCount() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.score = 11 + scoreOnServer.objectId = "yolo" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + + let publisher = query.countPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + XCTAssertEqual(found, 1) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testCountExplain() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let json = AnyResultsResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + + let publisher = query.countPublisher(explain: true) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { queryResult in + + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { + XCTFail("Error: Should cast to string") + return + } + XCTAssertEqual(response, expected) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testAggregate() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + let pipeline = [[String: String]]() + let publisher = query.aggregatePublisher(pipeline) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + guard let object = found.first else { + XCTFail("Should have unwrapped") + return + } + XCTAssert(object.hasSameObjectId(as: scoreOnServer)) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif From fe510c8848a5a94d1d37ce4c7eafec753f519303 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 16:44:25 -0500 Subject: [PATCH 09/15] Fixed deleteAll --- .../Contents.swift | 11 +- .../Contents.swift | 8 ++ Sources/ParseSwift/API/API+Commands.swift | 45 ++++--- Sources/ParseSwift/API/BatchUtils.swift | 4 +- .../Objects/ParseInstallation.swift | 23 ++-- Sources/ParseSwift/Objects/ParseObject.swift | 26 ++-- Sources/ParseSwift/Objects/ParseUser.swift | 29 +++-- .../ParseInstallationTests.swift | 10 +- .../ParseObjectBatchTests.swift | 87 +++++++++---- .../ParseOperationCombineTests.swift | 122 ++++++++++++++++++ Tests/ParseSwiftTests/ParseUserTests.swift | 10 +- 11 files changed, 276 insertions(+), 99 deletions(-) create mode 100644 Tests/ParseSwiftTests/ParseOperationCombineTests.swift diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift index b9688ba34..98581d432 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -238,12 +238,13 @@ do { [scoreToFetch, score2ToFetch].deleteAll { result in switch result { case .success(let deletedScores): - deletedScores.forEach { error in - guard let error = error else { - print("Successfully deleted scores") - return + deletedScores.forEach { result in + switch result { + case .success: + print("Successfully deleted score") + case .failure(let error): + print("Error deleting: \(error)") } - print("Error deleting: \(error)") } case .failure(let error): assertionFailure("Error deleting: \(error)") diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 68a4f6372..f282f6d7d 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -101,6 +101,14 @@ User.current?.signup { result in } } +//: Logging out - synchronously. +do { + try User.logout() + print("Successfully logged out") +} catch let error { + print("Error logging out: \(error)") +} + //: Password Reset Request - synchronously. do { try User.verificationEmail(email: "hello@parse.org") diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index 60b94abfa..a626a430a 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -452,38 +452,41 @@ extension API.Command where T: ParseObject { } // MARK: Batch - Deleting - // swiftlint:disable:next line_length - static func batch(commands: [API.NonParseBodyCommand]) -> RESTBatchCommandNoBodyType { - let commands = commands.compactMap { (command) -> API.NonParseBodyCommand? in + static func batch(commands: [API.NonParseBodyCommand]) -> RESTBatchCommandNoBodyType { + let commands = commands.compactMap { (command) -> API.NonParseBodyCommand? in let path = ParseConfiguration.mountPath + command.path.urlComponent - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: command.method, path: .any(path), mapper: command.mapper) } - let mapper = { (data: Data) -> [ParseError?] in + let mapper = { (data: Data) -> [(Result)] in - let decodingType = [ParseError?].self + let decodingType = [BatchResponseItem].self do { let responses = try ParseCoding.jsonDecoder().decode(decodingType, from: data) - return responses.enumerated().map({ (object) -> ParseError? in + return responses.enumerated().map({ (object) -> (Result) in let response = responses[object.offset] - if let error = response { - return error + if response.success != nil { + return .success(()) } else { - return nil + guard let parseError = response.error else { + return .failure(ParseError(code: .unknownError, message: "unknown error")) + } + + return .failure(parseError) } }) } catch { - guard (try? ParseCoding.jsonDecoder().decode(NoBody.self, from: data)) != nil else { - return [ParseError(code: .unknownError, message: "decoding error: \(error)")] + guard let parseError = error as? ParseError else { + return [(.failure(ParseError(code: .unknownError, message: "decoding error: \(error)")))] } - return [nil] + return [(.failure(parseError))] } } let batchCommand = BatchCommandNoBody(requests: commands) - return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) + return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } } @@ -642,17 +645,21 @@ internal extension API { internal extension API.NonParseBodyCommand { // MARK: Deleting - // swiftlint:disable:next line_length - static func deleteCommand(_ object: T) throws -> API.NonParseBodyCommand where T: ParseObject { + static func deleteCommand(_ object: T) throws -> API.NonParseBodyCommand where T: ParseObject { guard object.isSaved else { throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") } - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: .DELETE, path: object.endpoint - ) { (data) -> ParseError? in - try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + ) { (data) -> NoBody in + let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + if let error = error { + throw error + } else { + return NoBody() + } } } } // swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/API/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift index 7e6dd92bd..4d01208d6 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -13,8 +13,8 @@ typealias ParseObjectBatchResponse = [(Result)] // swiftlint:disable line_length typealias RESTBatchCommandType = API.Command, ParseObjectBatchResponse> where T: ParseObject -typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody -typealias ParseObjectBatchResponseNoBody = [ParseError?] +typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody +typealias ParseObjectBatchResponseNoBody = [(Result)] typealias RESTBatchCommandNoBodyType = API.NonParseBodyCommand, ParseObjectBatchResponseNoBody> where T: Encodable /* typealias ParseObjectBatchCommandEncodable = BatchCommand where T: ParseType diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index eb40df916..6da8db305 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -558,16 +558,21 @@ extension ParseInstallation { } } - func deleteCommand() throws -> API.NonParseBodyCommand { + func deleteCommand() throws -> API.NonParseBodyCommand { guard isSaved else { throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") } - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: .DELETE, path: endpoint - ) { (data) -> ParseError? in - try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + ) { (data) -> NoBody in + let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + if let error = error { + throw error + } else { + return NoBody() + } } } } @@ -858,13 +863,13 @@ public extension Sequence where Element: ParseInstallation { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func deleteAll(batchLimit limit: Int? = nil, - options: API.Options = []) throws -> [ParseError?] { + options: API.Options = []) throws -> [(Result)] { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { - let currentBatch = try API.Command + let currentBatch = try API.Command)> .batch(commands: $0) .execute(options: options) returnBatch.append(contentsOf: currentBatch) @@ -897,11 +902,11 @@ public extension Sequence where Element: ParseInstallation { batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result<[ParseError?], ParseError>) -> Void + completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index d2331af0b..8703ae1c5 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -331,13 +331,13 @@ public extension Sequence where Element: ParseObject { - throws: `ParseError` */ func deleteAll(batchLimit limit: Int? = nil, - options: API.Options = []) throws -> [ParseError?] { + options: API.Options = []) throws -> [(Result)] { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { - let currentBatch = try API.Command + let currentBatch = try API.Command)> .batch(commands: $0) .execute(options: options) returnBatch.append(contentsOf: currentBatch) @@ -367,11 +367,11 @@ public extension Sequence where Element: ParseObject { batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result<[ParseError?], ParseError>) -> Void + completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 @@ -703,9 +703,7 @@ extension ParseObject { - throws: An error of `ParseError` type. */ public func delete(options: API.Options = []) throws { - if let error = try deleteCommand().execute(options: options) { - throw error - } + _ = try deleteCommand().execute(options: options) } /** @@ -727,12 +725,8 @@ extension ParseObject { callbackQueue.async { switch result { - case .success(let error): - if let error = error { - completion(.failure(error)) - } else { - completion(.success(())) - } + case .success: + completion(.success(())) case .failure(let error): completion(.failure(error)) } @@ -749,7 +743,7 @@ extension ParseObject { } } - internal func deleteCommand() throws -> API.NonParseBodyCommand { - try API.NonParseBodyCommand.deleteCommand(self) + internal func deleteCommand() throws -> API.NonParseBodyCommand { + try API.NonParseBodyCommand.deleteCommand(self) } }// swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index e6c6a3ab1..b55adfbef 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -859,14 +859,10 @@ extension ParseUser { try deleteCommand().executeAsync(options: options) { result in switch result { - case .success(let error): + case .success: callbackQueue.async { try? Self.updateKeychainIfNeeded([self], deleting: true) - if let error = error { - completion(.failure(error)) - } else { - completion(.success(())) - } + completion(.success(())) } case .failure(let error): callbackQueue.async { @@ -885,16 +881,21 @@ extension ParseUser { } } - func deleteCommand() throws -> API.NonParseBodyCommand { + func deleteCommand() throws -> API.NonParseBodyCommand { guard isSaved else { throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") } - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: .DELETE, path: endpoint - ) { (data) -> ParseError? in - try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + ) { (data) -> NoBody in + let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + if let error = error { + throw error + } else { + return NoBody() + } } } } @@ -1183,9 +1184,9 @@ public extension Sequence where Element: ParseUser { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func deleteAll(batchLimit limit: Int? = nil, - options: API.Options = []) throws -> [ParseError?] { + options: API.Options = []) throws -> [(Result)] { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { @@ -1221,11 +1222,11 @@ public extension Sequence where Element: ParseUser { batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result<[ParseError?], ParseError>) -> Void + completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index 290e91a06..dbac1044e 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -1095,8 +1095,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } - let error: ParseError? = nil - let installationOnServer = [error] + let installationOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1113,7 +1112,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l do { let deleted = try [installation].deleteAll() deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } @@ -1138,8 +1137,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } - let error: ParseError? = nil - let installationOnServer = [error] + let installationOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1158,7 +1156,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l case .success(let deleted): deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index 17fd0854f..15289aed8 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -1280,8 +1280,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAll() { - let error: ParseError? = nil - let response = [error] + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1295,18 +1295,26 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let fetched = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() + let deleted = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { XCTFail("Should unwrap") return } - if let error = firstObject { + if case let .failure(error) = firstObject { XCTFail(error.localizedDescription) } + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = lastObject { + XCTFail(error.localizedDescription) + } } catch { XCTFail(error.localizedDescription) } @@ -1314,7 +1322,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le func testDeleteAllError() { let parseError = ParseError(code: .objectNotFound, message: "Object not found") - let response = [parseError] + let response = [BatchResponseItem(success: nil, error: parseError), + BatchResponseItem(success: nil, error: parseError)] let encoded: Data! do { encoded = try ParseCoding.jsonEncoder().encode(response) @@ -1327,15 +1336,26 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let fetched = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() + let deleted = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() + + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should have thrown ParseError") + return + } - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + if case let .failure(error) = firstObject { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + + guard let lastObject = deleted.last else { XCTFail("Should have thrown ParseError") return } - if let error = firstObject { + if case let .failure(error) = lastObject { XCTAssertEqual(error.code, parseError.code) } else { XCTFail("Should have thrown ParseError") @@ -1355,15 +1375,25 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le switch result { - case .success(let fetched): - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + case .success(let deleted): + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { XCTFail("Should unwrap") expectation1.fulfill() return } - if let error = firstObject { + if case let .failure(error) = firstObject { + XCTFail(error.localizedDescription) + } + + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + if case let .failure(error) = lastObject { XCTFail(error.localizedDescription) } @@ -1377,8 +1407,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAllAsyncMainQueue() { - let error: ParseError? = nil - let response = [error] + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] do { let encoded = try ParseCoding.jsonEncoder().encode(response) @@ -1402,15 +1432,27 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le switch result { - case .success(let fetched): - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + case .success(let deleted): + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should have thrown ParseError") + expectation1.fulfill() + return + } + + if case let .failure(error) = firstObject { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + + guard let lastObject = deleted.last else { XCTFail("Should have thrown ParseError") expectation1.fulfill() return } - if let error = firstObject { + if case let .failure(error) = lastObject { XCTAssertEqual(error.code, parseError.code) } else { XCTFail("Should have thrown ParseError") @@ -1428,7 +1470,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le func testDeleteAllAsyncMainQueueError() { let parseError = ParseError(code: .objectNotFound, message: "Object not found") - let response = [parseError] + let response = [BatchResponseItem(success: nil, error: parseError), + BatchResponseItem(success: nil, error: parseError)] do { let encoded = try ParseCoding.jsonEncoder().encode(response) diff --git a/Tests/ParseSwiftTests/ParseOperationCombineTests.swift b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift new file mode 100644 index 000000000..37ad0cbcb --- /dev/null +++ b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift @@ -0,0 +1,122 @@ +// +// ParseOperationCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseOperationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int? + var player: String? + + //custom initializers + init (objectId: String?) { + self.objectId = objectId + } + init(score: Int) { + self.score = score + self.player = "Jen" + } + init(score: Int, name: String) { + self.score = score + self.player = name + } + } + + override func setUpWithError() throws { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testSave() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var score = GameScore(score: 10) + score.objectId = "yarr" + let operations = score.operation + .increment("score", by: 1) + + var scoreOnServer = score + scoreOnServer.score = 11 + scoreOnServer.updatedAt = Date() + 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 encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = operations.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + 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) + XCTAssertNil(saved.ACL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 35802bc54..230bf52b7 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -1715,8 +1715,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } - let error: ParseError? = nil - let userOnServer = [error] + let userOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1733,7 +1732,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length do { let deleted = try [user].deleteAll() deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } @@ -1758,8 +1757,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } - let error: ParseError? = nil - let userOnServer = [error] + let userOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1778,7 +1776,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length case .success(let deleted): deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } From a640a70ff2b4d0874a81cb754af8aa2849e55f70 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 17:31:15 -0500 Subject: [PATCH 10/15] Working publishers --- .../Objects/ParseInstallation+combine.swift | 50 +++- .../Objects/ParseObject+combine.swift | 50 +++- .../Objects/ParseUser+combine.swift | 49 +++- .../ParseInstallationCombineTests.swift | 233 ++++++++++++++++ .../ParseSwiftTests/ParseObjectCombine.swift | 250 ++++++++++++++++++ .../ParseUserCombineTests.swift | 230 ++++++++++++++++ 6 files changed, 856 insertions(+), 6 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 2c0be78ef..950c629a6 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -32,7 +32,7 @@ public extension ParseInstallation { // MARK: Savable - Combine /** - Saves the `ParseInstallation` *asynchronously* and executes the given callback block. + Saves the `ParseInstallation` *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -47,7 +47,7 @@ public extension ParseInstallation { // MARK: Deletable - Combine /** - Deletes the `ParseInstallation` *asynchronously* and executes the given callback block. + Deletes the `ParseInstallation` *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -60,4 +60,50 @@ public extension ParseInstallation { } } +// MARK: Batch Support - Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Sequence where Element: ParseInstallation { + /** + Fetches a collection of installations *aynchronously* with the current data from the server and sets + an error if one occurs. Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + fetchAll(options: options, + completion: promise) + } + } + + /** + Saves a collection of installations *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + saveAll(options: options, + completion: promise) + } + } + + /** + Deletes a collection of installations *asynchronously* and and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + deleteAll(options: options, completion: promise) + } + } +} + #endif diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index f8ab1196d..cc92dc231 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -30,7 +30,7 @@ public extension ParseObject { } /** - Saves the `ParseObject` *asynchronously* and executes the given callback block. + Saves the `ParseObject` *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -44,7 +44,7 @@ public extension ParseObject { } /** - Deletes the `ParseObject` *asynchronously* and executes the given callback block. + Deletes the `ParseObject` *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -57,4 +57,50 @@ public extension ParseObject { } } +// MARK: Batch Support - Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Sequence where Element: ParseObject { + /** + Fetches a collection of objects *aynchronously* with the current data from the server and sets + an error if one occurs. Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + fetchAll(options: options, + completion: promise) + } + } + + /** + Saves a collection of objects *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + saveAll(options: options, + completion: promise) + } + } + + /** + Deletes a collection of objects *asynchronously* and and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + deleteAll(options: options, completion: promise) + } + } +} + #endif diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 953509b69..ffdc45ef0 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -153,7 +153,7 @@ public extension ParseUser { // MARK: Savable - Combine /** - Saves the `ParseUser` *asynchronously* and executes the given callback block. + Saves the `ParseUser` *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -168,7 +168,7 @@ public extension ParseUser { // MARK: Deletable - Combine /** - Deletes the `ParseUser` *asynchronously* and executes the given callback block. + Deletes the `ParseUser` *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. @@ -181,4 +181,49 @@ public extension ParseUser { } } +// MARK: Batch Support - Combine +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Sequence where Element: ParseUser { + /** + Fetches a collection of users *aynchronously* with the current data from the server and sets + an error if one occurs. Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + fetchAll(options: options, + completion: promise) + } + } + + /** + Saves a collection of users *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + saveAll(options: options, + completion: promise) + } + } + + /** + Deletes a collection of users *asynchronously* and and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + deleteAll(options: options, completion: promise) + } + } +} #endif diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index 453468802..04eef6330 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -310,6 +310,239 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type } wait(for: [expectation1], timeout: 20.0) } + + func testFetchAll() { + update() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + DispatchQueue.main.async { + guard var installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installation.customKey = "newValue" + let installationOnServer = QueryResponse(results: [installation], count: 1) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(installation) + installation = try installation.getDecoder().decode(Installation.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [installation].fetchAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + fetched.forEach { + switch $0 { + case .success(let fetched): + XCTAssert(fetched.hasSameObjectId(as: installation)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation.currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveAll() { + update() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + DispatchQueue.main.async { + guard var installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installation.customKey = "newValue" + let installationOnServer = [BatchResponseItem(success: installation, error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(installation) + installation = try installation.getDecoder().decode(Installation.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [installation].saveAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: installation)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation.currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testDeleteAll() { + update() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + DispatchQueue.main.async { + guard let installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + let installationOnServer = [BatchResponseItem(success: NoBody(), error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [installation].deleteAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { deleted in + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } } #endif diff --git a/Tests/ParseSwiftTests/ParseObjectCombine.swift b/Tests/ParseSwiftTests/ParseObjectCombine.swift index da9f5db5e..16943deb8 100644 --- a/Tests/ParseSwiftTests/ParseObjectCombine.swift +++ b/Tests/ParseSwiftTests/ParseObjectCombine.swift @@ -212,6 +212,256 @@ class ParseObjectCombineTests: XCTestCase { // swiftlint:disable:this type_body_ wait(for: [expectation1], timeout: 20.0) } + + func testFetchAll() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.objectId = "yolo" + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) + scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].fetchAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.count, 2) + guard let firstObject = try? fetched.first(where: {try $0.get().objectId == "yarr"}), + let secondObject = try? fetched.first(where: {try $0.get().objectId == "yolo"}) else { + XCTFail("Should unwrap") + return + } + + switch firstObject { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let fetchedCreatedAt = first.createdAt, + let fetchedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertNil(first.ACL) + XCTAssertEqual(first.score, scoreOnServer.score) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch secondObject { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + guard let savedCreatedAt = second.createdAt, + let savedUpdatedAt = second.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(second.ACL) + XCTAssertEqual(second.score, scoreOnServer2.score) + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveAll() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.objectId = "yolo" + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil), + BatchResponseItem(success: scoreOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) + scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [score, score2].saveAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(first.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + guard let savedCreatedAt = second.createdAt, + let savedUpdatedAt = second.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(second.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDeleteAll() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { deleted in + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = firstObject { + XCTFail(error.localizedDescription) + } + + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = lastObject { + XCTFail(error.localizedDescription) + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } } #endif diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 9345619d3..0e540630d 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -626,6 +626,236 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le publisher.store(in: &subscriptions) wait(for: [expectation1], timeout: 20.0) } + + func testFetchAll() { + login() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + guard var user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + user.updatedAt = user.updatedAt?.addingTimeInterval(+300) + user.customKey = "newValue" + let userOnServer = QueryResponse(results: [user], count: 1) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(user) + user = try user.getDecoder().decode(User.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [user].fetchAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + fetched.forEach { + switch $0 { + case .success(let fetched): + XCTAssert(fetched.hasSameObjectId(as: user)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveAll() { + login() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + guard var user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + user.updatedAt = user.updatedAt?.addingTimeInterval(+300) + user.customKey = "newValue" + let userOnServer = [BatchResponseItem(success: user, error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(user) + user = try user.getDecoder().decode(User.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [user].saveAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: user)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDeleteAll() { + login() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + guard let user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + let userOnServer = [BatchResponseItem(success: NoBody(), error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [user].deleteAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { deleted in + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } } #endif From 998e32ca23bef3549fdfc700379e8cbaf5ef9e94 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 17:49:28 -0500 Subject: [PATCH 11/15] Fix Swift 5.2 closures and new cache for jazzy --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 4 +- .../ParseAuthentication+combine.swift | 22 +++++----- .../Objects/ParseInstallation+combine.swift | 20 ++++----- .../Objects/ParseObject+combine.swift | 20 ++++----- .../Objects/ParseUser+combine.swift | 26 ++++++------ .../Operations/ParseOperation+combine.swift | 4 +- .../ParseSwift/Types/ParseCloud+combine.swift | 8 ++-- .../Types/ParseConfig+combine.swift | 8 ++-- .../ParseSwift/Types/ParseFile+combine.swift | 22 +++++----- Sources/ParseSwift/Types/Query+combine.swift | 42 +++++++++---------- 11 files changed, 90 insertions(+), 90 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c777f1e43..d78b8077e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,9 +109,9 @@ jobs: uses: actions/cache@v2 with: path: vendor/bundle - key: ${{ runner.os }}-gem-v2-${{ hashFiles('**/Gemfile.lock') }} + key: ${{ runner.os }}-gem-v3-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | - ${{ runner.os }}-gem-v2 + ${{ runner.os }}-gem-v3 - name: Install Bundle run: | bundle config path vendor/bundle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b75b7c192..b4ab29e6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,9 @@ jobs: uses: actions/cache@v2 with: path: vendor/bundle - key: ${{ runner.os }}-gem-v2-${{ hashFiles('**/Gemfile.lock') }} + key: ${{ runner.os }}-gem-v3-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | - ${{ runner.os }}-gem-v2 + ${{ runner.os }}-gem-v3 - name: Install Bundle run: | bundle config path vendor/bundle diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift index 7043f8217..68f70cbba 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift @@ -49,10 +49,10 @@ public extension ParseUser { authData: [String: String], options: API.Options = []) -> Future { Future { promise in - login(type, - authData: authData, - options: options, - completion: promise) + Self.login(type, + authData: authData, + options: options, + completion: promise) } } @@ -65,9 +65,9 @@ public extension ParseUser { func unlinkPublisher(_ type: String, options: API.Options = []) -> Future { Future { promise in - unlink(type, - options: options, - completion: promise) + self.unlink(type, + options: options, + completion: promise) } } @@ -85,10 +85,10 @@ public extension ParseUser { authData: [String: String], options: API.Options = []) -> Future { Future { promise in - link(type, - authData: authData, - options: options, - completion: promise) + Self.link(type, + authData: authData, + options: options, + completion: promise) } } diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 950c629a6..0debbddc1 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -25,8 +25,8 @@ public extension ParseInstallation { */ func fetchPublisher(options: API.Options = []) -> Future { Future { promise in - fetch(options: options, - completion: promise) + self.fetch(options: options, + completion: promise) } } @@ -40,8 +40,8 @@ public extension ParseInstallation { */ func savePublisher(options: API.Options = []) -> Future { Future { promise in - save(options: options, - completion: promise) + self.save(options: options, + completion: promise) } } @@ -55,7 +55,7 @@ public extension ParseInstallation { */ func deletePublisher(options: API.Options = []) -> Future { Future { promise in - delete(options: options, completion: promise) + self.delete(options: options, completion: promise) } } } @@ -73,8 +73,8 @@ public extension Sequence where Element: ParseInstallation { */ func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - fetchAll(options: options, - completion: promise) + self.fetchAll(options: options, + completion: promise) } } @@ -87,8 +87,8 @@ public extension Sequence where Element: ParseInstallation { */ func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - saveAll(options: options, - completion: promise) + self.saveAll(options: options, + completion: promise) } } @@ -101,7 +101,7 @@ public extension Sequence where Element: ParseInstallation { */ func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - deleteAll(options: options, completion: promise) + self.deleteAll(options: options, completion: promise) } } } diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index cc92dc231..5551fb08d 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -24,8 +24,8 @@ public extension ParseObject { */ func fetchPublisher(options: API.Options = []) -> Future { Future { promise in - fetch(options: options, - completion: promise) + self.fetch(options: options, + completion: promise) } } @@ -38,8 +38,8 @@ public extension ParseObject { */ func savePublisher(options: API.Options = []) -> Future { Future { promise in - save(options: options, - completion: promise) + self.save(options: options, + completion: promise) } } @@ -52,7 +52,7 @@ public extension ParseObject { */ func deletePublisher(options: API.Options = []) -> Future { Future { promise in - delete(options: options, completion: promise) + self.delete(options: options, completion: promise) } } } @@ -70,8 +70,8 @@ public extension Sequence where Element: ParseObject { */ func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - fetchAll(options: options, - completion: promise) + self.fetchAll(options: options, + completion: promise) } } @@ -84,8 +84,8 @@ public extension Sequence where Element: ParseObject { */ func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - saveAll(options: options, - completion: promise) + self.saveAll(options: options, + completion: promise) } } @@ -98,7 +98,7 @@ public extension Sequence where Element: ParseObject { */ func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - deleteAll(options: options, completion: promise) + self.deleteAll(options: options, completion: promise) } } } diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index ffdc45ef0..f059a6716 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -47,8 +47,8 @@ public extension ParseUser { */ func signupPublisher(options: API.Options = []) -> Future { Future { promise in - signup(options: options, - completion: promise) + self.signup(options: options, + completion: promise) } } @@ -86,7 +86,7 @@ public extension ParseUser { */ func becomePublisher(sessionToken: String, options: API.Options = []) -> Future { Future { promise in - become(sessionToken: sessionToken, options: options, completion: promise) + self.become(sessionToken: sessionToken, options: options, completion: promise) } } @@ -146,8 +146,8 @@ public extension ParseUser { */ func fetchPublisher(options: API.Options = []) -> Future { Future { promise in - fetch(options: options, - completion: promise) + self.fetch(options: options, + completion: promise) } } @@ -161,8 +161,8 @@ public extension ParseUser { */ func savePublisher(options: API.Options = []) -> Future { Future { promise in - save(options: options, - completion: promise) + self.save(options: options, + completion: promise) } } @@ -176,7 +176,7 @@ public extension ParseUser { */ func deletePublisher(options: API.Options = []) -> Future { Future { promise in - delete(options: options, completion: promise) + self.delete(options: options, completion: promise) } } } @@ -194,8 +194,8 @@ public extension Sequence where Element: ParseUser { */ func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - fetchAll(options: options, - completion: promise) + self.fetchAll(options: options, + completion: promise) } } @@ -208,8 +208,8 @@ public extension Sequence where Element: ParseUser { */ func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - saveAll(options: options, - completion: promise) + self.saveAll(options: options, + completion: promise) } } @@ -222,7 +222,7 @@ public extension Sequence where Element: ParseUser { */ func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { Future { promise in - deleteAll(options: options, completion: promise) + self.deleteAll(options: options, completion: promise) } } } diff --git a/Sources/ParseSwift/Operations/ParseOperation+combine.swift b/Sources/ParseSwift/Operations/ParseOperation+combine.swift index 33cc2d91a..9c8906ae1 100644 --- a/Sources/ParseSwift/Operations/ParseOperation+combine.swift +++ b/Sources/ParseSwift/Operations/ParseOperation+combine.swift @@ -23,8 +23,8 @@ public extension ParseOperation { */ func savePublisher(options: API.Options = []) -> Future { Future { promise in - save(options: options, - completion: promise) + self.save(options: options, + completion: promise) } } } diff --git a/Sources/ParseSwift/Types/ParseCloud+combine.swift b/Sources/ParseSwift/Types/ParseCloud+combine.swift index 5c9c2a7a4..070572c28 100644 --- a/Sources/ParseSwift/Types/ParseCloud+combine.swift +++ b/Sources/ParseSwift/Types/ParseCloud+combine.swift @@ -24,8 +24,8 @@ public extension ParseCloud { */ func runFunctionPublisher(options: API.Options = []) -> Future { Future { promise in - runFunction(options: options, - completion: promise) + self.runFunction(options: options, + completion: promise) } } @@ -39,8 +39,8 @@ public extension ParseCloud { */ func startJobPublisher(options: API.Options = []) -> Future { Future { promise in - startJob(options: options, - completion: promise) + self.startJob(options: options, + completion: promise) } } } diff --git a/Sources/ParseSwift/Types/ParseConfig+combine.swift b/Sources/ParseSwift/Types/ParseConfig+combine.swift index fcfe63fcc..9d34ae299 100644 --- a/Sources/ParseSwift/Types/ParseConfig+combine.swift +++ b/Sources/ParseSwift/Types/ParseConfig+combine.swift @@ -23,8 +23,8 @@ public extension ParseConfig { */ func fetchPublisher(options: API.Options = []) -> Future { Future { promise in - fetch(options: options, - completion: promise) + self.fetch(options: options, + completion: promise) } } @@ -37,8 +37,8 @@ public extension ParseConfig { */ func savePublisher(options: API.Options = []) -> Future { Future { promise in - save(options: options, - completion: promise) + self.save(options: options, + completion: promise) } } } diff --git a/Sources/ParseSwift/Types/ParseFile+combine.swift b/Sources/ParseSwift/Types/ParseFile+combine.swift index c4ead77e7..05748b1b4 100644 --- a/Sources/ParseSwift/Types/ParseFile+combine.swift +++ b/Sources/ParseSwift/Types/ParseFile+combine.swift @@ -21,8 +21,8 @@ public extension ParseFile { */ func fetchPublisher(options: API.Options = []) -> Future { Future { promise in - fetch(options: options, - completion: promise) + self.fetch(options: options, + completion: promise) } } @@ -38,9 +38,9 @@ public extension ParseFile { progress: @escaping ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)) -> Future { Future { promise in - fetch(options: options, - progress: progress, - completion: promise) + self.fetch(options: options, + progress: progress, + completion: promise) } } @@ -54,8 +54,8 @@ public extension ParseFile { */ func savePublisher(options: API.Options = []) -> Future { Future { promise in - save(options: options, - completion: promise) + self.save(options: options, + completion: promise) } } @@ -72,9 +72,9 @@ public extension ParseFile { func savePublisher(options: API.Options = [], progress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil) -> Future { Future { promise in - save(options: options, - progress: progress, - completion: promise) + self.save(options: options, + progress: progress, + completion: promise) } } @@ -86,7 +86,7 @@ public extension ParseFile { */ func deletePublisher(options: API.Options = []) -> Future { Future { promise in - delete(options: options, completion: promise) + self.delete(options: options, completion: promise) } } } diff --git a/Sources/ParseSwift/Types/Query+combine.swift b/Sources/ParseSwift/Types/Query+combine.swift index 1c8109f26..4e31c3e99 100644 --- a/Sources/ParseSwift/Types/Query+combine.swift +++ b/Sources/ParseSwift/Types/Query+combine.swift @@ -23,8 +23,8 @@ public extension Query { */ func findPublisher(options: API.Options = []) -> Future<[ResultType], ParseError> { Future { promise in - find(options: options, - completion: promise) + self.find(options: options, + completion: promise) } } @@ -39,10 +39,10 @@ public extension Query { hint: String? = nil, options: API.Options = []) -> Future { Future { promise in - find(explain: explain, - hint: hint, - options: options, - completion: promise) + self.find(explain: explain, + hint: hint, + options: options, + completion: promise) } } @@ -53,8 +53,8 @@ public extension Query { */ func firstPublisher(options: API.Options = []) -> Future { Future { promise in - first(options: options, - completion: promise) + self.first(options: options, + completion: promise) } } @@ -69,10 +69,10 @@ public extension Query { hint: String? = nil, options: API.Options = []) -> Future { Future { promise in - first(explain: explain, - hint: hint, - options: options, - completion: promise) + self.first(explain: explain, + hint: hint, + options: options, + completion: promise) } } @@ -83,8 +83,8 @@ public extension Query { */ func countPublisher(options: API.Options = []) -> Future { Future { promise in - count(options: options, - completion: promise) + self.count(options: options, + completion: promise) } } @@ -99,10 +99,10 @@ public extension Query { hint: String? = nil, options: API.Options = []) -> Future { Future { promise in - count(explain: explain, - hint: hint, - options: options, - completion: promise) + self.count(explain: explain, + hint: hint, + options: options, + completion: promise) } } @@ -116,9 +116,9 @@ public extension Query { func aggregatePublisher(_ pipeline: AggregateType, options: API.Options = []) -> Future<[ResultType], ParseError> { Future { promise in - aggregate(pipeline, - options: options, - completion: promise) + self.aggregate(pipeline, + options: options, + completion: promise) } } } From 39e797840a8a0bc471c870d4a7fdde94f049a914 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 30 Jan 2021 18:44:31 -0500 Subject: [PATCH 12/15] Add minimum catalyst support, nits, prepare for release --- CHANGELOG.md | 11 +++++++++- ParseSwift.podspec | 2 +- ParseSwift.xcodeproj/project.pbxproj | 16 +++++++------- Scripts/jazzy.sh | 2 +- .../Authentication/3rd Party/ParseApple.swift | 8 +++---- .../Internal/ParseAnonymous.swift | 4 ++-- .../ParseAuthentication+combine.swift | 4 ++-- .../Protocols/ParseAuthentication.swift | 8 +++---- .../LiveQuery/LiveQuerySocket.swift | 14 ++++++------ .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 22 +++++++++---------- .../Protocols/LiveQuerySocketDelegate.swift | 2 +- .../Protocols/ParseLiveQueryDelegate.swift | 4 ++-- .../ParseSwift/LiveQuery/Subscription.swift | 2 +- .../Objects/ParseInstallation+combine.swift | 6 ++--- .../Objects/ParseObject+combine.swift | 6 ++--- .../Objects/ParseUser+combine.swift | 6 ++--- .../Operations/ParseOperation+combine.swift | 2 +- .../ParseSwift/Types/ParseCloud+combine.swift | 2 +- .../Types/ParseConfig+combine.swift | 2 +- .../ParseSwift/Types/ParseFile+combine.swift | 2 +- Sources/ParseSwift/Types/Query+combine.swift | 2 +- Sources/ParseSwift/Types/Query.swift | 4 ++-- .../ParseAnonymousCombineTests.swift | 2 +- .../ParseAppleCombineTests.swift | 2 +- .../ParseAuthenticationTests.swift | 5 +++-- .../ParseCloudCombineTests.swift | 2 +- .../ParseConfigCombineTests.swift | 2 +- .../ParseFileCombineTests.swift | 2 +- .../ParseInstallationCombineTests.swift | 2 +- .../ParseSwiftTests/ParseLiveQueryTests.swift | 2 +- .../ParseSwiftTests/ParseObjectCombine.swift | 2 +- .../ParseOperationCombineTests.swift | 2 +- .../ParseQueryCombineTests.swift | 2 +- .../ParseUserCombineTests.swift | 2 +- 34 files changed, 84 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c3ba161..c1854a690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,18 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.2...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.3...main) * _Contributing to this repo? Add info about your change here to be included in next release_ +### 1.1.3 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.2...1.1.3) + +__New features__ +- SwiftUI ready! ([#73](https://github.com/parse-community/Parse-Swift/pull/73)), thanks to [Corey Baker](https://github.com/cbaker6). + +__Fixes__ +- Fixes some issues with `ParseUser.logout` ([#73](https://github.com/parse-community/Parse-Swift/pull/73)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 1.1.2 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.1...1.1.2) diff --git a/ParseSwift.podspec b/ParseSwift.podspec index f81957ee8..c4f5a64c9 100644 --- a/ParseSwift.podspec +++ b/ParseSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "ParseSwift" - s.version = "1.1.2" + s.version = "1.1.3" s.summary = "Parse Pure Swift SDK" s.homepage = "https://github.com/parse-community/Parse-Swift" s.authors = { diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 8f837af54..1de6d6c61 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -2225,7 +2225,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2247,7 +2247,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2311,7 +2311,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2335,7 +2335,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2480,7 +2480,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; @@ -2508,7 +2508,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; PRODUCT_NAME = ParseSwift; @@ -2534,7 +2534,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; @@ -2561,7 +2561,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; PRODUCT_NAME = ParseSwift; diff --git a/Scripts/jazzy.sh b/Scripts/jazzy.sh index f7cdfedc8..63ca4af7c 100755 --- a/Scripts/jazzy.sh +++ b/Scripts/jazzy.sh @@ -5,7 +5,7 @@ bundle exec jazzy \ --author_url http://parseplatform.org \ --github_url https://github.com/parse-community/Parse-Swift \ --root-url http://parseplatform.org/Parse-Swift/api/ \ - --module-version 1.1.2 \ + --module-version 1.1.3 \ --theme fullwidth \ --skip-undocumented \ --output ./docs/api \ diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift index 4d14ed6c2..05eaa5891 100644 --- a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift @@ -103,7 +103,7 @@ public extension ParseApple { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func loginPublisher(user: String, identityToken: String, options: API.Options = []) -> Future { @@ -111,7 +111,7 @@ public extension ParseApple { options: options) } - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func loginPublisher(authData: [String: String]?, options: API.Options = []) -> Future { guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), @@ -181,7 +181,7 @@ public extension ParseApple { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func linkPublisher(user: String, identityToken: String, options: API.Options = []) -> Future { @@ -189,7 +189,7 @@ public extension ParseApple { options: options) } - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func linkPublisher(authData: [String: String]?, options: API.Options = []) -> Future { guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), diff --git a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift index a927c374c..f8b84b1b3 100644 --- a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift +++ b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift @@ -74,7 +74,7 @@ public extension ParseAnonymous { #if canImport(Combine) - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func loginPublisher(authData: [String: String]? = nil, options: API.Options = []) -> Future { AuthenticatedUser.loginPublisher(__type, @@ -99,7 +99,7 @@ public extension ParseAnonymous { #if canImport(Combine) - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func linkPublisher(authData: [String: String]?, options: API.Options) -> Future { Future { promise in diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift index 68f70cbba..558d0bfc3 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift @@ -11,7 +11,7 @@ import Foundation import Combine // MARK: Convenience Implementations - Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseAuthentication { func unlinkPublisher(_ user: AuthenticatedUser, @@ -30,7 +30,7 @@ public extension ParseAuthentication { } } -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseUser { // MARK: 3rd Party Authentication - Login Combine diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index 7eae38766..01957b0cf 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -105,7 +105,7 @@ public protocol ParseAuthentication: Codable { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func loginPublisher(authData: [String: String]?, options: API.Options) -> Future @@ -116,7 +116,7 @@ public protocol ParseAuthentication: Codable { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func linkPublisher(authData: [String: String]?, options: API.Options) -> Future @@ -128,7 +128,7 @@ public protocol ParseAuthentication: Codable { - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func unlinkPublisher(_ user: AuthenticatedUser, options: API.Options) -> Future @@ -139,7 +139,7 @@ public protocol ParseAuthentication: Codable { - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func unlinkPublisher(options: API.Options) -> Future #endif diff --git a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift index 2de4525ce..20ef4af09 100644 --- a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift +++ b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift @@ -11,7 +11,7 @@ import Foundation import FoundationNetworking #endif -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) final class LiveQuerySocket: NSObject { private var session: URLSession! var delegates = [URLSessionWebSocketTask: LiveQuerySocketDelegate]() @@ -36,7 +36,7 @@ final class LiveQuerySocket: NSObject { } // MARK: Status -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { enum Status: String { case open @@ -45,7 +45,7 @@ extension LiveQuerySocket { } // MARK: Connect -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { func connect(task: URLSessionWebSocketTask, completion: @escaping (Error?) -> Void) throws { @@ -63,7 +63,7 @@ extension LiveQuerySocket { } // MARK: Send -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { func send(_ data: Data, task: URLSessionWebSocketTask, completion: @escaping (Error?) -> Void) { guard let encodedAsString = String(data: data, encoding: .utf8) else { @@ -80,7 +80,7 @@ extension LiveQuerySocket { } // MARK: Receive -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { func receive(_ task: URLSessionWebSocketTask) { @@ -105,13 +105,13 @@ extension LiveQuerySocket { } // MARK: URLSession -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension URLSession { static let liveQuery = LiveQuerySocket() } // MARK: URLSessionWebSocketDelegate -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket: URLSessionWebSocketDelegate { func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 4116df49d..a71d42a26 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -44,7 +44,7 @@ import FoundationNetworking running. Initializing new instances will create a new task/connection to the `ParseLiveQuery` server. When an instance is deinitialized it will automatically close it's connection gracefully. */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public final class ParseLiveQuery: NSObject { // Queues let synchronizationQueue: DispatchQueue @@ -211,7 +211,7 @@ public final class ParseLiveQuery: NSObject { } // MARK: Helpers -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { static var client = try? ParseLiveQuery() @@ -293,7 +293,7 @@ extension ParseLiveQuery { } // MARK: Delegate -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery: LiveQuerySocketDelegate { func status(_ status: LiveQuerySocket.Status) { @@ -483,7 +483,7 @@ extension ParseLiveQuery: LiveQuerySocketDelegate { } // MARK: Connection -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { /// Manually establish a connection to the `ParseLiveQuery` Server. @@ -557,7 +557,7 @@ extension ParseLiveQuery { } // MARK: SubscriptionRecord -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { class SubscriptionRecord { @@ -612,7 +612,7 @@ extension ParseLiveQuery { } // MARK: Subscribing -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { func subscribe(_ query: Query) throws -> Subscription { @@ -641,7 +641,7 @@ extension ParseLiveQuery { } // MARK: Unsubscribing -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { func unsubscribe(_ query: Query) throws where T: ParseObject { @@ -674,7 +674,7 @@ extension ParseLiveQuery { } // MARK: Updating -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { func update(_ handler: T) throws where T: ParseSubscription { @@ -691,7 +691,7 @@ extension ParseLiveQuery { } // MARK: ParseLiveQuery - Subscribe -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { #if canImport(Combine) /** @@ -760,7 +760,7 @@ public extension Query { } // MARK: ParseLiveQuery - Unsubscribe -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { /** Unsubscribes all current subscriptions for a given query on the default @@ -800,7 +800,7 @@ public extension Query { } // MARK: ParseLiveQuery - Update -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { /** Updates an existing subscription with a new query on the default `ParseLiveQuery` client. diff --git a/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift b/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift index 1ab766170..e19070e67 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift @@ -11,7 +11,7 @@ import Foundation import FoundationNetworking #endif -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) protocol LiveQuerySocketDelegate: AnyObject { func status(_ status: LiveQuerySocket.Status) func close(useDedicatedQueue: Bool) diff --git a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift index 9a717802a..1580fcadc 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift @@ -14,7 +14,7 @@ import FoundationNetworking // swiftlint:disable line_length ///Receive/respond to notifications from the ParseLiveQuery Server. -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public protocol ParseLiveQueryDelegate: AnyObject { /** @@ -59,7 +59,7 @@ public protocol ParseLiveQueryDelegate: AnyObject { #endif } -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQueryDelegate { func received(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, diff --git a/Sources/ParseSwift/LiveQuery/Subscription.swift b/Sources/ParseSwift/LiveQuery/Subscription.swift index c5eb4160a..dfc090754 100644 --- a/Sources/ParseSwift/LiveQuery/Subscription.swift +++ b/Sources/ParseSwift/LiveQuery/Subscription.swift @@ -63,7 +63,7 @@ private func == (lhs: Event, rhs: Event) -> Bool { indepedently as a ViewModel in MVVM. Also provides a publisher for pull responses of query such as: `find`, `first`, `count`, and `aggregate`. */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) open class Subscription: ParseSubscription, ObservableObject { //The query subscribed to. public var query: Query diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 0debbddc1..a3ab82a76 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -11,7 +11,7 @@ import Foundation import Combine // MARK: Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseInstallation { // MARK: Fetchable - Combine @@ -61,7 +61,7 @@ public extension ParseInstallation { } // MARK: Batch Support - Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Sequence where Element: ParseInstallation { /** Fetches a collection of installations *aynchronously* with the current data from the server and sets @@ -93,7 +93,7 @@ public extension Sequence where Element: ParseInstallation { } /** - Deletes a collection of installations *asynchronously* and and publishes when complete. + Deletes a collection of installations *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift index 5551fb08d..973fe4e43 100644 --- a/Sources/ParseSwift/Objects/ParseObject+combine.swift +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -11,7 +11,7 @@ import Foundation import Combine // MARK: Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseObject { /** @@ -58,7 +58,7 @@ public extension ParseObject { } // MARK: Batch Support - Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Sequence where Element: ParseObject { /** Fetches a collection of objects *aynchronously* with the current data from the server and sets @@ -90,7 +90,7 @@ public extension Sequence where Element: ParseObject { } /** - Deletes a collection of objects *asynchronously* and and publishes when complete. + Deletes a collection of objects *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index f059a6716..f1fcbd114 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -10,7 +10,7 @@ import Foundation import Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseUser { // MARK: Signing Out - Combine @@ -182,7 +182,7 @@ public extension ParseUser { } // MARK: Batch Support - Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Sequence where Element: ParseUser { /** Fetches a collection of users *aynchronously* with the current data from the server and sets @@ -214,7 +214,7 @@ public extension Sequence where Element: ParseUser { } /** - Deletes a collection of users *asynchronously* and and publishes when complete. + Deletes a collection of users *asynchronously* and publishes when complete. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. diff --git a/Sources/ParseSwift/Operations/ParseOperation+combine.swift b/Sources/ParseSwift/Operations/ParseOperation+combine.swift index 9c8906ae1..cef9b79e2 100644 --- a/Sources/ParseSwift/Operations/ParseOperation+combine.swift +++ b/Sources/ParseSwift/Operations/ParseOperation+combine.swift @@ -10,7 +10,7 @@ import Foundation import Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseOperation { // MARK: Savable - Combine diff --git a/Sources/ParseSwift/Types/ParseCloud+combine.swift b/Sources/ParseSwift/Types/ParseCloud+combine.swift index 070572c28..ef3405af5 100644 --- a/Sources/ParseSwift/Types/ParseCloud+combine.swift +++ b/Sources/ParseSwift/Types/ParseCloud+combine.swift @@ -11,7 +11,7 @@ import Foundation import Combine // MARK: Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseCloud { // MARK: Functions - Combine diff --git a/Sources/ParseSwift/Types/ParseConfig+combine.swift b/Sources/ParseSwift/Types/ParseConfig+combine.swift index 9d34ae299..13ea16656 100644 --- a/Sources/ParseSwift/Types/ParseConfig+combine.swift +++ b/Sources/ParseSwift/Types/ParseConfig+combine.swift @@ -11,7 +11,7 @@ import Foundation import Combine // MARK: Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseConfig { // MARK: Fetchable - Combine diff --git a/Sources/ParseSwift/Types/ParseFile+combine.swift b/Sources/ParseSwift/Types/ParseFile+combine.swift index 05748b1b4..b8852ba40 100644 --- a/Sources/ParseSwift/Types/ParseFile+combine.swift +++ b/Sources/ParseSwift/Types/ParseFile+combine.swift @@ -11,7 +11,7 @@ import Foundation import Combine // MARK: Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension ParseFile { /** diff --git a/Sources/ParseSwift/Types/Query+combine.swift b/Sources/ParseSwift/Types/Query+combine.swift index 4e31c3e99..d7b591231 100644 --- a/Sources/ParseSwift/Types/Query+combine.swift +++ b/Sources/ParseSwift/Types/Query+combine.swift @@ -11,7 +11,7 @@ import Foundation import Combine // MARK: Combine -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { // MARK: Queryable - Combine diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index cfd2c8cc2..4bed40fbe 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -754,7 +754,7 @@ public struct Query: Encodable, Equatable where T: ParseObject { - warning: This is only for `ParseLiveQuery`. - parameter keys: A variadic list of fields to receive back instead of the whole `ParseObject`. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: String...) -> Query { var mutableQuery = self mutableQuery.fields = keys @@ -771,7 +771,7 @@ public struct Query: Encodable, Equatable where T: ParseObject { - warning: This is only for `ParseLiveQuery`. - parameter keys: An array of fields to receive back instead of the whole `ParseObject`. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: [String]) -> Query { var mutableQuery = self mutableQuery.fields = keys diff --git a/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift index 91160ccc8..f1dff0e3e 100644 --- a/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseAuthenticationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct User: ParseUser { diff --git a/Tests/ParseSwiftTests/ParseAppleCombineTests.swift b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift index cb5b3059a..feebc740c 100644 --- a/Tests/ParseSwiftTests/ParseAppleCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseAppleCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct User: ParseUser { diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift index 59b0f3fc8..ca2d0f1ad 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -51,7 +51,7 @@ class ParseAuthenticationTests: XCTestCase { } #if canImport(Combine) - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func loginPublisher(authData: [String: String]?, options: API.Options) -> Future { let error = ParseError(code: .unknownError, message: "Not implemented") @@ -59,7 +59,8 @@ class ParseAuthenticationTests: XCTestCase { promise(.failure(error)) } } - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) func linkPublisher(authData: [String: String]?, options: API.Options) -> Future { let error = ParseError(code: .unknownError, message: "Not implemented") return Future { promise in diff --git a/Tests/ParseSwiftTests/ParseCloudCombineTests.swift b/Tests/ParseSwiftTests/ParseCloudCombineTests.swift index be1ae445e..367d79bf1 100644 --- a/Tests/ParseSwiftTests/ParseCloudCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseCloudCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseCloudCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct Cloud: ParseCloud { diff --git a/Tests/ParseSwiftTests/ParseConfigCombineTests.swift b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift index 5c7b84673..7b2eea0e9 100644 --- a/Tests/ParseSwiftTests/ParseConfigCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseConfigCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct Config: ParseConfig { diff --git a/Tests/ParseSwiftTests/ParseFileCombineTests.swift b/Tests/ParseSwiftTests/ParseFileCombineTests.swift index a97e7f02d..4a8162d02 100644 --- a/Tests/ParseSwiftTests/ParseFileCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseFileCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseFileCombineTests: XCTestCase { // swiftlint:disable:this type_body_length let temporaryDirectory = "\(NSTemporaryDirectory())test/" diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index 04eef6330..01655f78a 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct User: ParseUser { diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index d9c693f3d..9de6683dd 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -10,7 +10,7 @@ import Foundation import XCTest @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseLiveQueryTests: XCTestCase { struct GameScore: ParseObject { //: Those are required for Object diff --git a/Tests/ParseSwiftTests/ParseObjectCombine.swift b/Tests/ParseSwiftTests/ParseObjectCombine.swift index 16943deb8..2d70b0666 100644 --- a/Tests/ParseSwiftTests/ParseObjectCombine.swift +++ b/Tests/ParseSwiftTests/ParseObjectCombine.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseObjectCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct GameScore: ParseObject { diff --git a/Tests/ParseSwiftTests/ParseOperationCombineTests.swift b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift index 37ad0cbcb..8067b6437 100644 --- a/Tests/ParseSwiftTests/ParseOperationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseOperationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct GameScore: ParseObject { diff --git a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift index 85e67cc97..f176fc76a 100644 --- a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseQueryCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct GameScore: ParseObject { diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 0e540630d..b505d4207 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -13,7 +13,7 @@ import XCTest import Combine @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_length struct User: ParseUser { From 15e7ae14a0527ade111b4f533ab6b5d85312e8cd Mon Sep 17 00:00:00 2001 From: Corey Date: Sat, 30 Jan 2021 22:01:45 -0500 Subject: [PATCH 13/15] Nit --- Sources/ParseSwift/LiveQuery/Subscription.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/ParseSwift/LiveQuery/Subscription.swift b/Sources/ParseSwift/LiveQuery/Subscription.swift index dfc090754..e23264987 100644 --- a/Sources/ParseSwift/LiveQuery/Subscription.swift +++ b/Sources/ParseSwift/LiveQuery/Subscription.swift @@ -60,8 +60,7 @@ private func == (lhs: Event, rhs: Event) -> Bool { /** A default implementation of the `ParseSubscription` protocol. Suitable for `ObjectObserved` as the subscription can be used as a SwiftUI publisher. Meaning it can serve - indepedently as a ViewModel in MVVM. Also provides a publisher for pull responses of query such as: - `find`, `first`, `count`, and `aggregate`. + indepedently as a ViewModel in MVVM. */ @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) open class Subscription: ParseSubscription, ObservableObject { From f34f34fd3c58eaba8f065bde1311a55f48de4d35 Mon Sep 17 00:00:00 2001 From: Corey Date: Sun, 31 Jan 2021 12:17:00 -0500 Subject: [PATCH 14/15] Update Sources/ParseSwift/Types/ParseFile+combine.swift Co-authored-by: Tom Fox <13188249+TomWFox@users.noreply.github.com> --- Sources/ParseSwift/Types/ParseFile+combine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Types/ParseFile+combine.swift b/Sources/ParseSwift/Types/ParseFile+combine.swift index b8852ba40..d22be7d9e 100644 --- a/Sources/ParseSwift/Types/ParseFile+combine.swift +++ b/Sources/ParseSwift/Types/ParseFile+combine.swift @@ -79,7 +79,7 @@ public extension ParseFile { } /** - Deletes the file from the Parse cloud. Publishes when complete. + Deletes the file from the Parse Server. Publishes when complete. - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. From 738eccdf6ba7510713b23fda39f7ea7558bcc760 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sun, 31 Jan 2021 14:29:06 -0500 Subject: [PATCH 15/15] Bring Xcode, SPM, and Cocoapods minimum deployments to the same level: [.iOS(.v12), .macOS(.v10_13), .tvOS(.v12), .watchOS(.v5)] --- Package.swift | 2 +- ParseSwift.podspec | 6 +++--- ParseSwift.xcodeproj/project.pbxproj | 16 ++++++++++++++-- .../Authentication/3rd Party/ParseApple.swift | 6 +++--- .../ParseSwift/Objects/ParseUser+combine.swift | 3 ++- .../ParseAuthenticationTests.swift | 3 ++- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index 1db075989..2d267e373 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "ParseSwift", - platforms: [.iOS(.v11), .macOS(.v10_13), .tvOS(.v11), .watchOS(.v4)], + platforms: [.iOS(.v12), .macOS(.v10_13), .tvOS(.v12), .watchOS(.v5)], products: [ .library( name: "ParseSwift", diff --git a/ParseSwift.podspec b/ParseSwift.podspec index c4f5a64c9..fa204bd7b 100644 --- a/ParseSwift.podspec +++ b/ParseSwift.podspec @@ -10,10 +10,10 @@ Pod::Spec.new do |s| :git => "#{s.homepage}.git", :tag => "#{s.version}", } - s.ios.deployment_target = "11.0" + s.ios.deployment_target = "12.0" s.osx.deployment_target = "10.13" - s.tvos.deployment_target = "11.0" - s.watchos.deployment_target = "4.0" + s.tvos.deployment_target = "12.0" + s.watchos.deployment_target = "5.0" s.swift_versions = ['5.1', '5.2', '5.3'] s.source_files = "Sources/ParseSwift/**/*.swift" s.license = { diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 1de6d6c61..0d0bcdfd6 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -2232,6 +2232,8 @@ SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2254,6 +2256,8 @@ SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; @@ -2318,6 +2322,8 @@ SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2342,6 +2348,8 @@ SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; @@ -2490,7 +2498,8 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2517,7 +2526,8 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; @@ -2545,6 +2555,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2571,6 +2582,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift index 05eaa5891..fa872a43e 100644 --- a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift @@ -186,7 +186,7 @@ public extension ParseApple { identityToken: String, options: API.Options = []) -> Future { linkPublisher(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), - options: options) + options: options) } @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) @@ -201,8 +201,8 @@ public extension ParseApple { } } return AuthenticatedUser.linkPublisher(Self.__type, - authData: authData, - options: options) + authData: authData, + options: options) } #endif diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index f1fcbd114..3b02f554a 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -114,7 +114,8 @@ public extension ParseUser { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. */ - static func passwordResetPublisher(email: String, options: API.Options = []) -> Future { + static func passwordResetPublisher(email: String, + options: API.Options = []) -> Future { Future { promise in Self.passwordReset(email: email, options: options, completion: promise) } diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift index ca2d0f1ad..e7d95475f 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -61,7 +61,8 @@ class ParseAuthenticationTests: XCTestCase { } @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) - func linkPublisher(authData: [String: String]?, options: API.Options) -> Future { + func linkPublisher(authData: [String: String]?, + options: API.Options) -> Future { let error = ParseError(code: .unknownError, message: "Not implemented") return Future { promise in promise(.failure(error))