From 0e2c5c8f80434c75ddbd21801a1dfeb72948e699 Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Mon, 27 Mar 2017 11:25:23 -0700 Subject: [PATCH 01/11] MLIBZ-1311: Grouping / aggregation functions Former-commit-id: e85be50aa02297131ab78dc16d854c788793011f --- Cartfile.resolved | 2 +- Kinvey/Kinvey.xcodeproj/project.pbxproj | 8 +- Kinvey/Kinvey/AggregateOperation.swift | 164 +++++++++ Kinvey/Kinvey/Cache.swift | 178 ++++++---- Kinvey/Kinvey/CacheManager.swift | 4 +- Kinvey/Kinvey/CountOperation.swift | 4 +- Kinvey/Kinvey/DataStore.swift | 94 +++++- Kinvey/Kinvey/Endpoint.swift | 3 + Kinvey/Kinvey/FindOperation.swift | 10 +- Kinvey/Kinvey/GetOperation.swift | 6 +- Kinvey/Kinvey/HttpRequest.swift | 5 + Kinvey/Kinvey/HttpRequestFactory.swift | 14 + Kinvey/Kinvey/MemoryCache.swift | 59 +++- Kinvey/Kinvey/Operation.swift | 6 +- Kinvey/Kinvey/PullOperation.swift | 2 +- Kinvey/Kinvey/PurgeOperation.swift | 6 +- Kinvey/Kinvey/PushOperation.swift | 8 +- Kinvey/Kinvey/ReadOperation.swift | 2 +- Kinvey/Kinvey/RealmCache.swift | 50 +-- Kinvey/Kinvey/RemoveByIdOperation.swift | 2 +- Kinvey/Kinvey/RemoveByQueryOperation.swift | 2 +- Kinvey/Kinvey/RemoveOperation.swift | 10 +- Kinvey/Kinvey/RequestFactory.swift | 1 + Kinvey/Kinvey/SaveOperation.swift | 10 +- Kinvey/Kinvey/SyncOperation.swift | 2 +- Kinvey/Kinvey/WriteOperation.swift | 2 +- .../KinveyTests/DeltaSetCacheTestCase.swift | 8 +- Kinvey/KinveyTests/KinveyTestCase.swift | 6 +- Kinvey/KinveyTests/NetworkStoreTests.swift | 318 ++++++++++++++++++ Kinvey/KinveyTests/SyncStoreTests.swift | 6 +- 30 files changed, 835 insertions(+), 157 deletions(-) create mode 100644 Kinvey/Kinvey/AggregateOperation.swift diff --git a/Cartfile.resolved b/Cartfile.resolved index e6f5d2068..5008ac5ed 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -4,4 +4,4 @@ github "tjboneman/NSPredicate-MongoDB-Adaptor" "2444d4a790527eb5c9fcb4e4f7b4af41 github "Hearst-DD/ObjectMapper" "2.2.5" github "mxcl/PromiseKit" "4.1.8" github "DaveWoodCom/XCGLogger" "4.0.0" -github "realm/realm-cocoa" "v2.5.0" +github "realm/realm-cocoa" "v2.5.1" diff --git a/Kinvey/Kinvey.xcodeproj/project.pbxproj b/Kinvey/Kinvey.xcodeproj/project.pbxproj index b2dedc3c4..007dd606f 100644 --- a/Kinvey/Kinvey.xcodeproj/project.pbxproj +++ b/Kinvey/Kinvey.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 5706D9971DDFFC94009836D5 /* MockKinveyBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57136F621D5D23BF00731DDB /* MockKinveyBackend.swift */; }; 5706D9981DDFFC94009836D5 /* MockKinveyBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57136F621D5D23BF00731DDB /* MockKinveyBackend.swift */; }; 57089DD71D5CE80D00A36035 /* PullOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57089DD61D5CE80D00A36035 /* PullOperation.swift */; }; + 570BD2FD1E845E7A000341C9 /* AggregateOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570BD2FC1E845E7A000341C9 /* AggregateOperation.swift */; }; + 5711E1221E979EEF003351F0 /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D643471CAB3C8A00F6D16E /* MemoryCache.swift */; }; 57136F631D5D23BF00731DDB /* MockKinveyBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57136F621D5D23BF00731DDB /* MockKinveyBackend.swift */; }; 5714EBB01CCECE35001E3ECF /* AclTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5714EBAF1CCECE35001E3ECF /* AclTestCase.swift */; }; 5714EBB21CCEEAF9001E3ECF /* RemoveByIdOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5714EBB11CCEEAF9001E3ECF /* RemoveByIdOperation.swift */; }; @@ -229,7 +231,6 @@ 57D643401CA328F800F6D16E /* KinveyTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57A27C901C178F18000DF951 /* KinveyTestCase.swift */; }; 57D643411CA32CAF00F6D16E /* KIF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578F5C921C99EED100B20F17 /* KIF.swift */; }; 57D643421CA32CAF00F6D16E /* KIF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578F5C921C99EED100B20F17 /* KIF.swift */; }; - 57D643481CAB3C8A00F6D16E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D643471CAB3C8A00F6D16E /* MemoryCache.swift */; }; 57DB87E81C62B0F6002BA684 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DB87E71C62B0F6002BA684 /* Data.swift */; }; 57E1C3A31C17B3FF00578974 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E1C3A21C17B3FF00578974 /* Query.swift */; }; 57E1C3A71C17B4F300578974 /* Persistable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E1C3A61C17B4F300578974 /* Persistable.swift */; }; @@ -551,6 +552,7 @@ /* Begin PBXFileReference section */ 5706FEC21C1F9A6D0037E7D0 /* StoreTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreTestCase.swift; sourceTree = ""; }; 57089DD61D5CE80D00A36035 /* PullOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PullOperation.swift; sourceTree = ""; }; + 570BD2FC1E845E7A000341C9 /* AggregateOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateOperation.swift; sourceTree = ""; }; 5711C46C1C74F52F00073806 /* SaveOperationTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveOperationTest.swift; sourceTree = ""; }; 57136F621D5D23BF00731DDB /* MockKinveyBackend.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockKinveyBackend.swift; sourceTree = ""; }; 5714EBAF1CCECE35001E3ECF /* AclTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AclTestCase.swift; sourceTree = ""; }; @@ -926,6 +928,7 @@ 574B0FA61C729EAF00CDC48F /* GetOperation.swift */, 574B0FA81C729EB900CDC48F /* FindOperation.swift */, 57F91C281D6E2C020012850A /* CountOperation.swift */, + 570BD2FC1E845E7A000341C9 /* AggregateOperation.swift */, 574B0FAA1C729EC900CDC48F /* SaveOperation.swift */, 57FF4F6C1CCFE71B002947FF /* RemoveOperation.swift */, 574B0FAC1C729F3300CDC48F /* RemoveByQueryOperation.swift */, @@ -2057,6 +2060,7 @@ 57A27CA21C17910E000DF951 /* Acl.swift in Sources */, 57AC52881D395F7D000887D3 /* AuthSource.swift in Sources */, 57E1C3A31C17B3FF00578974 /* Query.swift in Sources */, + 570BD2FD1E845E7A000341C9 /* AggregateOperation.swift in Sources */, 57A27C9E1C178FB5000DF951 /* Client.swift in Sources */, 57E7C7A51C504AC500848748 /* Cache.swift in Sources */, 57557FE61D185BC3001D991C /* RealmResultsArray.swift in Sources */, @@ -2075,7 +2079,6 @@ 57FEB6B91C8E480300B43FC0 /* SyncOperation.swift in Sources */, 57A2ED8E1C49D20B006D26A9 /* HttpRequest.swift in Sources */, 57C2F1731D5E5A68005A214B /* BuilderType.swift in Sources */, - 57D643481CAB3C8A00F6D16E /* MemoryCache.swift in Sources */, 57BB56B61C4D8E8400F6B548 /* LocalResponse.swift in Sources */, 57B0E9501C5FF52200BA984F /* Push.swift in Sources */, 574912711C59323B00EA4F26 /* StoreType.swift in Sources */, @@ -2139,6 +2142,7 @@ 5781D1361CE3D0BA00369F40 /* FileTestCase.swift in Sources */, 578F5C911C99EE5C00B20F17 /* DeltaSetCacheTestCase.swift in Sources */, 57873DEC1DFF3FDC002C87BF /* PushTestCase.swift in Sources */, + 5711E1221E979EEF003351F0 /* MemoryCache.swift in Sources */, 576A1D361CCA92CA006B261E /* DataTypeTestCase.swift in Sources */, 57A960A11CC6D6FE005E52A8 /* JsonTestCase.swift in Sources */, 57136F631D5D23BF00731DDB /* MockKinveyBackend.swift in Sources */, diff --git a/Kinvey/Kinvey/AggregateOperation.swift b/Kinvey/Kinvey/AggregateOperation.swift new file mode 100644 index 000000000..33a9dfd9c --- /dev/null +++ b/Kinvey/Kinvey/AggregateOperation.swift @@ -0,0 +1,164 @@ +// +// AggregateOperation.swift +// Kinvey +// +// Created by Victor Hugo on 2017-03-23. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import Foundation + +class AggregateOperation: ReadOperation, ReadOperationType where T: NSObject { + + let aggregation: Aggregation + let predicate: NSPredicate? + + init(aggregation: Aggregation, condition predicate: NSPredicate? = nil, readPolicy: ReadPolicy, cache: AnyCache?, client: Client) { + self.aggregation = aggregation + self.predicate = predicate + super.init(readPolicy: readPolicy, cache: cache, client: client) + } + + func executeLocal(_ completionHandler: (([JsonDictionary]?, Swift.Error?) -> Void)? = nil) -> Request { + let request = LocalRequest() + request.execute { () -> Void in + if let cache = self.cache { + let result = cache.group(aggregation: aggregation, predicate: predicate) + completionHandler?(result, nil) + } else { + completionHandler?([], nil) + } + } + return request + } + + func executeNetwork(_ completionHandler: (([JsonDictionary]?, Swift.Error?) -> Void)? = nil) -> Request { + let request = client.networkRequestFactory.buildAppDataGroup(collectionName: T.collectionName(), keys: aggregation.keys, initialObject: aggregation.initialObject, reduceJSFunction: aggregation.reduceJSFunction, condition: predicate) + request.execute() { data, response, error in + if let response = response, response.isOK, + let data = data, + let json = try? JSONSerialization.jsonObject(with: data), + let result = json as? [JsonDictionary] + { + completionHandler?(result, nil) + } else { + completionHandler?(nil, buildError(data, response, error, self.client)) + } + } + return request + } + +} + +enum Aggregation { + + case custom(keys: [String], initialObject: JsonDictionary, reduceJSFunction: String) + case count(keys: [String]) + case sum(keys: [String], sum: String) + case avg(keys: [String], avg: String) + case min(keys: [String], min: String) + case max(keys: [String], max: String) + + var keys: [String] { + switch self { + case .custom(let keys, _, _), + .count(let keys), + .sum(let keys, _), + .avg(let keys, _), + .min(let keys, _), + .max(let keys, _): + return keys + } + } + + var resultKey: String { + switch self { + case .custom(_, _, _): + fatalError("Custom does not have a resultKey") + case .count: + return "count" + case .sum: + return "sum" + case .avg: + return "avg" + case .min: + return "min" + case .max: + return "max" + } + } + + var initialObject: JsonDictionary { + switch self { + case .custom(_, let initialObject, _): + return initialObject + case .count: + return [resultKey : 0] + case .sum: + return [resultKey : 0.0] + case .avg: + return ["sum" : 0.0, "count" : 0] + case .min: + return [resultKey : "Infinity"] + case .max: + return [resultKey : "-Infinity"] + } + } + + var reduceJSFunction: String { + switch self { + case .custom(_, _, let reduceJSFunction): + return reduceJSFunction + case .count(_): + return "function(doc, out) { out.\(resultKey)++; }" + case .sum(_, let sum): + return "function(doc, out) { out.\(resultKey) += doc.\(sum); }" + case .avg(_, let avg): + return "function(doc, out) { out.count++; out.sum += doc.\(avg); out.\(resultKey) = out.sum / out.count; }" + case .min(_, let min): + return "function(doc, out) { out.\(resultKey) = Math.min(out.\(resultKey), doc.\(min)); }" + case .max(_, let max): + return "function(doc, out) { out.\(resultKey) = Math.max(out.\(resultKey), doc.\(max)); }" + } + } + +} + +public typealias AggregationCustomResult = (value: T, custom: JsonDictionary) + +public protocol CountType {} +extension Int: CountType {} +extension Int8: CountType {} +extension Int16: CountType {} +extension Int32: CountType {} +extension Int64: CountType {} + +public typealias AggregationCountResult = (value: T, count: Count) + +public protocol AddableType {} +extension NSNumber: AddableType {} +extension Double: AddableType {} +extension Float: AddableType {} +extension Int: AddableType {} +extension Int8: AddableType {} +extension Int16: AddableType {} +extension Int32: AddableType {} +extension Int64: AddableType {} + +public typealias AggregationSumResult = (value: T, sum: Sum) +public typealias AggregationAvgResult = (value: T, avg: Avg) + +public protocol MinMaxType {} +extension NSNumber: MinMaxType {} +extension Double: MinMaxType {} +extension Float: MinMaxType {} +extension Int: MinMaxType {} +extension Int8: MinMaxType {} +extension Int16: MinMaxType {} +extension Int32: MinMaxType {} +extension Int64: MinMaxType {} +extension Date: MinMaxType {} +extension NSDate: MinMaxType {} + +public typealias AggregationMinResult = (value: T, min: Min) +public typealias AggregationMaxResult = (value: T, max: Max) diff --git a/Kinvey/Kinvey/Cache.swift b/Kinvey/Kinvey/Cache.swift index c4ca0a1d5..2d73b0281 100644 --- a/Kinvey/Kinvey/Cache.swift +++ b/Kinvey/Kinvey/Cache.swift @@ -8,49 +8,56 @@ import Foundation -internal protocol CacheType { +internal protocol CacheType: class { var persistenceId: String { get } var collectionName: String { get } var ttl: TimeInterval? { get set } - associatedtype `Type` + associatedtype `Type`: Persistable - func saveEntity(_ entity: Type) + func save(entity: Type) - func saveEntities(_ entities: [Type]) + func save(entities: [Type]) - func findEntity(_ objectId: String) -> Type? + func find(byId objectId: String) -> Type? - func findEntityByQuery(_ query: Query) -> [Type] + func find(byQuery query: Query) -> [Type] - func findIdsLmtsByQuery(_ query: Query) -> [String : String] + func findIdsLmts(byQuery query: Query) -> [String : String] func findAll() -> [Type] - func count(_ query: Query?) -> Int + func count(query: Query?) -> Int - func removeEntity(_ entity: Type) -> Bool + @discardableResult + func remove(entity: Type) -> Bool - func removeEntities(_ entity: [Type]) -> Bool + @discardableResult + func remove(entities: [Type]) -> Bool - func removeEntitiesByQuery(_ query: Query) -> Int + @discardableResult + func remove(byQuery query: Query) -> Int - func removeAllEntities() + func removeAll() func clear(query: Query?) + func detach(entities: [Type], query: Query?) -> [Type] + + func group(aggregation: Aggregation, predicate: NSPredicate?) -> [JsonDictionary] + } extension CacheType { func isEmpty() -> Bool { - return count(nil) == 0 + return count(query: nil) == 0 } } -internal class Cache: CacheType where T: NSObject { +internal class Cache where T: NSObject { internal typealias `Type` = T @@ -64,85 +71,126 @@ internal class Cache: CacheType where T: NSObject { self.ttl = ttl } - func detach(_ entity: [T], query: Query) -> [T] { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) +} + +class AnyCache: CacheType { + + var persistenceId: String { + return _getPersistenceId() } - func saveEntity(_ entity: T) { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + var collectionName: String { + return _getCollectionName() } - func saveEntities(_ entities: [T]) { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + var ttl: TimeInterval? { + get { + return _getTTL() + } + set { + _setTTL(newValue) + } } - func findEntity(_ objectId: String) -> T? { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + private let _getPersistenceId: () -> String + private let _getCollectionName: () -> String + private let _getTTL: () -> TimeInterval? + private let _setTTL: (TimeInterval?) -> Void + private let _saveEntity: (T) -> Void + private let _saveEntities: ([T]) -> Void + private let _findById: (String) -> T? + private let _findByQuery: (Query) -> [T] + private let _findIdsLmtsByQuery: (Query) -> [String : String] + private let _findAll: () -> [T] + private let _count: (Query?) -> Int + private let _removeEntity: (T) -> Bool + private let _removeEntities: ([T]) -> Bool + private let _removeByQuery: (Query) -> Int + private let _removeAll: () -> Void + private let _clear: (Query?) -> Void + private let _detach: ([T], Query?) -> [T] + private let _group: (Aggregation, NSPredicate?) -> [JsonDictionary] + + typealias `Type` = T + + init(_ cache: Cache) where Cache.`Type` == T { + _getPersistenceId = { return cache.persistenceId } + _getCollectionName = { return cache.collectionName } + _getTTL = { return cache.ttl } + _setTTL = { cache.ttl = $0 } + _saveEntity = cache.save(entity:) + _saveEntities = cache.save(entities:) + _findById = cache.find(byId:) + _findByQuery = cache.find(byQuery:) + _findIdsLmtsByQuery = cache.findIdsLmts(byQuery:) + _findAll = cache.findAll + _count = cache.count(query:) + _removeEntity = cache.remove(entity:) + _removeEntities = cache.remove(entities:) + _removeByQuery = cache.remove(byQuery:) + _removeAll = cache.removeAll + _clear = cache.clear(query:) + _detach = cache.detach(entities: query:) + _group = cache.group(aggregation: predicate:) } - func findEntityByQuery(_ query: Query) -> [T] { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func save(entity: T) { + _saveEntity(entity) } - func findIdsLmtsByQuery(_ query: Query) -> [String : String] { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func save(entities: [T]) { + _saveEntities(entities) + } + + func find(byId objectId: String) -> T? { + return _findById(objectId) + } + + func find(byQuery query: Query) -> [T] { + return _findByQuery(query) + } + + func findIdsLmts(byQuery query: Query) -> [String : String] { + return _findIdsLmtsByQuery(query) } func findAll() -> [T] { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + return _findAll() } - func count(_ query: Query? = nil) -> Int { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func count(query: Query?) -> Int { + return _count(query) } @discardableResult - func removeEntity(_ entity: T) -> Bool { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func remove(entity: T) -> Bool { + return _removeEntity(entity) } @discardableResult - func removeEntities(_ entity: [T]) -> Bool { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func remove(entities: [T]) -> Bool { + return _removeEntities(entities) } @discardableResult - func removeEntitiesByQuery(_ query: Query) -> Int { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func remove(byQuery query: Query) -> Int { + return _removeByQuery(query) + } + + func removeAll() { + _removeAll() + } + + func clear(query: Query?) { + _clear(query) } - func removeAllEntities() { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func detach(entities: [T], query: Query?) -> [T] { + return _detach(entities, query) } - func clear(query: Query? = nil) { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) + func group(aggregation: Aggregation, predicate: NSPredicate?) -> [JsonDictionary] { + return _group(aggregation, predicate) } } diff --git a/Kinvey/Kinvey/CacheManager.swift b/Kinvey/Kinvey/CacheManager.swift index bbe30e850..bc997f2cc 100644 --- a/Kinvey/Kinvey/CacheManager.swift +++ b/Kinvey/Kinvey/CacheManager.swift @@ -22,7 +22,7 @@ internal class CacheManager: NSObject { self.schemaVersion = schemaVersion } - func cache(fileURL: URL? = nil, type: T.Type) -> Cache? where T: NSObject { + func cache(fileURL: URL? = nil, type: T.Type) -> AnyCache? where T: NSObject { let fileManager = FileManager.default if let fileURL = fileURL { do { @@ -33,7 +33,7 @@ internal class CacheManager: NSObject { } } - return RealmCache(persistenceId: persistenceId, fileURL: fileURL, encryptionKey: encryptionKey, schemaVersion: schemaVersion) + return AnyCache(RealmCache(persistenceId: persistenceId, fileURL: fileURL, encryptionKey: encryptionKey, schemaVersion: schemaVersion)) } func fileCache(fileURL: URL? = nil) -> FileCache? { diff --git a/Kinvey/Kinvey/CountOperation.swift b/Kinvey/Kinvey/CountOperation.swift index bb2981d59..0dbf056a6 100644 --- a/Kinvey/Kinvey/CountOperation.swift +++ b/Kinvey/Kinvey/CountOperation.swift @@ -12,7 +12,7 @@ class CountOperation: ReadOperation, ReadOp let query: Query? - init(query: Query? = nil, readPolicy: ReadPolicy, cache: Cache?, client: Client) { + init(query: Query? = nil, readPolicy: ReadPolicy, cache: AnyCache?, client: Client) { self.query = query super.init(readPolicy: readPolicy, cache: cache, client: client) } @@ -21,7 +21,7 @@ class CountOperation: ReadOperation, ReadOp let request = LocalRequest() request.execute { () -> Void in if let cache = self.cache { - let count = cache.count(self.query) + let count = cache.count(query: self.query) completionHandler?(count, nil) } else { completionHandler?(0, nil) diff --git a/Kinvey/Kinvey/DataStore.swift b/Kinvey/Kinvey/DataStore.swift index 393a2f065..6b5a5e390 100644 --- a/Kinvey/Kinvey/DataStore.swift +++ b/Kinvey/Kinvey/DataStore.swift @@ -67,7 +67,7 @@ open class DataStore where T: NSObject { fileprivate let fileURL: URL? - internal let cache: Cache? + internal let cache: AnyCache? internal let sync: AnySync? fileprivate var deltaSet: Bool @@ -205,6 +205,98 @@ open class DataStore where T: NSObject { return request } + @discardableResult + open func group(keys: [String]? = nil, initialObject: JsonDictionary, reduceJSFunction: String, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationCustomResult]?, Swift.Error?) -> Void) -> Request { + let readPolicy = readPolicy ?? self.readPolicy + let keys = keys ?? [] + let aggregation: Aggregation = .custom(keys: keys, initialObject: initialObject, reduceJSFunction: reduceJSFunction) + let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) + let request = operation.execute { results, error in + let array = results?.map { + return AggregationCustomResult(value: T(JSON: $0)!, custom: $0) + } + let completionHandler = self.dispatchAsyncMainQueue(completionHandler) + completionHandler?(array, error) + } + return request + } + + @discardableResult + open func group(count keys: [String], countType: Count.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping + ([AggregationCountResult]?, Swift.Error?) -> Void) -> Request { + let readPolicy = readPolicy ?? self.readPolicy + let aggregation: Aggregation = .count(keys: keys) + let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) + let request = operation.execute { results, error in + let array = results?.map { + return AggregationCountResult(value: T(JSON: $0)!, count: $0[aggregation.resultKey] as! Count) + } + let completionHandler = self.dispatchAsyncMainQueue(completionHandler) + completionHandler?(array, error) + } + return request + } + + @discardableResult + open func group(keys: [String], sum: String, sumType: Sum.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationSumResult]?, Swift.Error?) -> Void) -> Request { + let readPolicy = readPolicy ?? self.readPolicy + let aggregation: Aggregation = .sum(keys: keys, sum: sum) + let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) + let request = operation.execute { results, error in + let array = results?.map { + return AggregationSumResult(value: T(JSON: $0)!, sum: $0[aggregation.resultKey] as! Sum) + } + let completionHandler = self.dispatchAsyncMainQueue(completionHandler) + completionHandler?(array, error) + } + return request + } + + @discardableResult + open func group(keys: [String], avg: String, avgType: Avg.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationAvgResult]?, Swift.Error?) -> Void) -> Request { + let readPolicy = readPolicy ?? self.readPolicy + let aggregation: Aggregation = .avg(keys: keys, avg: avg) + let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) + let request = operation.execute { results, error in + let array = results?.map { + return AggregationAvgResult(value: T(JSON: $0)!, avg: $0[aggregation.resultKey] as! Avg) + } + let completionHandler = self.dispatchAsyncMainQueue(completionHandler) + completionHandler?(array, error) + } + return request + } + + @discardableResult + open func group(keys: [String], min: String, minType: Min.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationMinResult]?, Swift.Error?) -> Void) -> Request { + let readPolicy = readPolicy ?? self.readPolicy + let aggregation: Aggregation = .min(keys: keys, min: min) + let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) + let request = operation.execute { results, error in + let array = results?.map { + return AggregationMinResult(value: T(JSON: $0)!, min: $0[aggregation.resultKey] as! Min) + } + let completionHandler = self.dispatchAsyncMainQueue(completionHandler) + completionHandler?(array, error) + } + return request + } + + @discardableResult + open func group(keys: [String], max: String, maxType: Max.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationMaxResult]?, Swift.Error?) -> Void) -> Request { + let readPolicy = readPolicy ?? self.readPolicy + let aggregation: Aggregation = .max(keys: keys, max: max) + let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) + let request = operation.execute { results, error in + let array = results?.map { + return AggregationMaxResult(value: T(JSON: $0)!, max: $0[aggregation.resultKey] as! Max) + } + let completionHandler = self.dispatchAsyncMainQueue(completionHandler) + completionHandler?(array, error) + } + return request + } + /// Creates or updates a record. @discardableResult open func save(_ persistable: inout T, writePolicy: WritePolicy? = nil, completionHandler: ObjectCompletionHandler?) -> Request { diff --git a/Kinvey/Kinvey/Endpoint.swift b/Kinvey/Kinvey/Endpoint.swift index 42e77d134..8311a3ae9 100644 --- a/Kinvey/Kinvey/Endpoint.swift +++ b/Kinvey/Kinvey/Endpoint.swift @@ -24,6 +24,7 @@ internal enum Endpoint { case appDataById(client: Client, collectionName: String, id: String) case appDataByQuery(client: Client, collectionName: String, query: Query?) case appDataCount(client: Client, collectionName: String, query: Query?) + case appDataGroup(client: Client, collectionName: String) case pushRegisterDevice(client: Client) case pushUnRegisterDevice(client: Client) @@ -94,6 +95,8 @@ internal enum Endpoint { } } return URL(string: url)! + case .appDataGroup(let client, let collectionName): + return client.apiHostName.appendingPathComponent("/appdata/\(client.appKey!)/\(collectionName)/_group") case .pushRegisterDevice(let client): return client.apiHostName.appendingPathComponent("/push/\(client.appKey!)/register-device") case .pushUnRegisterDevice(let client): diff --git a/Kinvey/Kinvey/FindOperation.swift b/Kinvey/Kinvey/FindOperation.swift index 304bbc9eb..393aef173 100644 --- a/Kinvey/Kinvey/FindOperation.swift +++ b/Kinvey/Kinvey/FindOperation.swift @@ -27,7 +27,7 @@ internal class FindOperation: ReadOperation typealias ResultsHandler = ([JsonDictionary]) -> Void let resultsHandler: ResultsHandler? - init(query: Query, deltaSet: Bool, readPolicy: ReadPolicy, cache: Cache?, client: Client, resultsHandler: ResultsHandler? = nil) { + init(query: Query, deltaSet: Bool, readPolicy: ReadPolicy, cache: AnyCache?, client: Client, resultsHandler: ResultsHandler? = nil) { self.query = query self.deltaSet = deltaSet self.resultsHandler = resultsHandler @@ -39,7 +39,7 @@ internal class FindOperation: ReadOperation let request = LocalRequest() request.execute { () -> Void in if let cache = self.cache { - let json = cache.findEntityByQuery(self.query) + let json = cache.find(byQuery: self.query) completionHandler?(json, nil) } else { completionHandler?([], nil) @@ -127,7 +127,7 @@ internal class FindOperation: ReadOperation let deltaSet = self.computeDeltaSet(self.query, refObjs: refObjs) self.removeCachedRecords(cache, keys: refObjs.keys, deleted: deltaSet.deleted) } - cache.saveEntities(entities) + cache.save(entities: entities) } completionHandler?(entities, nil) } else { @@ -141,12 +141,12 @@ internal class FindOperation: ReadOperation return request } - fileprivate func removeCachedRecords(_ cache: Cache, keys: S, deleted: Set) where S.Iterator.Element == String { + fileprivate func removeCachedRecords(_ cache: AnyCache, keys: S, deleted: Set) where S.Iterator.Element == String { let refKeys = Set(keys) let deleted = deleted.subtracting(refKeys) if deleted.count > 0 { let query = Query(format: "\(T.entityIdProperty()) IN %@", deleted as AnyObject) - cache.removeEntitiesByQuery(query) + cache.remove(byQuery: query) } } diff --git a/Kinvey/Kinvey/GetOperation.swift b/Kinvey/Kinvey/GetOperation.swift index b73f33bf9..9ea2afcde 100644 --- a/Kinvey/Kinvey/GetOperation.swift +++ b/Kinvey/Kinvey/GetOperation.swift @@ -12,7 +12,7 @@ internal class GetOperation: ReadOperation, R let id: String - init(id: String, readPolicy: ReadPolicy, cache: Cache?, client: Client) { + init(id: String, readPolicy: ReadPolicy, cache: AnyCache?, client: Client) { self.id = id super.init(readPolicy: readPolicy, cache: cache, client: client) } @@ -20,7 +20,7 @@ internal class GetOperation: ReadOperation, R func executeLocal(_ completionHandler: CompletionHandler?) -> Request { let request = LocalRequest() request.execute { () -> Void in - let persistable = self.cache?.findEntity(self.id) + let persistable = self.cache?.find(byId: self.id) completionHandler?(persistable, nil) } return request @@ -32,7 +32,7 @@ internal class GetOperation: ReadOperation, R if let response = response , response.isOK, let json = self.client.responseParser.parse(data) { let obj = T(JSON: json) if let obj = obj, let cache = self.cache { - cache.saveEntity(obj) + cache.save(entity: obj) } completionHandler?(obj, nil) } else { diff --git a/Kinvey/Kinvey/HttpRequest.swift b/Kinvey/Kinvey/HttpRequest.swift index 82c7fdcf9..1118fd9dd 100644 --- a/Kinvey/Kinvey/HttpRequest.swift +++ b/Kinvey/Kinvey/HttpRequest.swift @@ -422,5 +422,10 @@ internal class HttpRequest: TaskProgressRequest, Request { return "curl -X \(String(describing: request.httpMethod)) \(headers) \(request.url!)" } } + + func setBody(json: [String : Any]) { + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try! JSONSerialization.data(withJSONObject: json) + } } diff --git a/Kinvey/Kinvey/HttpRequestFactory.swift b/Kinvey/Kinvey/HttpRequestFactory.swift index 7daa321e1..6644b084d 100644 --- a/Kinvey/Kinvey/HttpRequestFactory.swift +++ b/Kinvey/Kinvey/HttpRequestFactory.swift @@ -151,6 +151,20 @@ class HttpRequestFactory: RequestFactory { return request } + func buildAppDataGroup(collectionName: String, keys: [String], initialObject: [String : Any], reduceJSFunction: String, condition: NSPredicate?) -> HttpRequest { + let request = HttpRequest(httpMethod: .post, endpoint: Endpoint.appDataGroup(client: client, collectionName: collectionName), credential: client.activeUser, client: client) + var json: [String : Any] = [ + "key" : keys, + "initial" : initialObject, + "reduce" : reduceJSFunction + ] + if let condition = condition { + json["condition"] = condition.mongoDBQuery + } + request.setBody(json: json) + return request + } + func buildAppDataSave(_ persistable: T) -> HttpRequest { let collectionName = T.collectionName() var bodyObject = persistable.toJSON() diff --git a/Kinvey/Kinvey/MemoryCache.swift b/Kinvey/Kinvey/MemoryCache.swift index 4dca7fdf4..ad956d48b 100644 --- a/Kinvey/Kinvey/MemoryCache.swift +++ b/Kinvey/Kinvey/MemoryCache.swift @@ -7,8 +7,11 @@ // import Foundation +@testable import Kinvey -class MemoryCache: Cache where T: NSObject { +class MemoryCache: Cache, CacheType where T: NSObject { + + typealias `Type` = T var memory = [String : T]() @@ -16,22 +19,22 @@ class MemoryCache: Cache where T: NSObject { super.init(persistenceId: "") } - override func saveEntity(_ entity: T) { + func save(entity: T) { let objId = entity.entityId! memory[objId] = entity } - override func saveEntities(_ entities: [T]) { + func save(entities: [T]) { for entity in entities { - saveEntity(entity) + save(entity: entity) } } - override func findEntity(_ objectId: String) -> T? { + func find(byId objectId: String) -> T? { return memory[objectId] } - override func findEntityByQuery(_ query: Query) -> [T] { + func find(byQuery query: Query) -> [T] { guard let predicate = query.predicate else { return memory.values.map({ (json) -> Type in return json @@ -44,9 +47,9 @@ class MemoryCache: Cache where T: NSObject { }) } - override func findIdsLmtsByQuery(_ query: Query) -> [String : String] { + func findIdsLmts(byQuery query: Query) -> [String : String] { var results = [String : String]() - let array = findEntityByQuery(query).map { (entity) -> (String, String) in + let array = find(byQuery: query).map { (entity) -> (String, String) in let kmd = entity.metadata! return (entity.entityId!, kmd.lmt!) } @@ -56,34 +59,56 @@ class MemoryCache: Cache where T: NSObject { return results } - override func findAll() -> [T] { - return findEntityByQuery(Query()) + func findAll() -> [T] { + return find(byQuery: Query()) } - override func count(_ query: Query? = nil) -> Int { + func count(query: Query? = nil) -> Int { if let query = query { - return findEntityByQuery(query).count + return find(byQuery: query).count } return memory.count } @discardableResult - override func removeEntity(_ entity: T) -> Bool { + func remove(entity: T) -> Bool { let objId = entity.entityId! return memory.removeValue(forKey: objId) != nil } @discardableResult - override func removeEntitiesByQuery(_ query: Query) -> Int { - let objs = findEntityByQuery(query) + func remove(entities: [T]) -> Bool { + for entity in entities { + if !remove(entity: entity) { + return false + } + } + return true + } + + @discardableResult + func remove(byQuery query: Query) -> Int { + let objs = find(byQuery: query) for obj in objs { - removeEntity(obj) + remove(entity: obj) } return objs.count } - override func removeAllEntities() { + func removeAll() { memory.removeAll() } + func clear(query: Query?) { + memory.removeAll() + } + + func detach(entities: [T], query: Query?) -> [T] { + return entities + } + + func group(aggregation: Aggregation, predicate: NSPredicate?) -> [JsonDictionary] { + return [] + } + } diff --git a/Kinvey/Kinvey/Operation.swift b/Kinvey/Kinvey/Operation.swift index 33543e38f..ea0740c02 100644 --- a/Kinvey/Kinvey/Operation.swift +++ b/Kinvey/Kinvey/Operation.swift @@ -112,10 +112,10 @@ internal class Operation: NSObject where T: NSObject { typealias UIntCompletionHandler = (UInt?, Swift.Error?) -> Void typealias UIntArrayCompletionHandler = (UInt?, [T]?, Swift.Error?) -> Void - let cache: Cache? + let cache: AnyCache? let client: Client - init(cache: Cache? = nil, client: Client) { + init(cache: AnyCache? = nil, client: Client) { self.cache = cache self.client = client } @@ -138,7 +138,7 @@ internal class Operation: NSObject where T: NSObject { return (created: Set(), updated: Set(), deleted: Set()) } let refKeys = Set(refObjs.keys) - let cachedObjs = cache.findIdsLmtsByQuery(query) + let cachedObjs = cache.findIdsLmts(byQuery: query) let cachedKeys = Set(cachedObjs.keys) let createdKeys = refKeys.subtracting(cachedKeys) let deletedKeys = cachedKeys.subtracting(refKeys) diff --git a/Kinvey/Kinvey/PullOperation.swift b/Kinvey/Kinvey/PullOperation.swift index e9d77d586..d6b84713f 100644 --- a/Kinvey/Kinvey/PullOperation.swift +++ b/Kinvey/Kinvey/PullOperation.swift @@ -10,7 +10,7 @@ import Foundation internal class PullOperation: FindOperation where T: NSObject { - override init(query: Query, deltaSet: Bool, readPolicy: ReadPolicy, cache: Cache?, client: Client, resultsHandler: ResultsHandler? = nil) { + override init(query: Query, deltaSet: Bool, readPolicy: ReadPolicy, cache: AnyCache?, client: Client, resultsHandler: ResultsHandler? = nil) { super.init(query: query, deltaSet: deltaSet, readPolicy: readPolicy, cache: cache, client: client, resultsHandler: resultsHandler) } diff --git a/Kinvey/Kinvey/PurgeOperation.swift b/Kinvey/Kinvey/PurgeOperation.swift index c1e9947d0..d5bb7376f 100644 --- a/Kinvey/Kinvey/PurgeOperation.swift +++ b/Kinvey/Kinvey/PurgeOperation.swift @@ -11,7 +11,7 @@ import PromiseKit internal class PurgeOperation: SyncOperation where T: NSObject { - internal override init(sync: AnySync?, cache: Cache?, client: Client) { + internal override init(sync: AnySync?, cache: AnyCache?, client: Client) { super.init(sync: sync, cache: cache, client: client) } @@ -33,7 +33,7 @@ internal class PurgeOperation: SyncOperation: SyncOperation { fulfill, reject in if let objectId = pendingOperation.objectId { let query = Query(format: "\(T.entityIdProperty()) == %@", objectId) - cache?.removeEntitiesByQuery(query) + cache?.remove(byQuery: query) } sync.removePendingOperation(pendingOperation) fulfill() diff --git a/Kinvey/Kinvey/PushOperation.swift b/Kinvey/Kinvey/PushOperation.swift index cacfb7140..acc4ed868 100644 --- a/Kinvey/Kinvey/PushOperation.swift +++ b/Kinvey/Kinvey/PushOperation.swift @@ -63,7 +63,7 @@ fileprivate class PushRequest: NSObject, Request { internal class PushOperation: SyncOperation where T: NSObject { - internal override init(sync: AnySync?, cache: Cache?, client: Client) { + internal override init(sync: AnySync?, cache: AnyCache?, client: Client) { super.init(sync: sync, cache: cache, client: client) } @@ -90,13 +90,13 @@ internal class PushOperation: SyncOperation: Operation where T: NSObje let readPolicy: ReadPolicy - init(readPolicy: ReadPolicy, cache: Cache?, client: Client) { + init(readPolicy: ReadPolicy, cache: AnyCache?, client: Client) { self.readPolicy = readPolicy super.init(cache: cache, client: client) } diff --git a/Kinvey/Kinvey/RealmCache.swift b/Kinvey/Kinvey/RealmCache.swift index 6dd1ccedc..d8e96766d 100644 --- a/Kinvey/Kinvey/RealmCache.swift +++ b/Kinvey/Kinvey/RealmCache.swift @@ -19,7 +19,9 @@ let typesNeedsPredicateTranslation = [ BoolValue.self.className() ] -internal class RealmCache: Cache where T: NSObject { +internal class RealmCache: Cache, CacheType where T: NSObject { + + typealias `Type` = T let realm: Realm let objectSchema: ObjectSchema @@ -297,18 +299,18 @@ internal class RealmCache: Cache where T: NSObject { } - override func detach(_ results: [T], query: Query?) -> [T] { - log.verbose("Detaching \(results.count) object(s)") + func detach(entities: [T], query: Query?) -> [T] { + log.verbose("Detaching \(entities.count) object(s)") var detachedResults = [T]() let skip = query?.skip ?? 0 - let limit = query?.limit ?? results.count + let limit = query?.limit ?? entities.count var arrayEnumerate: [T] - if skip != 0 || limit != results.count { - let begin = max(min(skip, results.count), 0) - let end = max(min(skip + limit, results.count), 0) - arrayEnumerate = Array(results[begin ..< end]) + if skip != 0 || limit != entities.count { + let begin = max(min(skip, entities.count), 0) + let end = max(min(skip + limit, entities.count), 0) + arrayEnumerate = Array(entities[begin ..< end]) } else { - arrayEnumerate = results + arrayEnumerate = entities } for entity in arrayEnumerate { if let entity = entity as? Object { @@ -323,10 +325,10 @@ internal class RealmCache: Cache where T: NSObject { if let predicate = query?.predicate { results = results.filter(predicate: predicate) } - return detach(results, query: query) + return detach(entities: results, query: query) } - override func saveEntity(_ entity: T) { + func save(entity: T) { log.verbose("Saving object: \(entity)") executor.executeAndWait { try! self.realm.write { @@ -335,7 +337,7 @@ internal class RealmCache: Cache where T: NSObject { } } - override func saveEntities(_ entities: [T]) { + func save(entities: [T]) { log.verbose("Saving \(entities.count) object(s)") executor.executeAndWait { try! self.realm.write { @@ -346,7 +348,7 @@ internal class RealmCache: Cache where T: NSObject { } } - override func findEntity(_ objectId: String) -> T? { + func find(byId objectId: String) -> T? { log.verbose("Finding object by ID: \(objectId)") var result: T? executor.executeAndWait { @@ -360,7 +362,7 @@ internal class RealmCache: Cache where T: NSObject { return result } - override func findEntityByQuery(_ query: Query) -> [T] { + func find(byQuery query: Query) -> [T] { log.verbose("Finding objects by query: \(query)") var results = [T]() executor.executeAndWait { @@ -369,7 +371,7 @@ internal class RealmCache: Cache where T: NSObject { return results } - override func findIdsLmtsByQuery(_ query: Query) -> [String : String] { + func findIdsLmts(byQuery query: Query) -> [String : String] { log.verbose("Finding ids and lmts by query: \(query)") var results = [String : String]() executor.executeAndWait { @@ -382,7 +384,7 @@ internal class RealmCache: Cache where T: NSObject { return results } - override func findAll() -> [T] { + func findAll() -> [T] { log.verbose("Finding All") var results = [T]() executor.executeAndWait { @@ -391,7 +393,7 @@ internal class RealmCache: Cache where T: NSObject { return results } - override func count(_ query: Query? = nil) -> Int { + func count(query: Query? = nil) -> Int { log.verbose("Counting by query: \(String(describing: query))") var result = 0 executor.executeAndWait { @@ -404,7 +406,7 @@ internal class RealmCache: Cache where T: NSObject { return result } - override func removeEntity(_ entity: T) -> Bool { + func remove(entity: T) -> Bool { log.verbose("Removing object: \(entity)") var result = false if let entityId = entity.entityId { @@ -422,7 +424,7 @@ internal class RealmCache: Cache where T: NSObject { return result } - override func removeEntities(_ entities: [T]) -> Bool { + func remove(entities: [T]) -> Bool { log.verbose("Removing objects: \(entities)") var result = false executor.executeAndWait { @@ -439,7 +441,7 @@ internal class RealmCache: Cache where T: NSObject { return result } - override func removeEntitiesByQuery(_ query: Query) -> Int { + func remove(byQuery query: Query) -> Int { log.verbose("Removing objects by query: \(query)") var result = 0 executor.executeAndWait { @@ -452,7 +454,7 @@ internal class RealmCache: Cache where T: NSObject { return result } - override func removeAllEntities() { + func removeAll() { log.verbose("Removing all objects") executor.executeAndWait { try! self.realm.write { @@ -461,7 +463,7 @@ internal class RealmCache: Cache where T: NSObject { } } - override func clear(query: Query? = nil) { + func clear(query: Query? = nil) { log.verbose("Clearing cache") executor.executeAndWait { try! self.realm.write { @@ -484,6 +486,10 @@ internal class RealmCache: Cache where T: NSObject { } } + func group(aggregation: Aggregation, predicate: NSPredicate?) -> [JsonDictionary] { + fatalError("Custom Aggregation not supported against local cache") + } + } extension NSComparisonPredicate { diff --git a/Kinvey/Kinvey/RemoveByIdOperation.swift b/Kinvey/Kinvey/RemoveByIdOperation.swift index b30804c74..d12e3004c 100644 --- a/Kinvey/Kinvey/RemoveByIdOperation.swift +++ b/Kinvey/Kinvey/RemoveByIdOperation.swift @@ -16,7 +16,7 @@ internal class RemoveByIdOperation: RemoveOperation where T: return client.networkRequestFactory.buildAppDataRemoveById(collectionName: T.collectionName(), objectId: objectId) } - internal init(objectId: String, writePolicy: WritePolicy, sync: AnySync? = nil, cache: Cache? = nil, client: Client) { + internal init(objectId: String, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { self.objectId = objectId let query = Query(format: "\(T.entityIdProperty()) == %@", objectId as Any) super.init(query: query, writePolicy: writePolicy, sync: sync, cache: cache, client: client) diff --git a/Kinvey/Kinvey/RemoveByQueryOperation.swift b/Kinvey/Kinvey/RemoveByQueryOperation.swift index b1a7d0790..8045dbabb 100644 --- a/Kinvey/Kinvey/RemoveByQueryOperation.swift +++ b/Kinvey/Kinvey/RemoveByQueryOperation.swift @@ -10,7 +10,7 @@ import Foundation internal class RemoveByQueryOperation: RemoveOperation where T: NSObject { - override init(query: Query, writePolicy: WritePolicy, sync: AnySync? = nil, cache: Cache? = nil, client: Client) { + override init(query: Query, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { super.init(query: query, writePolicy: writePolicy, sync: sync, cache: cache, client: client) } diff --git a/Kinvey/Kinvey/RemoveOperation.swift b/Kinvey/Kinvey/RemoveOperation.swift index d07bc3aaf..c1b2687e6 100644 --- a/Kinvey/Kinvey/RemoveOperation.swift +++ b/Kinvey/Kinvey/RemoveOperation.swift @@ -13,7 +13,7 @@ class RemoveOperation: WriteOperation where T: NSObject let query: Query lazy var request: HttpRequest = self.buildRequest() - init(query: Query, writePolicy: WritePolicy, sync: AnySync? = nil, cache: Cache? = nil, client: Client) { + init(query: Query, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { self.query = query super.init(writePolicy: writePolicy, sync: sync, cache: cache, client: client) } @@ -29,10 +29,10 @@ class RemoveOperation: WriteOperation where T: NSObject request.execute { () -> Void in var count: Int? if let cache = self.cache { - let realmObjects = cache.findEntityByQuery(self.query) + let realmObjects = cache.find(byQuery: self.query) count = realmObjects.count - let detachedObjects = cache.detach(realmObjects, query: self.query) - if cache.removeEntities(realmObjects) { + let detachedObjects = cache.detach(entities: realmObjects, query: self.query) + if cache.remove(entities: realmObjects) { let idKey = T.entityIdProperty() for object in detachedObjects { if let objectId = object[idKey] as? String, let sync = self.sync { @@ -58,7 +58,7 @@ class RemoveOperation: WriteOperation where T: NSObject let results = self.client.responseParser.parse(data), let count = results["count"] as? Int { - self.cache?.removeEntitiesByQuery(self.query) + self.cache?.remove(byQuery: self.query) completionHandler?(count, nil) } else { completionHandler?(nil, buildError(data, response, error, self.client)) diff --git a/Kinvey/Kinvey/RequestFactory.swift b/Kinvey/Kinvey/RequestFactory.swift index 37928bba1..267ce58dc 100644 --- a/Kinvey/Kinvey/RequestFactory.swift +++ b/Kinvey/Kinvey/RequestFactory.swift @@ -29,6 +29,7 @@ protocol RequestFactory { func buildAppDataGetById(collectionName: String, id: String) -> HttpRequest func buildAppDataFindByQuery(collectionName: String, query: Query) -> HttpRequest func buildAppDataCountByQuery(collectionName: String, query: Query?) -> HttpRequest + func buildAppDataGroup(collectionName: String, keys: [String], initialObject: [String : Any], reduceJSFunction: String, condition: NSPredicate?) -> HttpRequest func buildAppDataSave(_ persistable: T) -> HttpRequest func buildAppDataRemoveByQuery(collectionName: String, query: Query) -> HttpRequest func buildAppDataRemoveById(collectionName: String, objectId: String) -> HttpRequest diff --git a/Kinvey/Kinvey/SaveOperation.swift b/Kinvey/Kinvey/SaveOperation.swift index be81e0e04..318fd72c4 100644 --- a/Kinvey/Kinvey/SaveOperation.swift +++ b/Kinvey/Kinvey/SaveOperation.swift @@ -12,12 +12,12 @@ internal class SaveOperation: WriteOperation where T: NSO var persistable: T - init(persistable: inout T, writePolicy: WritePolicy, sync: AnySync? = nil, cache: Cache? = nil, client: Client) { + init(persistable: inout T, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { self.persistable = persistable super.init(writePolicy: writePolicy, sync: sync, cache: cache, client: client) } - init(persistable: T, writePolicy: WritePolicy, sync: AnySync? = nil, cache: Cache? = nil, client: Client) { + init(persistable: T, writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { self.persistable = persistable super.init(writePolicy: writePolicy, sync: sync, cache: cache, client: client) } @@ -29,7 +29,7 @@ internal class SaveOperation: WriteOperation where T: NSO let persistable = self.fillObject(&self.persistable) if let cache = self.cache { - cache.saveEntity(persistable) + cache.save(entity: persistable) } if let sync = self.sync { @@ -52,8 +52,8 @@ internal class SaveOperation: WriteOperation where T: NSO sync.removeAllPendingOperations(objectId, methods: ["POST", "PUT"]) } if let persistable = persistable, let cache = self.cache { - cache.removeEntity(self.persistable) - cache.saveEntity(persistable) + cache.remove(entity: self.persistable) + cache.save(entity: persistable) } self.merge(&self.persistable, json: json) } diff --git a/Kinvey/Kinvey/SyncOperation.swift b/Kinvey/Kinvey/SyncOperation.swift index 536e7f686..132c73dfd 100644 --- a/Kinvey/Kinvey/SyncOperation.swift +++ b/Kinvey/Kinvey/SyncOperation.swift @@ -14,7 +14,7 @@ internal class SyncOperation: Operation where T: NSObje let sync: AnySync? - internal init(sync: AnySync?, cache: Cache?, client: Client) { + internal init(sync: AnySync?, cache: AnyCache?, client: Client) { self.sync = sync super.init(cache: cache, client: client) } diff --git a/Kinvey/Kinvey/WriteOperation.swift b/Kinvey/Kinvey/WriteOperation.swift index 9edb4e447..f527aef63 100644 --- a/Kinvey/Kinvey/WriteOperation.swift +++ b/Kinvey/Kinvey/WriteOperation.swift @@ -15,7 +15,7 @@ internal class WriteOperation: Operation where T: NSObject let writePolicy: WritePolicy let sync: AnySync? - init(writePolicy: WritePolicy, sync: AnySync? = nil, cache: Cache? = nil, client: Client) { + init(writePolicy: WritePolicy, sync: AnySync? = nil, cache: AnyCache? = nil, client: Client) { self.writePolicy = writePolicy self.sync = sync super.init(cache: cache, client: client) diff --git a/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift b/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift index 6de9b8f8d..9cd728f4f 100644 --- a/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift +++ b/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift @@ -52,21 +52,21 @@ class DeltaSetCacheTestCase: KinveyTestCase { let person = Person() person.personId = "update" person.metadata = Metadata(JSON: [Metadata.LmtKey : date.toString()]) - cache.saveEntity(person) + cache.save(entity: person) } do { let person = Person() person.personId = "noChange" person.metadata = Metadata(JSON: [Metadata.LmtKey : date.toString()]) - cache.saveEntity(person) + cache.save(entity: person) } do { let person = Person() person.personId = "delete" person.metadata = Metadata(JSON: [Metadata.LmtKey : date.toString()]) - cache.saveEntity(person) + cache.save(entity: person) } - let operation = Operation(cache: cache, client: client) + let operation = Operation(cache: AnyCache(cache), client: client) let query = Query() let refObjs: [JsonDictionary] = [ [ diff --git a/Kinvey/KinveyTests/KinveyTestCase.swift b/Kinvey/KinveyTests/KinveyTestCase.swift index d9c6c15ed..e6d7ce430 100644 --- a/Kinvey/KinveyTests/KinveyTestCase.swift +++ b/Kinvey/KinveyTests/KinveyTestCase.swift @@ -427,10 +427,8 @@ class KinveyTestCase: XCTestCase { private func removeAll(_ type: T.Type) where T: NSObject { let store = DataStore.collection() - if let cache = store.cache as? RealmCache { - try! cache.realm.write { - cache.realm.deleteAll() - } + if let cache = store.cache { + cache.clear(query: nil) } } diff --git a/Kinvey/KinveyTests/NetworkStoreTests.swift b/Kinvey/KinveyTests/NetworkStoreTests.swift index 13af73950..15c797ce9 100644 --- a/Kinvey/KinveyTests/NetworkStoreTests.swift +++ b/Kinvey/KinveyTests/NetworkStoreTests.swift @@ -1235,4 +1235,322 @@ class NetworkStoreTests: StoreTestCase { } } + func testGroupCustomAggregation() { + signUp() + + let store = DataStore.collection(.network) + + if useMockData { + mockResponse(json: [ + ["sum" : 926] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationGroup = expectation(description: "Group") + + store.group( + initialObject: ["sum" : 0], + reduceJSFunction: "function(doc,out) { out.sum += doc.age; }", + condition: NSPredicate(format: "age > %@", NSNumber(value: 18)) + ) { (results, error) in + XCTAssertNotNil(results) + XCTAssertNil(error) + + if let (person, result) = results?.first { + XCTAssertNil(person.name) + XCTAssertNotNil(result["sum"] as? Int) + } + + expectationGroup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationGroup = nil + } + } + + func testGroupCustomAggregationByName() { + signUp() + + let store = DataStore.collection(.network) + + if useMockData { + mockResponse(json: [ + [ + "name" : "Victor", + "sum" : 926 + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationGroup = expectation(description: "Group") + + store.group( + keys: ["name"], + initialObject: ["sum" : 0], + reduceJSFunction: "function(doc,out) { out.sum += doc.age; }", + condition: NSPredicate(format: "age > %@", NSNumber(value: 18)) + ) { (results, error) in + XCTAssertNotNil(results) + XCTAssertNil(error) + + if let (person, result) = results?.first { + XCTAssertNotNil(person.name) + XCTAssertNotNil(result["sum"] as? Int) + } + + expectationGroup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationGroup = nil + } + } + + func testGroupAggregationCountByName() { + signUp() + + let store = DataStore.collection(.network) + + if useMockData { + mockResponse(json: [ + [ + "name" : "Victor", + "count" : 32 + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationGroup = expectation(description: "Group") + + store.group( + count: ["name"], + countType: Int.self, + condition: NSPredicate(format: "age > %@", NSNumber(value: 18)) + ) { (results, error) in + XCTAssertNotNil(results) + XCTAssertNil(error) + + if let results = results { + XCTAssertGreaterThanOrEqual(results.count, 0) + + if let first = results.first { + XCTAssertNotNil(first.value.name) + XCTAssertEqual(first.count, 32) + } + } + + expectationGroup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationGroup = nil + } + } + + func testGroupAggregationSumByName() { + signUp() + + let store = DataStore.collection(.network) + + if useMockData { + mockResponse(json: [ + [ + "name" : "Victor", + "sum" : 926.2 + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationGroup = expectation(description: "Group") + + store.group( + keys: ["name"], + sum: "age", + sumType: Double.self, + condition: NSPredicate(format: "age > %@", NSNumber(value: 18)) + ) { (results, error) in + XCTAssertNotNil(results) + XCTAssertNil(error) + + if let results = results { + XCTAssertGreaterThanOrEqual(results.count, 0) + + if let first = results.first { + XCTAssertNotNil(first.value.name) + XCTAssertEqual(first.sum, 926.2) + } + } + + expectationGroup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationGroup = nil + } + } + + func testGroupAggregationAvgByName() { + signUp() + + let store = DataStore.collection(.network) + + if useMockData { + mockResponse(json: [ + [ + "name" : "Victor", + "sum" : 926, + "count" : 32, + "avg" : 28.9375 + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationGroup = expectation(description: "Group") + + store.group( + keys: ["name"], + avg: "age", + avgType: Double.self, + condition: NSPredicate(format: "age > %@", NSNumber(value: 18)) + ) { (result, error) in + XCTAssertNotNil(result) + XCTAssertNil(error) + + if let result = result { + XCTAssertGreaterThanOrEqual(result.count, 0) + + if let first = result.first { + XCTAssertNotNil(first.value.name) + XCTAssertEqual(first.avg, 28.9375) + } + } + + expectationGroup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationGroup = nil + } + } + + func testGroupAggregationMinByName() { + signUp() + + let store = DataStore.collection(.network) + + if useMockData { + mockResponse(json: [ + [ + "name" : "Victor", + "min" : 27.6 + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationGroup = expectation(description: "Group") + + store.group( + keys: ["name"], + min: "age", + minType: Float.self, + condition: NSPredicate(format: "age > %@", NSNumber(value: 18)) + ) { (result, error) in + XCTAssertNotNil(result) + XCTAssertNil(error) + + if let result = result { + XCTAssertGreaterThanOrEqual(result.count, 0) + + if let (person, min) = result.first { + XCTAssertNotNil(person.name) + XCTAssertEqual(min, 27.6) + } + } + + expectationGroup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationGroup = nil + } + } + + func testGroupAggregationMaxByName() { + signUp() + + let store = DataStore.collection(.network) + + if useMockData { + mockResponse(json: [ + [ + "name" : "Victor", + "max" : 30.5 + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationGroup = expectation(description: "Group") + + store.group( + keys: ["name"], + max: "age", + maxType: Float.self, + condition: NSPredicate(format: "age > %@", NSNumber(value: 18)) + ) { (result, error) in + XCTAssertNotNil(result) + XCTAssertNil(error) + + if let result = result { + XCTAssertGreaterThanOrEqual(result.count, 0) + + if let (person, max) = result.first { + XCTAssertNotNil(person.name) + XCTAssertEqual(max, 30.5) + } + } + + expectationGroup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationGroup = nil + } + } + } diff --git a/Kinvey/KinveyTests/SyncStoreTests.swift b/Kinvey/KinveyTests/SyncStoreTests.swift index 14d5c7cef..8692564cf 100644 --- a/Kinvey/KinveyTests/SyncStoreTests.swift +++ b/Kinvey/KinveyTests/SyncStoreTests.swift @@ -477,7 +477,7 @@ class SyncStoreTests: StoreTestCase { if let results = results { XCTAssertEqual(results.count, 3) - let cacheCount = Int((self.store.cache?.count())!) + let cacheCount = Int((self.store.cache?.count(query: nil))!) XCTAssertEqual(cacheCount, results.count) } @@ -505,7 +505,7 @@ class SyncStoreTests: StoreTestCase { if let results = results { XCTAssertEqual(results.count, 1) - let cacheCount = Int((self.store.cache?.count())!) + let cacheCount = self.store.cache?.count(query: nil) XCTAssertEqual(cacheCount, results.count) if let person = results.first { @@ -617,7 +617,7 @@ class SyncStoreTests: StoreTestCase { if let person = results.first { XCTAssertEqual(person.personId, "Victor") - let cacheCount = Int((self.store.cache?.count())!) + let cacheCount = self.store.cache?.count(query: nil) XCTAssertEqual(cacheCount, results.count) } From 343b09e4eb69b527185c6405cd099354f93651b0 Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Mon, 10 Apr 2017 14:51:55 -0700 Subject: [PATCH 02/11] MLIBZ-1770: lookup custom user types Former-commit-id: a8faa6de7efdee058dafff77886cc305ff6967f3 --- Kinvey/Kinvey/JsonResponseParser.swift | 14 ++--- Kinvey/Kinvey/ResponseParser.swift | 4 +- Kinvey/Kinvey/User.swift | 2 +- Kinvey/KinveyTests/UserTests.swift | 83 ++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/Kinvey/Kinvey/JsonResponseParser.swift b/Kinvey/Kinvey/JsonResponseParser.swift index ff0023904..a167926de 100644 --- a/Kinvey/Kinvey/JsonResponseParser.swift +++ b/Kinvey/Kinvey/JsonResponseParser.swift @@ -51,32 +51,32 @@ class JsonResponseParser: ResponseParser { return nil } - fileprivate func parse(_ json: JsonDictionary, userType: U.Type) -> U? { + fileprivate func parse(_ json: JsonDictionary, userType: UserType.Type) -> UserType? { let map = Map(mappingType: .fromJSON, JSON: json) - let user = userType.init(map: map) + let user: UserType? = userType.init(map: map) user?.mapping(map: map) return user } - func parseUser(_ data: Data?) -> User? { + func parseUser(_ data: Data?) -> UserType? { if let data = data , data.count > 0, let result = try? JSONSerialization.jsonObject(with: data) as? JsonDictionary, let json = result { let user = parse(json, userType: client.userType) - return user + return user as? UserType } return nil } - func parseUsers(_ data: Data?) -> [User]? { + func parseUsers(_ data: Data?) -> [UserType]? { if let data = data , data.count > 0, let result = try? JSONSerialization.jsonObject(with: data) as? [JsonDictionary], let jsonArray = result { - var users = [User]() + var users = [UserType]() for json in jsonArray { - if let user = parse(json, userType: client.userType) { + if let user = parse(json, userType: client.userType) as? UserType { users.append(user) } } diff --git a/Kinvey/Kinvey/ResponseParser.swift b/Kinvey/Kinvey/ResponseParser.swift index d1df48112..964a434ca 100644 --- a/Kinvey/Kinvey/ResponseParser.swift +++ b/Kinvey/Kinvey/ResponseParser.swift @@ -19,8 +19,8 @@ internal protocol ResponseParser { func parse(_ data: Data?) -> T? func parse(_ data: Data?) -> [T]? - func parseUser(_ data: Data?) -> User? - func parseUsers(_ data: Data?) -> [User]? + func parseUser(_ data: Data?) -> UserType? + func parseUsers(_ data: Data?) -> [UserType]? } diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index ab0ee8ebc..e529fe12b 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -431,7 +431,7 @@ open class User: NSObject, Credential, Mappable { let request = client.networkRequestFactory.buildUserLookup(user: self, userQuery: userQuery) Promise<[U]> { fulfill, reject in request.execute() { (data, response, error) in - if let response = response , response.isOK, let users: [U] = client.responseParser.parse(data) { + if let response = response, response.isOK, let users: [U] = client.responseParser.parseUsers(data) { fulfill(users) } else { reject(buildError(data, response, error, client)) diff --git a/Kinvey/KinveyTests/UserTests.swift b/Kinvey/KinveyTests/UserTests.swift index ce45e68cb..6ad251cd8 100644 --- a/Kinvey/KinveyTests/UserTests.swift +++ b/Kinvey/KinveyTests/UserTests.swift @@ -546,6 +546,89 @@ class UserTests: KinveyTestCase { } } + func testCustomUserLookup() { + client.userType = MyUser.self + + signUp() + + XCTAssertNotNil(client.activeUser) + XCTAssertTrue(client.activeUser is MyUser) + + if let user = client.activeUser as? MyUser { + let email = "victor@kinvey.com" + + if useMockData { + mockResponse(json: [ + [ + "_id" : UUID().uuidString, + "username" : "victor", + "email" : email + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + do { + weak var expectationUserLookup = expectation(description: "User Lookup") + + let userQuery = UserQuery { + $0.email = email + } + user.lookup(userQuery) { users, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(users) + XCTAssertNil(error) + + if let users = users { + XCTAssertEqual(users.count, 1) + + if let user = users.first { + XCTAssertTrue(user is MyUser) + XCTAssertEqual(user.email, email) + } + } + + expectationUserLookup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationUserLookup = nil + } + } + + do { + weak var expectationUserLookup = expectation(description: "User Lookup") + + let userQuery = UserQuery { + $0.email = email + } + user.lookup(userQuery) { (users: [MyUser]?, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(users) + XCTAssertNil(error) + + if let users = users { + XCTAssertEqual(users.count, 1) + + if let user = users.first { + XCTAssertEqual(user.email, email) + } + } + + expectationUserLookup?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationUserLookup = nil + } + } + } + } + func testLogoutLogin() { guard !useMockData else { return From e7957a10613ef6812ff17da5e62c86b58f9ca952 Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Mon, 10 Apr 2017 16:41:17 -0700 Subject: [PATCH 03/11] MLIBZ-1735: ping call Former-commit-id: 42160e67af0f9b753f878a2969ff8893740e16cc --- Kinvey/Kinvey.xcodeproj/project.pbxproj | 4 ++ Kinvey/Kinvey/Client.swift | 63 +++++++++++++++-- Kinvey/Kinvey/Endpoint.swift | 4 ++ Kinvey/Kinvey/Error.swift | 5 +- Kinvey/Kinvey/HttpRequestFactory.swift | 5 ++ Kinvey/Kinvey/Kinvey.swift | 7 ++ Kinvey/Kinvey/RequestFactory.swift | 1 + Kinvey/KinveyTests/ClientTestCase.swift | 90 +++++++++++++++++++++++++ 8 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 Kinvey/KinveyTests/ClientTestCase.swift diff --git a/Kinvey/Kinvey.xcodeproj/project.pbxproj b/Kinvey/Kinvey.xcodeproj/project.pbxproj index b2dedc3c4..d1449bd49 100644 --- a/Kinvey/Kinvey.xcodeproj/project.pbxproj +++ b/Kinvey/Kinvey.xcodeproj/project.pbxproj @@ -247,6 +247,7 @@ 57E448061DF62E05003D1AFA /* NoCacheTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795AB011DD136B8001FC808 /* NoCacheTestCase.swift */; }; 57E448071DF62E35003D1AFA /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571991081CB45EEE00070CDA /* Person.swift */; }; 57E4DB191C8A26CA0017B406 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E4DB181C8A26CA0017B406 /* Keychain.swift */; }; + 57E516391E9C3BE600A2AAD3 /* ClientTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E516381E9C3BE600A2AAD3 /* ClientTestCase.swift */; }; 57E7C7A51C504AC500848748 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E7C7A41C504AC500848748 /* Cache.swift */; }; 57E7C7A71C504B0900848748 /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E7C7A61C504B0900848748 /* CacheManager.swift */; }; 57E7C7A91C504E7B00848748 /* PendingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E7C7A81C504E7B00848748 /* PendingOperation.swift */; }; @@ -739,6 +740,7 @@ 57E1C3B41C18253B00578974 /* JsonResponseParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JsonResponseParser.swift; sourceTree = ""; }; 57E448041DF62DC7003D1AFA /* KinveyTests No Cache.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "KinveyTests No Cache.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 57E4DB181C8A26CA0017B406 /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; + 57E516381E9C3BE600A2AAD3 /* ClientTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientTestCase.swift; sourceTree = ""; }; 57E7C7A41C504AC500848748 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; 57E7C7A61C504B0900848748 /* CacheManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; 57E7C7A81C504E7B00848748 /* PendingOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingOperation.swift; sourceTree = ""; }; @@ -1160,6 +1162,7 @@ 57873DEB1DFF3FDC002C87BF /* PushTestCase.swift */, 578D9FC01E8DE4D100C2B280 /* PushMissingConfiguration.swift */, 575465A31E66405D0063B4B6 /* PerformanceProductTestCase.swift */, + 57E516381E9C3BE600A2AAD3 /* ClientTestCase.swift */, ); path = KinveyTests; sourceTree = ""; @@ -2153,6 +2156,7 @@ 5765B83D1C972D7000080FFA /* URLProtocols.swift in Sources */, 571991071CB45EC400070CDA /* SyncStoreTests.swift in Sources */, 578F5C931C99EED100B20F17 /* KIF.swift in Sources */, + 57E516391E9C3BE600A2AAD3 /* ClientTestCase.swift in Sources */, 577155521CA0F1D400C91B4B /* FindOperationTest.swift in Sources */, 575465A41E66405D0063B4B6 /* PerformanceProductTestCase.swift in Sources */, 5796B3E71DEE8EC900209C9F /* CacheStoreTests.swift in Sources */, diff --git a/Kinvey/Kinvey/Client.swift b/Kinvey/Kinvey/Client.swift index a437f8911..f4d69aa55 100644 --- a/Kinvey/Kinvey/Client.swift +++ b/Kinvey/Kinvey/Client.swift @@ -8,6 +8,7 @@ import Foundation import ObjectMapper +import PromiseKit private let lockEncryptionKey = NSLock() @@ -266,10 +267,62 @@ open class Client: Credential { return Client.fileURL(appKey: self.appKey!, tag: tag) } - public func encode(with aCoder: NSCoder) { - aCoder.encode(appKey, forKey: "appKey") - aCoder.encode(appSecret, forKey: "appSecret") - aCoder.encode(apiHostName, forKey: "apiHostName") - aCoder.encode(authHostName, forKey: "authHostName") + @discardableResult + public func ping(completionHandler: @escaping (EnvironmentInfo?, Swift.Error?) -> Void) -> Request { + guard let _ = appKey, let _ = appSecret else { + let message = "Please initialize your client calling the initialize() method before call ping()" + log.error(message) + fatalError(message) + } + + let request = networkRequestFactory.buildAppDataPing() + Promise { fulfill, reject in + request.execute() { data, response, error in + if let response = response, + response.isOK, + let data = data, + let json = try? JSONSerialization.jsonObject(with: data), + let result = json as? [String : String], + let environmentInfo = EnvironmentInfo(JSON: result) + { + fulfill(environmentInfo) + } else { + reject(buildError(data, response, error, self)) + } + } + }.then { + completionHandler($0, nil) + }.catch { + completionHandler(nil, $0) + } + return request } } + +public struct EnvironmentInfo: StaticMappable { + + public let version: String + public let kinvey: String + public let appName: String + public let environmentName: String + + public static func objectForMapping(map: Map) -> BaseMappable? { + guard let version: String = map["version"].value(), + let kinvey: String = map["kinvey"].value(), + let appName: String = map["appName"].value(), + let environmentName: String = map["environmentName"].value() + else { + return nil + } + return EnvironmentInfo( + version: version, + kinvey: kinvey, + appName: appName, + environmentName: environmentName + ) + } + + public mutating func mapping(map: Map) { + } + +} diff --git a/Kinvey/Kinvey/Endpoint.swift b/Kinvey/Kinvey/Endpoint.swift index 42e77d134..fb3266263 100644 --- a/Kinvey/Kinvey/Endpoint.swift +++ b/Kinvey/Kinvey/Endpoint.swift @@ -20,6 +20,8 @@ internal enum Endpoint { case userResetPassword(usernameOrEmail: String, client: Client) case userForgotUsername(client: Client) + case appDataPing(client: Client) + case appData(client: Client, collectionName: String) case appDataById(client: Client, collectionName: String, id: String) case appDataByQuery(client: Client, collectionName: String, query: Query?) @@ -63,6 +65,8 @@ internal enum Endpoint { return client.apiHostName.appendingPathComponent("/rpc/\(client.appKey!)/\(usernameOrEmail)/user-password-reset-initiate") case .userForgotUsername(let client): return client.apiHostName.appendingPathComponent("/rpc/\(client.appKey!)/user-forgot-username") + case .appDataPing(let client): + return client.apiHostName.appendingPathComponent("/appdata/\(client.appKey!)") case .appData(let client, let collectionName): return client.apiHostName.appendingPathComponent("/appdata/\(client.appKey!)/\(collectionName)") case .appDataById(let client, let collectionName, let id): diff --git a/Kinvey/Kinvey/Error.swift b/Kinvey/Kinvey/Error.swift index 814903e99..14d727695 100644 --- a/Kinvey/Kinvey/Error.swift +++ b/Kinvey/Kinvey/Error.swift @@ -59,6 +59,8 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD /// Error when a `User` doen't have an email or username. case userWithoutEmailOrUsername + /// Error when the `appKey` and `appSecret` does not match with any Kinvey environment. + case appNotFound(description: String) /// Error localized description. public var description: String { @@ -69,7 +71,8 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD .unknownError(_, _, let description), .unauthorized(_, _, _, let description), .invalidOperation(let description), - .missingConfiguration(_, _, _, let description): + .missingConfiguration(_, _, _, let description), + .appNotFound(let description): return description case .objectIdMissing: return NSLocalizedString("Error.objectIdMissing", bundle: bundle, comment: "") diff --git a/Kinvey/Kinvey/HttpRequestFactory.swift b/Kinvey/Kinvey/HttpRequestFactory.swift index 7daa321e1..bddcb38da 100644 --- a/Kinvey/Kinvey/HttpRequestFactory.swift +++ b/Kinvey/Kinvey/HttpRequestFactory.swift @@ -136,6 +136,11 @@ class HttpRequestFactory: RequestFactory { return request } + func buildAppDataPing() -> HttpRequest { + let request = HttpRequest(httpMethod: .get, endpoint: Endpoint.appDataPing(client: client), client: client) + return request + } + func buildAppDataGetById(collectionName: String, id: String) -> HttpRequest { let request = HttpRequest(endpoint: Endpoint.appDataById(client: client, collectionName: collectionName, id: id), credential: client.activeUser, client: client) return request diff --git a/Kinvey/Kinvey/Kinvey.swift b/Kinvey/Kinvey/Kinvey.swift index 0f8390e4f..5d596cc85 100644 --- a/Kinvey/Kinvey/Kinvey.swift +++ b/Kinvey/Kinvey/Kinvey.swift @@ -93,6 +93,13 @@ func buildError(_ data: Data?, _ response: Response?, _ error: Swift.Error?, _ c let description = json["description"] { return Error.missingConfiguration(httpResponse: response.httpResponse, data: data, debug: debug, description: description) + } else if let response = response, + response.isNotFound, + let json = client.responseParser.parse(data) as? [String : String], + json["error"] == "AppNotFound", + let description = json["description"] + { + return Error.appNotFound(description: description) } else if let response = response, let json = client.responseParser.parse(data) { return Error.buildUnknownJsonError(httpResponse: response.httpResponse, data: data, json: json) } else { diff --git a/Kinvey/Kinvey/RequestFactory.swift b/Kinvey/Kinvey/RequestFactory.swift index 37928bba1..5258ca3d5 100644 --- a/Kinvey/Kinvey/RequestFactory.swift +++ b/Kinvey/Kinvey/RequestFactory.swift @@ -26,6 +26,7 @@ protocol RequestFactory { func buildUserResetPassword(usernameOrEmail: String) -> HttpRequest func buildUserForgotUsername(email: String) -> HttpRequest + func buildAppDataPing() -> HttpRequest func buildAppDataGetById(collectionName: String, id: String) -> HttpRequest func buildAppDataFindByQuery(collectionName: String, query: Query) -> HttpRequest func buildAppDataCountByQuery(collectionName: String, query: Query?) -> HttpRequest diff --git a/Kinvey/KinveyTests/ClientTestCase.swift b/Kinvey/KinveyTests/ClientTestCase.swift new file mode 100644 index 000000000..2c6838ab5 --- /dev/null +++ b/Kinvey/KinveyTests/ClientTestCase.swift @@ -0,0 +1,90 @@ +// +// ClientTestCase.swift +// Kinvey +// +// Created by Victor Hugo on 2017-04-10. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import XCTest +import Kinvey + +class ClientTestCase: KinveyTestCase { + + func testPing() { + if useMockData { + mockResponse(json: [ + "version" : "3.9.28", + "kinvey" : "hello My App", + "appName" : "My App", + "environmentName" : "My Environment" + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationPing = self.expectation(description: "Ping") + + Kinvey.sharedClient.ping { (envInfo, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(envInfo) + XCTAssertNil(error) + + if let envInfo = envInfo { + XCTAssertEqual(envInfo.version, "3.9.28") + XCTAssertEqual(envInfo.kinvey, "hello My App") + XCTAssertEqual(envInfo.appName, "My App") + XCTAssertEqual(envInfo.environmentName, "My Environment") + } + + expectationPing?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationPing = nil + } + } + + func testPingAppNotFound() { + if useMockData { + mockResponse(statusCode: 404, json: [ + "error" : "AppNotFound", + "description" : "This app backend not found", + "debug" : "" + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationPing = self.expectation(description: "Ping") + + Kinvey.sharedClient.ping { (envInfo, error) in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNil(envInfo) + XCTAssertNotNil(error) + + if let error = error as? Kinvey.Error { + XCTAssertEqual(error.description, "This app backend not found") + switch error { + case .appNotFound(let description): + XCTAssertEqual(description, "This app backend not found") + default: + XCTFail() + } + } + + expectationPing?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationPing = nil + } + } + +} From 6e71dc09aeb9de9102cf56589490592c31314a6e Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Wed, 12 Apr 2017 17:04:42 -0700 Subject: [PATCH 04/11] MLIBZ-1671: result enum Former-commit-id: 2223cba8e2b56cc57cc44fbafc7a64a0f90dcafb --- Kinvey/Kinvey.xcodeproj/project.pbxproj | 4 + Kinvey/Kinvey/AggregateOperation.swift | 12 +- Kinvey/Kinvey/Client.swift | 79 ++- Kinvey/Kinvey/CountOperation.swift | 12 +- Kinvey/Kinvey/CustomEndpoint.swift | 184 +++-- Kinvey/Kinvey/DataStore.swift | 607 +++++++++++++--- Kinvey/Kinvey/Error.swift | 12 + Kinvey/Kinvey/FileStore.swift | 204 +++++- Kinvey/Kinvey/FindOperation.swift | 34 +- Kinvey/Kinvey/Geolocation.swift | 15 +- Kinvey/Kinvey/GetOperation.swift | 20 +- Kinvey/Kinvey/HttpRequest.swift | 12 +- Kinvey/Kinvey/HttpRequestFactory.swift | 4 +- Kinvey/Kinvey/Kinvey.swift | 14 +- Kinvey/Kinvey/Localizable.strings | 2 + Kinvey/Kinvey/MIC.swift | 77 +- Kinvey/Kinvey/MemoryCache.swift | 4 +- Kinvey/Kinvey/Persistable.swift | 105 +-- Kinvey/Kinvey/PurgeOperation.swift | 6 +- Kinvey/Kinvey/Push.swift | 56 +- Kinvey/Kinvey/PushOperation.swift | 8 +- Kinvey/Kinvey/ReadOperation.swift | 8 +- Kinvey/Kinvey/RemoveOperation.swift | 16 +- Kinvey/Kinvey/Result.swift | 16 + Kinvey/Kinvey/SaveOperation.swift | 16 +- Kinvey/Kinvey/SyncOperation.swift | 2 +- Kinvey/Kinvey/User.swift | 396 ++++++++--- Kinvey/Kinvey/WriteOperation.swift | 36 +- Kinvey/KinveyTests/AclTestCase.swift | 2 +- .../CacheMigrationTestCaseStep1.swift | 4 +- Kinvey/KinveyTests/CacheStoreTests.swift | 2 +- .../KinveyTests/DeltaSetCacheTestCase.swift | 52 +- Kinvey/KinveyTests/FileTestCase.swift | 203 +++++- Kinvey/KinveyTests/KinveyTestCase.swift | 39 +- Kinvey/KinveyTests/SyncStoreTests.swift | 19 +- Kinvey/KinveyTests/URLProtocols.swift | 2 + Kinvey/KinveyTests/UserTests.swift | 662 +++++++++++++++--- 37 files changed, 2339 insertions(+), 607 deletions(-) create mode 100644 Kinvey/Kinvey/Result.swift diff --git a/Kinvey/Kinvey.xcodeproj/project.pbxproj b/Kinvey/Kinvey.xcodeproj/project.pbxproj index 3b3ce4157..f5768f5cb 100644 --- a/Kinvey/Kinvey.xcodeproj/project.pbxproj +++ b/Kinvey/Kinvey.xcodeproj/project.pbxproj @@ -257,6 +257,7 @@ 57E7C7B31C51545900848748 /* ReadPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E7C7B21C51545900848748 /* ReadPolicy.swift */; }; 57E7C7B51C51981400848748 /* RequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E7C7B41C51981400848748 /* RequestType.swift */; }; 57E7C7B71C519FD000848748 /* HttpHeaderCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E7C7B61C519FD000848748 /* HttpHeaderCredential.swift */; }; + 57F216AC1E9DA1640084AAF9 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F216AB1E9DA1640084AAF9 /* Result.swift */; }; 57F91C261D6D15590012850A /* TaskProgressRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F91C251D6D15590012850A /* TaskProgressRequest.swift */; }; 57F91C291D6E2C020012850A /* CountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F91C281D6E2C020012850A /* CountOperation.swift */; }; 57FB57D91C86581300AA590F /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FB57D81C86581300AA590F /* String.swift */; }; @@ -751,6 +752,7 @@ 57E7C7B21C51545900848748 /* ReadPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReadPolicy.swift; path = Kinvey/ReadPolicy.swift; sourceTree = SOURCE_ROOT; }; 57E7C7B41C51981400848748 /* RequestType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestType.swift; sourceTree = ""; }; 57E7C7B61C519FD000848748 /* HttpHeaderCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpHeaderCredential.swift; sourceTree = ""; }; + 57F216AB1E9DA1640084AAF9 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 57F91C251D6D15590012850A /* TaskProgressRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskProgressRequest.swift; sourceTree = ""; }; 57F91C281D6E2C020012850A /* CountOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountOperation.swift; sourceTree = ""; }; 57FB57D81C86581300AA590F /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; @@ -1122,6 +1124,7 @@ 577E6FA71D18E45F00B5DA36 /* Executor.swift */, 57C2F1721D5E5A68005A214B /* BuilderType.swift */, 57A526521E42A07900B33A51 /* Geolocation.swift */, + 57F216AB1E9DA1640084AAF9 /* Result.swift */, ); path = Kinvey; sourceTree = ""; @@ -2070,6 +2073,7 @@ 573851AC1D47C7EB00E4712A /* FileCache.swift in Sources */, 574912731C5932A700EA4F26 /* WritePolicy.swift in Sources */, 57BB56B41C4D8D2B00F6B548 /* LocalRequest.swift in Sources */, + 57F216AC1E9DA1640084AAF9 /* Result.swift in Sources */, 57E1C3A71C17B4F300578974 /* Persistable.swift in Sources */, 57A526531E42A07900B33A51 /* Geolocation.swift in Sources */, 57FB57D91C86581300AA590F /* String.swift in Sources */, diff --git a/Kinvey/Kinvey/AggregateOperation.swift b/Kinvey/Kinvey/AggregateOperation.swift index 33a9dfd9c..d6d5ea0fd 100644 --- a/Kinvey/Kinvey/AggregateOperation.swift +++ b/Kinvey/Kinvey/AggregateOperation.swift @@ -19,20 +19,20 @@ class AggregateOperation: ReadOperation Void)? = nil) -> Request { + func executeLocal(_ completionHandler: CompletionHandler? = nil) -> Request { let request = LocalRequest() request.execute { () -> Void in if let cache = self.cache { let result = cache.group(aggregation: aggregation, predicate: predicate) - completionHandler?(result, nil) + completionHandler?(.success(result)) } else { - completionHandler?([], nil) + completionHandler?(.success([])) } } return request } - func executeNetwork(_ completionHandler: (([JsonDictionary]?, Swift.Error?) -> Void)? = nil) -> Request { + func executeNetwork(_ completionHandler: CompletionHandler? = nil) -> Request { let request = client.networkRequestFactory.buildAppDataGroup(collectionName: T.collectionName(), keys: aggregation.keys, initialObject: aggregation.initialObject, reduceJSFunction: aggregation.reduceJSFunction, condition: predicate) request.execute() { data, response, error in if let response = response, response.isOK, @@ -40,9 +40,9 @@ class AggregateOperation: ReadOperation(appKey: String, appSecret: String, accessGroup: String? = nil, apiHostName: URL = Client.defaultApiHostName, authHostName: URL = Client.defaultAuthHostName, encrypted: Bool, schema: Schema? = nil, completionHandler: User.UserHandler) { + initialize( + appKey: appKey, + appSecret: appSecret, + accessGroup: accessGroup, + apiHostName: apiHostName, + authHostName: authHostName, + encrypted: encrypted, + schema: schema + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler(user, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + /// Initialize a `Client` instance with all the needed parameters and requires a boolean to encrypt or not any store created using this client instance. + open func initialize(appKey: String, appSecret: String, accessGroup: String? = nil, apiHostName: URL = Client.defaultApiHostName, authHostName: URL = Client.defaultAuthHostName, encrypted: Bool, schema: Schema? = nil, completionHandler: (Result) -> Void) { validateInitialize(appKey: appKey, appSecret: appSecret) var encryptionKey: Data? = nil @@ -196,6 +216,26 @@ open class Client: Credential { /// Initialize a `Client` instance with all the needed parameters. open func initialize(appKey: String, appSecret: String, accessGroup: String? = nil, apiHostName: URL = Client.defaultApiHostName, authHostName: URL = Client.defaultAuthHostName, encryptionKey: Data? = nil, schema: Schema? = nil, completionHandler: @escaping User.UserHandler) { + initialize( + appKey: appKey, + appSecret: appSecret, + accessGroup: accessGroup, + apiHostName: apiHostName, + authHostName: authHostName, + encryptionKey: encryptionKey, + schema: schema + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler(user, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + /// Initialize a `Client` instance with all the needed parameters. + open func initialize(appKey: String, appSecret: String, accessGroup: String? = nil, apiHostName: URL = Client.defaultApiHostName, authHostName: URL = Client.defaultAuthHostName, encryptionKey: Data? = nil, schema: Schema? = nil, completionHandler: @escaping (Result) -> Void) { validateInitialize(appKey: appKey, appSecret: appSecret) self.encryptionKey = encryptionKey self.schemaVersion = schema?.version ?? 0 @@ -229,11 +269,19 @@ open class Client: Credential { if let user = keychain.user { user.client = self activeUser = user - completionHandler((user as! U), nil) + let customUser = user as! U + completionHandler(.success(customUser)) } else if let kinveyAuth = sharedKeychain?.kinveyAuth { - User.login(authSource: .kinvey, kinveyAuth.toJSON(), client: self, completionHandler: completionHandler) + User.login(authSource: .kinvey, kinveyAuth.toJSON(), client: self) { (result: Result) in + switch result { + case .success(let user): + completionHandler(.success(user)) + case .failure(let error): + completionHandler(.failure(error)) + } + } } else { - completionHandler(nil, nil) + completionHandler(.success(nil)) } } @@ -251,10 +299,17 @@ open class Client: Credential { } } - internal func isInitialized () -> Bool { + internal func isInitialized() -> Bool { return self.appKey != nil && self.appSecret != nil } + internal func validate() -> Swift.Error? { + guard isInitialized() else { + return Error.clientNotInitialized + } + return nil + } + internal class func fileURL(appKey: String, tag: String = defaultTag) -> URL { let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) let path = paths.first! as NSString @@ -269,6 +324,18 @@ open class Client: Credential { @discardableResult public func ping(completionHandler: @escaping (EnvironmentInfo?, Swift.Error?) -> Void) -> Request { + return ping() { (result: Result) in + switch result { + case .success(let envInfo): + completionHandler(envInfo, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + @discardableResult + public func ping(completionHandler: @escaping (Result) -> Void) -> Request { guard let _ = appKey, let _ = appSecret else { let message = "Please initialize your client calling the initialize() method before call ping()" log.error(message) @@ -291,9 +358,9 @@ open class Client: Credential { } } }.then { - completionHandler($0, nil) + completionHandler(.success($0)) }.catch { - completionHandler(nil, $0) + completionHandler(.failure($0)) } return request } diff --git a/Kinvey/Kinvey/CountOperation.swift b/Kinvey/Kinvey/CountOperation.swift index 0dbf056a6..4efd81e50 100644 --- a/Kinvey/Kinvey/CountOperation.swift +++ b/Kinvey/Kinvey/CountOperation.swift @@ -17,20 +17,20 @@ class CountOperation: ReadOperation, ReadOp super.init(readPolicy: readPolicy, cache: cache, client: client) } - func executeLocal(_ completionHandler: ((Int?, Swift.Error?) -> Void)? = nil) -> Request { + func executeLocal(_ completionHandler: CompletionHandler? = nil) -> Request { let request = LocalRequest() request.execute { () -> Void in if let cache = self.cache { let count = cache.count(query: self.query) - completionHandler?(count, nil) + completionHandler?(.success(count)) } else { - completionHandler?(0, nil) + completionHandler?(.success(0)) } } return request } - func executeNetwork(_ completionHandler: ((Int?, Swift.Error?) -> Void)? = nil) -> Request { + func executeNetwork(_ completionHandler: CompletionHandler? = nil) -> Request { let request = client.networkRequestFactory.buildAppDataCountByQuery(collectionName: T.collectionName(), query: query) request.execute() { data, response, error in if let response = response , response.isOK, @@ -39,9 +39,9 @@ class CountOperation: ReadOperation, ReadOp let result = json as? [String : Int], let count = result["count"] { - completionHandler?(count, nil) + completionHandler?(.success(count)) } else { - completionHandler?(nil, buildError(data, response, error, self.client)) + completionHandler?(.failure(buildError(data, response, error, self.client))) } } return request diff --git a/Kinvey/Kinvey/CustomEndpoint.swift b/Kinvey/Kinvey/CustomEndpoint.swift index c89a5a33f..bb7b6cd5b 100644 --- a/Kinvey/Kinvey/CustomEndpoint.swift +++ b/Kinvey/Kinvey/CustomEndpoint.swift @@ -8,6 +8,7 @@ import Foundation import ObjectMapper +import PromiseKit /// Class to interact with a custom endpoint in the backend. open class CustomEndpoint { @@ -61,14 +62,19 @@ open class CustomEndpoint { @available(*, deprecated, message: "Please use the generic version of execute(params: CustomEndpoint.Params?) method") open static func execute(_ name: String, params: JsonDictionary? = nil, client: Client = sharedClient, completionHandler: CompletionHandler? = nil) -> Request { let params = params != nil ? Params(params!) : nil - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let json: JsonDictionary = client.responseParser.parse(data) { - completionHandler(json, nil) + var request: Request! + Promise { fulfill, reject in + request = callEndpoint(name, params: params, client: client) { data, response, error in + if let response = response, response.isOK, let json: JsonDictionary = client.responseParser.parse(data) { + fulfill(json) } else { - completionHandler(nil, buildError(data, response, error, client)) + reject(buildError(data, response, error, client)) } } + }.then { json in + completionHandler?(json, nil) + }.catch { error in + completionHandler?(nil, error) } return request } @@ -78,14 +84,19 @@ open class CustomEndpoint { @available(*, deprecated, message: "Please use the generic version of execute(params: CustomEndpoint.Params?) method") open static func execute(_ name: String, params: JsonDictionary? = nil, client: Client = sharedClient, completionHandler: CompletionHandler<[JsonDictionary]>? = nil) -> Request { let params = params != nil ? Params(params!) : nil - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let json = client.responseParser.parseArray(data) { - completionHandler(json, nil) + var request: Request! + Promise<[JsonDictionary]> { fulfill, reject in + request = callEndpoint(name, params: params, client: client) { data, response, error in + if let response = response, response.isOK, let json = client.responseParser.parseArray(data) { + fulfill(json) } else { - completionHandler(nil, buildError(data, response, error, client)) + reject(buildError(data, response, error, client)) } } + }.then { jsonArray in + completionHandler?(jsonArray, nil) + }.catch { error in + completionHandler?(nil, error) } return request } @@ -93,108 +104,151 @@ open class CustomEndpoint { /// Executes a custom endpoint by name and passing the expected parameters. @discardableResult open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler? = nil) -> Request { - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let json = client.responseParser.parse(data) { - completionHandler(json, nil) - } else { - completionHandler(nil, buildError(data, response, error, client)) - } + return execute( + name, + params: params, + client: client + ) { (result: Result) in + switch result { + case .success(let json): + completionHandler?(json, nil) + case .failure(let error): + completionHandler?(nil, error) } } - return request } /// Executes a custom endpoint by name and passing the expected parameters. @discardableResult - open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler<[JsonDictionary]>? = nil) -> Request { - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let json = client.responseParser.parseArray(data) { - completionHandler(json, nil) + open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + var request: Request! + Promise { fulfill, reject in + request = callEndpoint(name, params: params, client: client) { data, response, error in + if let response = response, response.isOK, let json = client.responseParser.parse(data) { + fulfill(json) } else { - completionHandler(nil, buildError(data, response, error, client)) + reject(buildError(data, response, error, client)) } } + }.then { json in + completionHandler?(.success(json)) + }.catch { error in + completionHandler?(.failure(error)) } return request } - //MARK: Mappable - /// Executes a custom endpoint by name and passing the expected parameters. @discardableResult - open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler? = nil) -> Request { - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let obj: T = client.responseParser.parse(data) { - completionHandler(obj, nil) - } else { - completionHandler(nil, buildError(data, response, error, client)) - } + open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler<[JsonDictionary]>? = nil) -> Request { + return execute( + name, + params: params, + client: client + ) { (result: Result<[JsonDictionary], Swift.Error>) in + switch result { + case .success(let json): + completionHandler?(json, nil) + case .failure(let error): + completionHandler?(nil, error) } } - return request } /// Executes a custom endpoint by name and passing the expected parameters. @discardableResult - open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler<[T]>? = nil) -> Request { - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let objArray: [T] = client.responseParser.parse(data) { - completionHandler(objArray, nil) + open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: ((Result<[JsonDictionary], Swift.Error>) -> Void)? = nil) -> Request { + var request: Request! + Promise<[JsonDictionary]> { fulfill, reject in + request = callEndpoint(name, params: params, client: client) { data, response, error in + if let response = response , response.isOK, let json = client.responseParser.parseArray(data) { + fulfill(json) } else { - completionHandler(nil, buildError(data, response, error, client)) + reject(buildError(data, response, error, client)) } } + }.then { json in + completionHandler?(.success(json)) + }.catch { error in + completionHandler?(.failure(error)) } return request } - //MARK: StaticMappable + //MARK: BaseMappable: Mappable or StaticMappable /// Executes a custom endpoint by name and passing the expected parameters. @discardableResult - open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler? = nil) -> Request { - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let obj: T = client.responseParser.parse(data) { - completionHandler(obj, nil) - } else { - completionHandler(nil, buildError(data, response, error, client)) - } + open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler? = nil) -> Request { + return execute( + name, + params: params, + client: client + ) { (result: Result) in + switch result { + case .success(let obj): + completionHandler?(obj, nil) + case .failure(let error): + completionHandler?(nil, error) } } - return request } /// Executes a custom endpoint by name and passing the expected parameters. @discardableResult - open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler<[T]>? = nil) -> Request { - let request = callEndpoint(name, params: params, client: client) { data, response, error in - if let completionHandler = dispatchAsyncMainQueue(completionHandler) { - if let response = response , response.isOK, let objArray: [T] = client.responseParser.parse(data) { - completionHandler(objArray, nil) + open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + var request: Request! + Promise { fulfill, reject in + request = callEndpoint(name, params: params, client: client) { data, response, error in + if let response = response , response.isOK, let obj: T = client.responseParser.parse(data) { + fulfill(obj) } else { - completionHandler(nil, buildError(data, response, error, client)) + reject(buildError(data, response, error, client)) } } + }.then { obj in + completionHandler?(.success(obj)) + }.catch { error in + completionHandler?(.failure(error)) } return request } - //MARK: Dispatch Async Main Queue + /// Executes a custom endpoint by name and passing the expected parameters. + @discardableResult + open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: CompletionHandler<[T]>? = nil) -> Request { + return execute( + name, + params: params, + client: client + ) { (result: Result<[T], Swift.Error>) in + switch result { + case .success(let objArray): + completionHandler?(objArray, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } - fileprivate static func dispatchAsyncMainQueue(_ completionHandler: ((R?, Swift.Error?) -> Void)? = nil) -> ((R?, Swift.Error?) -> Void)? { - if let completionHandler = completionHandler { - return { (obj, error) -> Void in - DispatchQueue.main.async(execute: { () -> Void in - completionHandler(obj, error) - }) + /// Executes a custom endpoint by name and passing the expected parameters. + @discardableResult + open static func execute(_ name: String, params: Params? = nil, client: Client = sharedClient, completionHandler: ((Result<[T], Swift.Error>) -> Void)? = nil) -> Request { + var request: Request! + Promise<[T]> { fulfill, reject in + request = callEndpoint(name, params: params, client: client) { data, response, error in + if let response = response, response.isOK, let objArray: [T] = client.responseParser.parse(data) { + fulfill(objArray) + } else { + reject(buildError(data, response, error, client)) + } } + }.then { objArray in + completionHandler?(.success(objArray)) + }.catch { error in + completionHandler?(.failure(error)) } - return nil + return request } } diff --git a/Kinvey/Kinvey/DataStore.swift b/Kinvey/Kinvey/DataStore.swift index 6b5a5e390..78f7dbf07 100644 --- a/Kinvey/Kinvey/DataStore.swift +++ b/Kinvey/Kinvey/DataStore.swift @@ -7,6 +7,7 @@ // import Foundation +import PromiseKit class DataStoreTypeTag: Hashable { @@ -76,7 +77,11 @@ open class DataStore where T: NSObject { open var ttl: TTL? { didSet { if let cache = cache { - cache.ttl = ttl != nil ? ttl!.1.toTimeInterval(ttl!.0) : nil + if let (value, unit) = ttl { + cache.ttl = unit.toTimeInterval(value) + } else { + cache.ttl = nil + } } } } @@ -143,6 +148,25 @@ open class DataStore where T: NSObject { */ @discardableResult open func find(byId id: String, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ObjectCompletionHandler) -> Request { + return find(byId: id, readPolicy: readPolicy) { (result: Result) in + switch result { + case .success(let obj): + completionHandler(obj, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + /** + Gets a single record using the `_id` of the record. + - parameter id: The `_id` value of the entity to be find + - parameter readPolicy: Enforces a different `ReadPolicy` otherwise use the client's `ReadPolicy`. Default value: `nil` + - parameter completionHandler: Completion handler to be called once the respose returns + - returns: A `Request` instance which will allow cancel the request later + */ + @discardableResult + open func find(byId id: String, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result) -> Void) -> Request { return find(id, readPolicy: readPolicy, completionHandler: completionHandler) } @@ -165,11 +189,36 @@ open class DataStore where T: NSObject { */ @discardableResult open func find(_ id: String, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ObjectCompletionHandler) -> Request { + return find(id, readPolicy: readPolicy) { (result: Result) in + switch result { + case .success(let obj): + completionHandler(obj, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + /** + Gets a single record using the `_id` of the record. + + PS: This method is just a shortcut for `findById()` + - parameter id: The `_id` value of the entity to be find + - parameter readPolicy: Enforces a different `ReadPolicy` otherwise use the client's `ReadPolicy`. Default value: `nil` + - parameter completionHandler: Completion handler to be called once the respose returns + - returns: A `Request` instance which will allow cancel the request later + */ + @discardableResult + open func find(_ id: String, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result) -> Void) -> Request { validate(id: id) let readPolicy = readPolicy ?? self.readPolicy let operation = GetOperation(id: id, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute(dispatchAsyncMainQueue(completionHandler)) + let request = operation.execute { result in + DispatchQueue.main.async { + completionHandler(result) + } + } return request } @@ -183,10 +232,34 @@ open class DataStore where T: NSObject { */ @discardableResult open func find(_ query: Query = Query(), deltaSet: Bool? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ArrayCompletionHandler) -> Request { + return find(query, deltaSet: deltaSet, readPolicy: readPolicy) { (result: Result<[T], Swift.Error>) in + switch result { + case .success(let objs): + completionHandler(objs, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + /** + Gets a list of records that matches with the query passed by parameter. + - parameter query: The query used to filter the results + - parameter deltaSet: Enforces delta set cache otherwise use the client's `deltaSet` value. Default value: `false` + - parameter readPolicy: Enforces a different `ReadPolicy` otherwise use the client's `ReadPolicy`. Default value: `nil` + - parameter completionHandler: Completion handler to be called once the respose returns + - returns: A `Request` instance which will allow cancel the request later + */ + @discardableResult + open func find(_ query: Query = Query(), deltaSet: Bool? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result<[T], Swift.Error>) -> Void) -> Request { let readPolicy = readPolicy ?? self.readPolicy let deltaSet = deltaSet ?? self.deltaSet let operation = FindOperation(query: Query(query: query, persistableType: T.self), deltaSet: deltaSet, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute(dispatchAsyncMainQueue(completionHandler)) + let request = operation.execute { result in + DispatchQueue.main.async { + completionHandler(result) + } + } return request } @@ -199,24 +272,65 @@ open class DataStore where T: NSObject { */ @discardableResult open func count(_ query: Query? = nil, readPolicy: ReadPolicy? = nil, completionHandler: IntCompletionHandler?) -> Request { + return count(query, readPolicy: readPolicy) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /** + Gets a count of how many records that matches with the (optional) query passed by parameter. + - parameter query: The query used to filter the results + - parameter readPolicy: Enforces a different `ReadPolicy` otherwise use the client's `ReadPolicy`. Default value: `nil` + - parameter completionHandler: Completion handler to be called once the respose returns + - returns: A `Request` instance which will allow cancel the request later + */ + @discardableResult + open func count(_ query: Query? = nil, readPolicy: ReadPolicy? = nil, completionHandler: ((Result) -> Void)?) -> Request { let readPolicy = readPolicy ?? self.readPolicy let operation = CountOperation(query: query, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute(dispatchAsyncMainQueue(completionHandler)) + let request = operation.execute { result in + DispatchQueue.main.async { + completionHandler?(result) + } + } return request } @discardableResult open func group(keys: [String]? = nil, initialObject: JsonDictionary, reduceJSFunction: String, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationCustomResult]?, Swift.Error?) -> Void) -> Request { + return group(keys: keys, initialObject: initialObject, reduceJSFunction: reduceJSFunction, condition: condition, readPolicy: readPolicy) { (result: Result<[AggregationCustomResult], Swift.Error>) in + switch result { + case .success(let results): + completionHandler(results, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + @discardableResult + open func group(keys: [String]? = nil, initialObject: JsonDictionary, reduceJSFunction: String, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result<[AggregationCustomResult], Swift.Error>) -> Void) -> Request { let readPolicy = readPolicy ?? self.readPolicy let keys = keys ?? [] let aggregation: Aggregation = .custom(keys: keys, initialObject: initialObject, reduceJSFunction: reduceJSFunction) let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute { results, error in - let array = results?.map { - return AggregationCustomResult(value: T(JSON: $0)!, custom: $0) + let request = operation.execute { result in + switch result { + case .success(let results): + let array = results.map { AggregationCustomResult(value: T(JSON: $0)!, custom: $0) } + DispatchQueue.main.async { + completionHandler(.success(array)) + } + case .failure(let error): + DispatchQueue.main.async { + completionHandler(.failure(error)) + } } - let completionHandler = self.dispatchAsyncMainQueue(completionHandler) - completionHandler?(array, error) } return request } @@ -224,100 +338,212 @@ open class DataStore where T: NSObject { @discardableResult open func group(count keys: [String], countType: Count.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationCountResult]?, Swift.Error?) -> Void) -> Request { + return group(count: keys, countType: countType, condition: condition, readPolicy: readPolicy) { (result: Result<[AggregationCountResult], Swift.Error>) in + switch result { + case .success(let results): + completionHandler(results, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + @discardableResult + open func group(count keys: [String], countType: Count.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping + (Result<[AggregationCountResult], Swift.Error>) -> Void) -> Request { let readPolicy = readPolicy ?? self.readPolicy let aggregation: Aggregation = .count(keys: keys) let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute { results, error in - let array = results?.map { - return AggregationCountResult(value: T(JSON: $0)!, count: $0[aggregation.resultKey] as! Count) + let request = operation.execute { result in + switch result { + case .success(let results): + let array = results.map { AggregationCountResult(value: T(JSON: $0)!, count: $0[aggregation.resultKey] as! Count) } + DispatchQueue.main.async { + completionHandler(.success(array)) + } + case .failure(let error): + DispatchQueue.main.async { + completionHandler(.failure(error)) + } } - let completionHandler = self.dispatchAsyncMainQueue(completionHandler) - completionHandler?(array, error) } return request } @discardableResult open func group(keys: [String], sum: String, sumType: Sum.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationSumResult]?, Swift.Error?) -> Void) -> Request { + return group(keys: keys, sum: sum, sumType: sumType, condition: condition, readPolicy: readPolicy) { (result: Result<[AggregationSumResult], Swift.Error>) in + switch result { + case .success(let results): + completionHandler(results, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + @discardableResult + open func group(keys: [String], sum: String, sumType: Sum.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result<[AggregationSumResult], Swift.Error>) -> Void) -> Request { let readPolicy = readPolicy ?? self.readPolicy let aggregation: Aggregation = .sum(keys: keys, sum: sum) let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute { results, error in - let array = results?.map { - return AggregationSumResult(value: T(JSON: $0)!, sum: $0[aggregation.resultKey] as! Sum) + let request = operation.execute { result in + switch result { + case .success(let results): + let array = results.map { AggregationSumResult(value: T(JSON: $0)!, sum: $0[aggregation.resultKey] as! Sum) } + DispatchQueue.main.async { + completionHandler(.success(array)) + } + case .failure(let error): + DispatchQueue.main.async { + completionHandler(.failure(error)) + } } - let completionHandler = self.dispatchAsyncMainQueue(completionHandler) - completionHandler?(array, error) } return request } @discardableResult open func group(keys: [String], avg: String, avgType: Avg.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationAvgResult]?, Swift.Error?) -> Void) -> Request { + return group(keys: keys, avg: avg, avgType: avgType, condition: condition, readPolicy: readPolicy) { (result: Result<[AggregationAvgResult], Swift.Error>) in + switch result { + case .success(let results): + completionHandler(results, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + @discardableResult + open func group(keys: [String], avg: String, avgType: Avg.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result<[AggregationAvgResult], Swift.Error>) -> Void) -> Request { let readPolicy = readPolicy ?? self.readPolicy let aggregation: Aggregation = .avg(keys: keys, avg: avg) let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute { results, error in - let array = results?.map { - return AggregationAvgResult(value: T(JSON: $0)!, avg: $0[aggregation.resultKey] as! Avg) + let request = operation.execute { result in + switch result { + case .success(let results): + let array = results.map { AggregationAvgResult(value: T(JSON: $0)!, avg: $0[aggregation.resultKey] as! Avg) } + DispatchQueue.main.async { + completionHandler(.success(array)) + } + case .failure(let error): + DispatchQueue.main.async { + completionHandler(.failure(error)) + } } - let completionHandler = self.dispatchAsyncMainQueue(completionHandler) - completionHandler?(array, error) } return request } @discardableResult open func group(keys: [String], min: String, minType: Min.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationMinResult]?, Swift.Error?) -> Void) -> Request { + return group(keys: keys, min: min, minType: minType, condition: condition, readPolicy: readPolicy) { (result: Result<[AggregationMinResult], Swift.Error>) in + switch result { + case .success(let results): + completionHandler(results, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + @discardableResult + open func group(keys: [String], min: String, minType: Min.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result<[AggregationMinResult], Swift.Error>) -> Void) -> Request { let readPolicy = readPolicy ?? self.readPolicy let aggregation: Aggregation = .min(keys: keys, min: min) let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute { results, error in - let array = results?.map { - return AggregationMinResult(value: T(JSON: $0)!, min: $0[aggregation.resultKey] as! Min) + let request = operation.execute { result in + switch result { + case .success(let results): + let array = results.map { AggregationMinResult(value: T(JSON: $0)!, min: $0[aggregation.resultKey] as! Min) } + DispatchQueue.main.async { + completionHandler(.success(array)) + } + case .failure(let error): + DispatchQueue.main.async { + completionHandler(.failure(error)) + } } - let completionHandler = self.dispatchAsyncMainQueue(completionHandler) - completionHandler?(array, error) } return request } @discardableResult open func group(keys: [String], max: String, maxType: Max.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping ([AggregationMaxResult]?, Swift.Error?) -> Void) -> Request { + return group(keys: keys, max: max, maxType: maxType, condition: condition, readPolicy: readPolicy) { (result: Result<[AggregationMaxResult], Swift.Error>) in + switch result { + case .success(let results): + completionHandler(results, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } + + @discardableResult + open func group(keys: [String], max: String, maxType: Max.Type? = nil, condition: NSPredicate? = nil, readPolicy: ReadPolicy? = nil, completionHandler: @escaping (Result<[AggregationMaxResult], Swift.Error>) -> Void) -> Request { let readPolicy = readPolicy ?? self.readPolicy let aggregation: Aggregation = .max(keys: keys, max: max) let operation = AggregateOperation(aggregation: aggregation, condition: condition, readPolicy: readPolicy, cache: cache, client: client) - let request = operation.execute { results, error in - let array = results?.map { - return AggregationMaxResult(value: T(JSON: $0)!, max: $0[aggregation.resultKey] as! Max) + let request = operation.execute { result in + switch result { + case .success(let results): + let array = results.map { AggregationMaxResult(value: T(JSON: $0)!, max: $0[aggregation.resultKey] as! Max) } + DispatchQueue.main.async { + completionHandler(.success(array)) + } + case .failure(let error): + DispatchQueue.main.async { + completionHandler(.failure(error)) + } } - let completionHandler = self.dispatchAsyncMainQueue(completionHandler) - completionHandler?(array, error) } return request } /// Creates or updates a record. @discardableResult - open func save(_ persistable: inout T, writePolicy: WritePolicy? = nil, completionHandler: ObjectCompletionHandler?) -> Request { - let writePolicy = writePolicy ?? self.writePolicy - let operation = SaveOperation(persistable: persistable, writePolicy: writePolicy, sync: sync, cache: cache, client: client) - let request = operation.execute(dispatchAsyncMainQueue(completionHandler)) - return request + open func save(_ persistable: T, writePolicy: WritePolicy? = nil, completionHandler: ObjectCompletionHandler? = nil) -> Request { + return save(persistable, writePolicy: writePolicy) { (result: Result) in + switch result { + case .success(let obj): + completionHandler?(obj, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } } /// Creates or updates a record. @discardableResult - open func save(_ persistable: T, writePolicy: WritePolicy? = nil, completionHandler: ObjectCompletionHandler?) -> Request { + open func save(_ persistable: T, writePolicy: WritePolicy? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { let writePolicy = writePolicy ?? self.writePolicy let operation = SaveOperation(persistable: persistable, writePolicy: writePolicy, sync: sync, cache: cache, client: client) - let request = operation.execute(dispatchAsyncMainQueue(completionHandler)) + let request = operation.execute { result in + DispatchQueue.main.async { + completionHandler?(result) + } + } return request } /// Deletes a record. @discardableResult open func remove(_ persistable: T, writePolicy: WritePolicy? = nil, completionHandler: IntCompletionHandler?) throws -> Request { + return try remove(persistable, writePolicy: writePolicy) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Deletes a record. + @discardableResult + open func remove(_ persistable: T, writePolicy: WritePolicy? = nil, completionHandler: ((Result) -> Void)?) throws -> Request { guard let id = persistable.entityId else { log.error("Object Id is missing") throw Error.objectIdMissing @@ -328,6 +554,19 @@ open class DataStore where T: NSObject { /// Deletes a list of records. @discardableResult open func remove(_ array: [T], writePolicy: WritePolicy? = nil, completionHandler: IntCompletionHandler?) -> Request { + return remove(array, writePolicy: writePolicy) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Deletes a list of records. + @discardableResult + open func remove(_ array: [T], writePolicy: WritePolicy? = nil, completionHandler: ((Result) -> Void)?) -> Request { var ids: [String] = [] for persistable in array { if let id = persistable.entityId { @@ -347,11 +586,28 @@ open class DataStore where T: NSObject { /// Deletes a record using the `_id` of the record. @discardableResult open func remove(byId id: String, writePolicy: WritePolicy? = nil, completionHandler: IntCompletionHandler?) -> Request { + return remove(byId: id, writePolicy: writePolicy) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Deletes a record using the `_id` of the record. + @discardableResult + open func remove(byId id: String, writePolicy: WritePolicy? = nil, completionHandler: ((Result) -> Void)?) -> Request { validate(id: id) let writePolicy = writePolicy ?? self.writePolicy let operation = RemoveByIdOperation(objectId: id, writePolicy: writePolicy, sync: sync, cache: cache, client: client) - let request = operation.execute(dispatchAsyncMainQueue(completionHandler)) + let request = operation.execute { result in + DispatchQueue.main.async { + completionHandler?(result) + } + } return request } @@ -365,6 +621,19 @@ open class DataStore where T: NSObject { /// Deletes a list of records using the `_id` of the records. @discardableResult open func remove(byIds ids: [String], writePolicy: WritePolicy? = nil, completionHandler: IntCompletionHandler?) -> Request { + return remove(byIds: ids, writePolicy: writePolicy) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Deletes a list of records using the `_id` of the records. + @discardableResult + open func remove(byIds ids: [String], writePolicy: WritePolicy? = nil, completionHandler: ((Result) -> Void)?) -> Request { if ids.isEmpty { let message = "ids cannot be an empty array" log.severe(message) @@ -378,49 +647,130 @@ open class DataStore where T: NSObject { /// Deletes a list of records that matches with the query passed by parameter. @discardableResult open func remove(_ query: Query = Query(), writePolicy: WritePolicy? = nil, completionHandler: IntCompletionHandler?) -> Request { + return remove(query, writePolicy: writePolicy) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Deletes a list of records that matches with the query passed by parameter. + @discardableResult + open func remove(_ query: Query = Query(), writePolicy: WritePolicy? = nil, completionHandler: ((Result) -> Void)?) -> Request { let writePolicy = writePolicy ?? self.writePolicy let operation = RemoveByQueryOperation(query: Query(query: query, persistableType: T.self), writePolicy: writePolicy, sync: sync, cache: cache, client: client) - let request = operation.execute(dispatchAsyncMainQueue(completionHandler)) + let request = operation.execute { result in + DispatchQueue.main.async { + completionHandler?(result) + } + } return request } /// Deletes all the records. @discardableResult open func removeAll(_ writePolicy: WritePolicy? = nil, completionHandler: IntCompletionHandler?) -> Request { + return removeAll(writePolicy) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Deletes all the records. + @discardableResult + open func removeAll(_ writePolicy: WritePolicy? = nil, completionHandler: ((Result) -> Void)?) -> Request { return remove(writePolicy: writePolicy, completionHandler: completionHandler) } /// Sends to the backend all the pending records in the local cache. @discardableResult open func push(timeout: TimeInterval? = nil, completionHandler: UIntErrorTypeArrayCompletionHandler? = nil) -> Request { - let completionHandler = dispatchAsyncMainQueue(completionHandler) - if type == .network { - completionHandler?(nil, [Error.invalidDataStoreType]) - return LocalRequest() + return push(timeout: timeout) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let errors): + completionHandler?(nil, errors) + } + } + } + + /// Sends to the backend all the pending records in the local cache. + @discardableResult + open func push(timeout: TimeInterval? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + var request: Request! + Promise { fulfill, reject in + if type == .network { + request = LocalRequest() + reject(MultipleErrors(errors: [Error.invalidDataStoreType])) + } else { + let operation = PushOperation(sync: sync, cache: cache, client: client) + request = operation.execute(timeout: timeout) { result in + switch result { + case .success(let count): + fulfill(count) + case .failure(let errors): + reject(MultipleErrors(errors: errors)) + } + } + } + }.then { count in + completionHandler?(.success(count)) + }.catch { error in + let error = error as! MultipleErrors + completionHandler?(.failure(error.errors)) } - - let operation = PushOperation(sync: sync, cache: cache, client: client) - let request = operation.execute(timeout: timeout, completionHandler: completionHandler) return request } /// Gets the records from the backend that matches with the query passed by parameter and saves locally in the local cache. @discardableResult open func pull(_ query: Query = Query(), deltaSet: Bool? = nil, completionHandler: DataStore.ArrayCompletionHandler? = nil) -> Request { - let completionHandler = dispatchAsyncMainQueue(completionHandler) - if type == .network { - completionHandler?(nil, Error.invalidDataStoreType) - return LocalRequest() + return pull(query, deltaSet: deltaSet) { (result: Result<[T], Swift.Error>) in + switch result { + case .success(let array): + completionHandler?(array, nil) + case .failure(let error): + completionHandler?(nil, error) + } } - - if self.syncCount() > 0 { - completionHandler?(nil, Error.invalidOperation(description: "You must push all pending sync items before new data is pulled. Call push() on the data store instance to push pending items, or purge() to remove them.")) - return LocalRequest() + } + + /// Gets the records from the backend that matches with the query passed by parameter and saves locally in the local cache. + @discardableResult + open func pull(_ query: Query = Query(), deltaSet: Bool? = nil, completionHandler: ((Result<[T], Swift.Error>) -> Void)? = nil) -> Request { + var request: Request! + Promise<[T]> { fulfill, reject in + if type == .network { + request = LocalRequest() + reject(Error.invalidDataStoreType) + } else if self.syncCount() > 0 { + request = LocalRequest() + reject(Error.invalidOperation(description: "You must push all pending sync items before new data is pulled. Call push() on the data store instance to push pending items, or purge() to remove them.")) + } else { + let deltaSet = deltaSet ?? self.deltaSet + let operation = PullOperation(query: Query(query: query, persistableType: T.self), deltaSet: deltaSet, readPolicy: .forceNetwork, cache: cache, client: client) + request = operation.execute { result in + switch result { + case .success(let array): + fulfill(array) + case .failure(let error): + reject(error) + } + } + } + }.then { array in + completionHandler?(.success(array)) + }.catch { error in + completionHandler?(.failure(error)) } - - let deltaSet = deltaSet ?? self.deltaSet - let operation = PullOperation(query: Query(query: query, persistableType: T.self), deltaSet: deltaSet, readPolicy: .forceNetwork, cache: cache, client: client) - let request = operation.execute(completionHandler) return request } @@ -435,77 +785,102 @@ open class DataStore where T: NSObject { /// Calls `push` and then `pull` methods, so it sends all the pending records in the local cache and then gets the records from the backend and saves locally in the local cache. @discardableResult open func sync(_ query: Query = Query(), deltaSet: Bool? = nil, completionHandler: UIntArrayCompletionHandler? = nil) -> Request { - let completionHandler = dispatchAsyncMainQueue(completionHandler) - if type == .network { - completionHandler?(nil, nil, [Error.invalidDataStoreType]) - return LocalRequest() - } - - let requests = MultiRequest() - let request = push() { count, errors in - if let count = count , errors == nil || errors!.isEmpty { - let deltaSet = deltaSet ?? self.deltaSet - let request = self.pull(query, deltaSet: deltaSet) { results, error in - completionHandler?(count, results, error != nil ? [error!] : nil) - } - requests.addRequest(request) - } else { - completionHandler?(count, nil, errors) + return sync(query, deltaSet: deltaSet) { (result: Result<(UInt, [T]), [Swift.Error]>) in + switch result { + case .success(let count, let array): + completionHandler?(count, array, nil) + case .failure(let errors): + completionHandler?(nil, nil, errors) } } - requests.addRequest(request) - return requests } - /// Deletes all the pending changes in the local cache. + /// Calls `push` and then `pull` methods, so it sends all the pending records in the local cache and then gets the records from the backend and saves locally in the local cache. @discardableResult - open func purge(_ query: Query = Query(), completionHandler: DataStore.IntCompletionHandler? = nil) -> Request { - let completionHandler = dispatchAsyncMainQueue(completionHandler) - - if type == .network { - completionHandler?(nil, Error.invalidDataStoreType) - return LocalRequest() - } - - let executor = Executor() - - let operation = PurgeOperation(sync: sync, cache: cache, client: client) - let request = operation.execute { (count, error: Swift.Error?) -> Void in - if let count = count { - executor.execute { - self.pull(query) { (results, error) -> Void in - completionHandler?(count, error) + open func sync(_ query: Query = Query(), deltaSet: Bool? = nil, completionHandler: ((Result<(UInt, [T]), [Swift.Error]>) -> Void)? = nil) -> Request { + let requests = MultiRequest() + Promise<(UInt, [T])> { fulfill, reject in + if type == .network { + requests += LocalRequest() + reject(MultipleErrors(errors: [Error.invalidDataStoreType])) + } else { + let request = push() { (result: Result) in + switch result { + case .success(let count): + let deltaSet = deltaSet ?? self.deltaSet + let request = self.pull(query, deltaSet: deltaSet) { (result: Result<[T], Swift.Error>) in + switch result { + case .success(let array): + fulfill(count, array) + case .failure(let error): + reject(error) + } + } + requests.addRequest(request) + case .failure(let errors): + reject(MultipleErrors(errors: errors)) } } - } else { - completionHandler?(count, error) + requests += request } + }.then { count, array in + completionHandler?(.success(count, array)) + }.catch { error in + let error = error as! MultipleErrors + completionHandler?(.failure(error.errors)) } - return request + return requests } - //MARK: Dispatch Async Main Queue - - fileprivate func dispatchAsyncMainQueue(_ completionHandler: ((R?, E?) -> Void)?) -> ((R?, E?) -> Void)? { - if let completionHandler = completionHandler { - return { (obj1: R?, obj2: E?) -> Void in - DispatchQueue.main.async(execute: { () -> Void in - completionHandler(obj1, obj2) - }) + /// Deletes all the pending changes in the local cache. + @discardableResult + open func purge(_ query: Query = Query(), completionHandler: DataStore.IntCompletionHandler? = nil) -> Request { + return purge(query) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) } } - return nil } - fileprivate func dispatchAsyncMainQueue(_ completionHandler: ((R1?, R2?, R3?) -> Void)?) -> ((R1?, R2?, R3?) -> Void)? { - if let completionHandler = completionHandler { - return { (obj1: R1?, obj2: R2?, obj3: R3?) -> Void in - DispatchQueue.main.async(execute: { () -> Void in - completionHandler(obj1, obj2, obj3) - }) + /// Deletes all the pending changes in the local cache. + @discardableResult + open func purge(_ query: Query = Query(), completionHandler: ((Result) -> Void)? = nil) -> Request { + var request: Request! + Promise { fulfill, reject in + if type == .network { + request = LocalRequest() + reject(Error.invalidDataStoreType) + } else { + let executor = Executor() + + let operation = PurgeOperation(sync: sync, cache: cache, client: client) + request = operation.execute { result in + switch result { + case .success(let count): + executor.execute { + self.pull(query) { (result: Result<[T], Swift.Error>) in + switch result { + case .success: + fulfill(count) + case .failure(let error): + reject(error) + } + } + } + case .failure(let error): + reject(error) + } + } } + }.then { count in + completionHandler?(.success(count)) + }.catch { error in + completionHandler?(.failure(error)) } - return nil + return request } /// Clear all data for all collections. diff --git a/Kinvey/Kinvey/Error.swift b/Kinvey/Kinvey/Error.swift index 14d727695..f66f5a31e 100644 --- a/Kinvey/Kinvey/Error.swift +++ b/Kinvey/Kinvey/Error.swift @@ -62,6 +62,10 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD /// Error when the `appKey` and `appSecret` does not match with any Kinvey environment. case appNotFound(description: String) + /// Error when any operation is called but the client was not initiliazed yet. + case clientNotInitialized + + /// Error localized description. public var description: String { let bundle = Bundle(for: Client.self) @@ -90,6 +94,8 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD return NSLocalizedString("Error.invalidDataStoreType", bundle: bundle, comment: "") case .userWithoutEmailOrUsername: return NSLocalizedString("Error.userWithoutEmailOrUsername", bundle: bundle, comment: "") + case .clientNotInitialized: + return NSLocalizedString("Error.clientNotInitialized", bundle: bundle, comment: "") } } @@ -177,3 +183,9 @@ public enum Error: Swift.Error, LocalizedError, CustomStringConvertible, CustomD } } + +struct MultipleErrors: Swift.Error { + + let errors: [Swift.Error] + +} diff --git a/Kinvey/Kinvey/FileStore.swift b/Kinvey/Kinvey/FileStore.swift index b3d5aa46c..d7dbca871 100644 --- a/Kinvey/Kinvey/FileStore.swift +++ b/Kinvey/Kinvey/FileStore.swift @@ -86,6 +86,24 @@ open class FileStore { /// Uploads a `UIImage` in a PNG or JPEG format. @discardableResult open func upload(_ file: File, image: UIImage, imageRepresentation: ImageRepresentation = .png, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + return upload( + file, + image: image, + imageRepresentation: imageRepresentation, + ttl: ttl + ) { (result: Result) in + switch result { + case .success(let file): + completionHandler?(file, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Uploads a `UIImage` in a PNG or JPEG format. + @discardableResult + open func upload(_ file: File, image: UIImage, imageRepresentation: ImageRepresentation = .png, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { let data = imageRepresentation.data(image: image)! file.mimeType = imageRepresentation.mimeType return upload(file, data: data, ttl: ttl, completionHandler: completionHandler) @@ -95,20 +113,54 @@ open class FileStore { /// Uploads a file using the file path. @discardableResult open func upload(_ file: File, path: String, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + return upload( + file, + path: path, + ttl: ttl + ) { (result: Result) in + switch result { + case .success(let file): + completionHandler?(file, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Uploads a file using the file path. + @discardableResult + open func upload(_ file: File, path: String, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { return upload(file, fromSource: .url(URL(fileURLWithPath: path)), ttl: ttl, completionHandler: completionHandler) } /// Uploads a file using a input stream. @discardableResult open func upload(_ file: File, stream: InputStream, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + return upload( + file, + stream: stream, + ttl: ttl + ) { (result: Result) in + switch result { + case .success(let file): + completionHandler?(file, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Uploads a file using a input stream. + @discardableResult + open func upload(_ file: File, stream: InputStream, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { return upload(file, fromSource: .stream(stream), ttl: ttl, completionHandler: completionHandler) } fileprivate func getFileMetadata(_ file: File, ttl: TTL? = nil) -> (request: Request, promise: Promise) { let request = self.client.networkRequestFactory.buildBlobDownloadFile(file, ttl: ttl) let promise = Promise { fulfill, reject in - request.execute({ (data, response, error) -> Void in - if let response = response , response.isOK, + request.execute() { (data, response, error) -> Void in + if let response = response, response.isOK, let json = self.client.responseParser.parse(data), let newFile = File(JSON: json) { newFile.path = file.path @@ -120,7 +172,7 @@ open class FileStore { } else { reject(buildError(data, response, error, self.client)) } - }) + } } return (request: request, promise: promise) } @@ -128,6 +180,23 @@ open class FileStore { /// Uploads a file using a `NSData`. @discardableResult open func upload(_ file: File, data: Data, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + return upload( + file, + data: data, + ttl: ttl + ) { (result: Result) in + switch result { + case .success(let file): + completionHandler?(file, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Uploads a file using a `NSData`. + @discardableResult + open func upload(_ file: File, data: Data, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { return upload(file, fromSource: .data(data), ttl: ttl, completionHandler: completionHandler) } @@ -140,7 +209,7 @@ open class FileStore { } /// Uploads a file using a `NSData`. - fileprivate func upload(_ file: File, fromSource source: InputSource, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { + fileprivate func upload(_ file: File, fromSource source: InputSource, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { if file.size.value == nil { switch source { case let .data(data): @@ -178,8 +247,8 @@ open class FileStore { var request = URLRequest(url: uploadURL) request.httpMethod = "PUT" if let uploadHeaders = file.uploadHeaders { - for header in uploadHeaders { - request.setValue(header.1, forHTTPHeaderField: header.0) + for (headerField, value) in uploadHeaders { + request.setValue(value, forHTTPHeaderField: headerField) } } request.setValue("0", forHTTPHeaderField: "Content-Length") @@ -240,8 +309,8 @@ open class FileStore { var request = URLRequest(url: file.uploadURL!) request.httpMethod = "PUT" if let uploadHeaders = file.uploadHeaders { - for header in uploadHeaders { - request.setValue(header.1, forHTTPHeaderField: header.0) + for (headerField, value) in uploadHeaders { + request.setValue(value, forHTTPHeaderField: headerField) } } @@ -317,11 +386,13 @@ open class FileStore { } } }.then { file in //fetching download url - return self.getFileMetadata(file, ttl: ttl).1 + let (request, promise) = self.getFileMetadata(file, ttl: ttl) + requests += request + return promise }.then { file in - completionHandler?(file, nil) + completionHandler?(.success(file)) }.catch { error in - completionHandler?(file, error) + completionHandler?(.failure(error)) } return requests } @@ -329,12 +400,27 @@ open class FileStore { /// Refresh a `File` instance. @discardableResult open func refresh(_ file: File, ttl: TTL? = nil, completionHandler: FileCompletionHandler? = nil) -> Request { - let fileMetadata = getFileMetadata(file, ttl: ttl) - let request = fileMetadata.0 - fileMetadata.1.then { file in - completionHandler?(file, nil) + return refresh( + file, + ttl: ttl + ) { (result: Result) in + switch result { + case .success(let file): + completionHandler?(file, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Refresh a `File` instance. + @discardableResult + open func refresh(_ file: File, ttl: TTL? = nil, completionHandler: ((Result) -> Void)? = nil) -> Request { + let (request, promise) = getFileMetadata(file, ttl: ttl) + promise.then { file in + completionHandler?(.success(file)) }.catch { error in - completionHandler?(file, error) + completionHandler?(.failure(error)) } return request } @@ -439,15 +525,32 @@ open class FileStore { /// Downloads a file using the `downloadURL` of the `File` instance. @discardableResult open func download(_ file: File, storeType: StoreType = .cache, ttl: TTL? = nil, completionHandler: FilePathCompletionHandler? = nil) -> Request { + return download( + file, + storeType: storeType, + ttl: ttl + ) { (result: Result<(File, URL), Swift.Error>) in + switch result { + case .success(let file, let url): + completionHandler?(file, url, nil) + case .failure(let error): + completionHandler?(nil, nil, error) + } + } + } + + /// Downloads a file using the `downloadURL` of the `File` instance. + @discardableResult + open func download(_ file: File, storeType: StoreType = .cache, ttl: TTL? = nil, completionHandler: ((Result<(File, URL), Swift.Error>) -> Void)? = nil) -> Request { crashIfInvalid(file: file) if storeType == .sync || storeType == .cache, let entityId = file.fileId, let cachedFile = cachedFile(entityId), - file.pathURL != nil + let pathURL = file.pathURL { DispatchQueue.main.async { - completionHandler?(cachedFile, cachedFile.pathURL, nil) + completionHandler?(.success(cachedFile, pathURL)) } } @@ -478,9 +581,9 @@ open class FileStore { } } }.then { file, localUrl -> Void in - completionHandler?(file, localUrl, nil) - }.catch { [file] error in - completionHandler?(file, nil, error) + completionHandler?(.success(file, localUrl)) + }.catch { error in + completionHandler?(.failure(error)) } return multiRequest } else { @@ -491,11 +594,27 @@ open class FileStore { /// Downloads a file using the `downloadURL` of the `File` instance. @discardableResult open func download(_ file: File, ttl: TTL? = nil, completionHandler: FileDataCompletionHandler? = nil) -> Request { + return download( + file, + ttl: ttl + ) { (result: Result<(File, Data), Swift.Error>) in + switch result { + case .success(let file, let data): + completionHandler?(file, data, nil) + case .failure(let error): + completionHandler?(nil, nil, error) + } + } + } + + /// Downloads a file using the `downloadURL` of the `File` instance. + @discardableResult + open func download(_ file: File, ttl: TTL? = nil, completionHandler: ((Result<(File, Data), Swift.Error>) -> Void)? = nil) -> Request { crashIfInvalid(file: file) if let entityId = file.fileId, let cachedFile = cachedFile(entityId), let path = file.path, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { DispatchQueue.main.async { - completionHandler?(cachedFile, data, nil) + completionHandler?(.success(cachedFile, data)) } } @@ -521,9 +640,9 @@ open class FileStore { multiRequest += (request, addProgress: true) return promise }.then { data in - completionHandler?(file, data, nil) + completionHandler?(.success(file, data)) }.catch { error in - completionHandler?(file, nil, error) + completionHandler?(.failure(error)) } return multiRequest } @@ -531,6 +650,19 @@ open class FileStore { /// Deletes a file instance in the backend. @discardableResult open func remove(_ file: File, completionHandler: UIntCompletionHandler? = nil) -> Request { + return remove(file) { (result: Result) in + switch result { + case .success(let count): + completionHandler?(count, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Deletes a file instance in the backend. + @discardableResult + open func remove(_ file: File, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildBlobDeleteFile(file) Promise { fulfill, reject in request.execute({ (data, response, error) -> Void in @@ -548,9 +680,9 @@ open class FileStore { } }) }.then { count in - completionHandler?(count, nil) + completionHandler?(.success(count)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return request } @@ -558,6 +690,22 @@ open class FileStore { /// Gets a list of files that matches with the query passed by parameter. @discardableResult open func find(_ query: Query = Query(), ttl: TTL? = nil, completionHandler: FileArrayCompletionHandler? = nil) -> Request { + return find( + query, + ttl: ttl + ) { (result: Result<[File], Swift.Error>) in + switch result { + case .success(let files): + completionHandler?(files, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Gets a list of files that matches with the query passed by parameter. + @discardableResult + open func find(_ query: Query = Query(), ttl: TTL? = nil, completionHandler: ((Result<[File], Swift.Error>) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildBlobQueryFile(query, ttl: ttl) Promise<[File]> { fulfill, reject in request.execute { (data, response, error) -> Void in @@ -572,9 +720,9 @@ open class FileStore { } } }.then { files in - completionHandler?(files, nil) + completionHandler?(.success(files)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return request } diff --git a/Kinvey/Kinvey/FindOperation.swift b/Kinvey/Kinvey/FindOperation.swift index 393aef173..02a70e49c 100644 --- a/Kinvey/Kinvey/FindOperation.swift +++ b/Kinvey/Kinvey/FindOperation.swift @@ -35,14 +35,14 @@ internal class FindOperation: ReadOperation } @discardableResult - func executeLocal(_ completionHandler: (([T]?, Swift.Error?) -> Void)? = nil) -> Request { + func executeLocal(_ completionHandler: CompletionHandler? = nil) -> Request { let request = LocalRequest() request.execute { () -> Void in if let cache = self.cache { let json = cache.find(byQuery: self.query) - completionHandler?(json, nil) + completionHandler?(.success(json)) } else { - completionHandler?([], nil) + completionHandler?(.success([])) } } return request @@ -51,7 +51,7 @@ internal class FindOperation: ReadOperation typealias ArrayCompletionHandler = ([Any]?, Error?) -> Void @discardableResult - func executeNetwork(_ completionHandler: (([T]?, Swift.Error?) -> Void)? = nil) -> Request { + func executeNetwork(_ completionHandler: CompletionHandler? = nil) -> Request { let deltaSet = self.deltaSet && (cache != nil ? !cache!.isEmpty() : false) let fields: Set? = deltaSet ? [PersistableIdKey, "\(PersistableMetadataKey).\(Metadata.LmtKey)"] : nil let request = client.networkRequestFactory.buildAppDataFindByQuery(collectionName: T.collectionName(), query: fields != nil ? Query(query) { $0.fields = fields } : query) @@ -81,11 +81,12 @@ internal class FindOperation: ReadOperation newRefObjs[key] = value } } - operation.execute { (results, error) -> Void in - if let results = results { + operation.execute { (result) -> Void in + switch result { + case .success(let results): fulfill(results) - } else { - reject(buildError(data, response, error, self.client)) + case .failure(let error): + reject(error) } } } @@ -97,7 +98,7 @@ internal class FindOperation: ReadOperation } self.executeLocal(completionHandler) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } } else if allIds.count > 0 { let query = Query(format: "\(PersistableIdKey) IN %@", allIds) @@ -105,14 +106,15 @@ internal class FindOperation: ReadOperation let operation = FindOperation(query: query, deltaSet: false, readPolicy: .forceNetwork, cache: cache, client: self.client) { jsonArray in newRefObjs = self.reduceToIdsLmts(jsonArray) } - operation.execute { (results, error) -> Void in - if let _ = results { + operation.execute { (result) -> Void in + switch result { + case .success: if self.mustRemoveCachedRecords, let refObjs = newRefObjs { self.removeCachedRecords(cache, keys: refObjs.keys, deleted: deltaSet.deleted) } self.executeLocal(completionHandler) - } else { - completionHandler?(nil, buildError(data, response, error, self.client)) + case .failure(let error): + completionHandler?(.failure(buildError(data, response, error, self.client))) } } } else { @@ -129,13 +131,13 @@ internal class FindOperation: ReadOperation } cache.save(entities: entities) } - completionHandler?(entities, nil) + completionHandler?(.success(entities)) } else { - completionHandler?(nil, buildError(data, response, error, self.client)) + completionHandler?(.failure(buildError(data, response, error, self.client))) } } } else { - completionHandler?(nil, buildError(data, response, error, self.client)) + completionHandler?(.failure(buildError(data, response, error, self.client))) } } return request diff --git a/Kinvey/Kinvey/Geolocation.swift b/Kinvey/Kinvey/Geolocation.swift index b6eaf0e8f..531f541cd 100644 --- a/Kinvey/Kinvey/Geolocation.swift +++ b/Kinvey/Kinvey/Geolocation.swift @@ -65,20 +65,23 @@ class GeoPointTransform: TransformOf { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout GeoPoint, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- (right.1, GeoPointTransform()) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- (map, GeoPointTransform()) } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout GeoPoint?, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- (right.1, GeoPointTransform()) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- (map, GeoPointTransform()) } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout GeoPoint!, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- (right.1, GeoPointTransform()) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- (map, GeoPointTransform()) } func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { diff --git a/Kinvey/Kinvey/GetOperation.swift b/Kinvey/Kinvey/GetOperation.swift index 9ea2afcde..ea0ca7148 100644 --- a/Kinvey/Kinvey/GetOperation.swift +++ b/Kinvey/Kinvey/GetOperation.swift @@ -20,8 +20,11 @@ internal class GetOperation: ReadOperation, R func executeLocal(_ completionHandler: CompletionHandler?) -> Request { let request = LocalRequest() request.execute { () -> Void in - let persistable = self.cache?.find(byId: self.id) - completionHandler?(persistable, nil) + if let persistable = self.cache?.find(byId: self.id) { + completionHandler?(.success(persistable)) + } else { + completionHandler?(.failure(buildError(client: self.client))) + } } return request } @@ -29,14 +32,15 @@ internal class GetOperation: ReadOperation, R func executeNetwork(_ completionHandler: CompletionHandler?) -> Request { let request = client.networkRequestFactory.buildAppDataGetById(collectionName: T.collectionName(), id: id) request.execute() { data, response, error in - if let response = response , response.isOK, let json = self.client.responseParser.parse(data) { + if let response = response, + response.isOK, + let json = self.client.responseParser.parse(data), let obj = T(JSON: json) - if let obj = obj, let cache = self.cache { - cache.save(entity: obj) - } - completionHandler?(obj, nil) + { + self.cache?.save(entity: obj) + completionHandler?(.success(obj)) } else { - completionHandler?(nil, buildError(data, response, error, self.client)) + completionHandler?(.failure(buildError(data, response, error, self.client))) } } return request diff --git a/Kinvey/Kinvey/HttpRequest.swift b/Kinvey/Kinvey/HttpRequest.swift index 1118fd9dd..d87d4ac84 100644 --- a/Kinvey/Kinvey/HttpRequest.swift +++ b/Kinvey/Kinvey/HttpRequest.swift @@ -169,8 +169,8 @@ extension URLRequest { public var description: String { var description = "\(httpMethod ?? "GET") \(url?.absoluteString ?? "")" if let headers = allHTTPHeaderFields { - for keyPair in headers { - description += "\n\(keyPair.0): \(keyPair.1)" + for (headerField, value) in headers { + description += "\n\(headerField): \(value)" } } if let body = httpBody, let bodyString = String(data: body, encoding: String.Encoding.utf8) { @@ -186,8 +186,8 @@ extension HTTPURLResponse { /// Description for the NSHTTPURLResponse including url and headers open override var description: String { var description = "\(statusCode) \(HTTPURLResponse.localizedString(forStatusCode: statusCode))" - for keyPair in allHeaderFields { - description += "\n\(keyPair.0): \(keyPair.1)" + for (headerField, value) in allHeaderFields { + description += "\n\(headerField): \(value)" } return description } @@ -415,8 +415,8 @@ internal class HttpRequest: TaskProgressRequest, Request { var headers = "" if let allHTTPHeaderFields = request.allHTTPHeaderFields { - for header in allHTTPHeaderFields { - headers += "-H \"\(header.0): \(header.1)\" " + for (headerField, value) in allHTTPHeaderFields { + headers += "-H \"\(headerField): \(value)\" " } } return "curl -X \(String(describing: request.httpMethod)) \(headers) \(request.url!)" diff --git a/Kinvey/Kinvey/HttpRequestFactory.swift b/Kinvey/Kinvey/HttpRequestFactory.swift index ba4fd818c..a95592b88 100644 --- a/Kinvey/Kinvey/HttpRequestFactory.swift +++ b/Kinvey/Kinvey/HttpRequestFactory.swift @@ -262,8 +262,8 @@ class HttpRequestFactory: RequestFactory { } fileprivate func ttlInSeconds(_ ttl: TTL?) -> UInt? { - if let ttl = ttl { - return UInt(ttl.1.toTimeInterval(ttl.0)) + if let (value, unit) = ttl { + return UInt(unit.toTimeInterval(value)) } return nil } diff --git a/Kinvey/Kinvey/Kinvey.swift b/Kinvey/Kinvey/Kinvey.swift index 5d596cc85..b27d5149e 100644 --- a/Kinvey/Kinvey/Kinvey.swift +++ b/Kinvey/Kinvey/Kinvey.swift @@ -70,10 +70,22 @@ let defaultTag = "kinvey" let userDocumentDirectory: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! func buildError(_ data: Data?, _ response: URLResponse?, _ error: Swift.Error?, _ client: Client) -> Swift.Error { - return buildError(data, HttpResponse(response: response), error, client) + return buildError(data: data, urlResponse: response, error: error, client: client) +} + +func buildError(data: Data?, urlResponse: URLResponse?, error: Swift.Error?, client: Client) -> Swift.Error { + return buildError(data: data, response: HttpResponse(response: urlResponse), error: error, client: client) } func buildError(_ data: Data?, _ response: Response?, _ error: Swift.Error?, _ client: Client) -> Swift.Error { + return buildError(data: data, response: response, error: error, client: client) +} + +func buildError(client: Client) -> Swift.Error { + return buildError(data: nil, response: nil, error: nil, client: client) +} + +func buildError(data: Data?, response: Response?, error: Swift.Error?, client: Client) -> Swift.Error { if let error = error { return error } else if let response = response , response.isUnauthorized, diff --git a/Kinvey/Kinvey/Localizable.strings b/Kinvey/Kinvey/Localizable.strings index 7fc2b2590..18e2336fc 100644 --- a/Kinvey/Kinvey/Localizable.strings +++ b/Kinvey/Kinvey/Localizable.strings @@ -17,3 +17,5 @@ "Error.invalidDataStoreType" = "DataStore type does not support this operation"; "Error.userWithoutEmailOrUsername" = "User has no email or username"; + +"Error.clientNotInitialized" = "Client is not initialized. Please call the initialize() method to initialize the client and try again."; diff --git a/Kinvey/Kinvey/MIC.swift b/Kinvey/Kinvey/MIC.swift index 311ed4b53..0859240b5 100644 --- a/Kinvey/Kinvey/MIC.swift +++ b/Kinvey/Kinvey/MIC.swift @@ -49,22 +49,35 @@ open class MIC { } @discardableResult - class func login(redirectURI: URL, code: String, client: Client = sharedClient, completionHandler: User.UserHandler? = nil) -> Request { + class func login(redirectURI: URL, code: String, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let requests = MultiRequest() - let request = client.networkRequestFactory.buildOAuthToken(redirectURI: redirectURI, code: code) - request.execute { (data, response, error) in - if let response = response, response.isOK, let authData = client.responseParser.parse(data) { - requests += User.login(authSource: .kinvey, authData, client: client, completionHandler: completionHandler) - } else { - completionHandler?(nil, buildError(data, response, error, client)) + Promise { fulfill, reject in + let request = client.networkRequestFactory.buildOAuthToken(redirectURI: redirectURI, code: code) + request.execute { (data, response, error) in + if let response = response, response.isOK, let authData = client.responseParser.parse(data) { + requests += User.login(authSource: .kinvey, authData, client: client) { (result: Result) in + switch result { + case .success(let user): + fulfill(user) + case .failure(let error): + reject(error) + } + } + } else { + reject(buildError(data, response, error, client)) + } } + requests += request + }.then { user in + completionHandler?(.success(user)) + }.catch { error in + completionHandler?(.failure(error)) } - requests += request return requests } @discardableResult - class func login(redirectURI: URL, username: String, password: String, client: Client = sharedClient, completionHandler: User.UserHandler? = nil) -> Request { + class func login(redirectURI: URL, username: String, password: String, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let requests = MultiRequest() let request = client.networkRequestFactory.buildOAuthGrantAuth(redirectURI: redirectURI) Promise { fulfill, reject in @@ -93,10 +106,11 @@ open class MIC { let url = URL(string: location), let code = parseCode(redirectURI: redirectURI, url: url) { - requests += login(redirectURI: redirectURI, code: code, client: client) { user, error in - if let user = user { + requests += login(redirectURI: redirectURI, code: code, client: client) { result in + switch result { + case .success(let user): fulfill(user as! U) - } else if let error = error { + case .failure(let error): reject(error) } } @@ -108,9 +122,9 @@ open class MIC { requests += request } }.then { user in - completionHandler?(user, nil) + completionHandler?(.success(user)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return requests } @@ -165,15 +179,9 @@ public enum MICApiVersion: String { import UIKit import WebKit -enum MICUserActionResult { - - case cancel, timeout - -} - -class MICLoginViewController : UIViewController, WKNavigationDelegate, UIWebViewDelegate { +class MICLoginViewController: UIViewController, WKNavigationDelegate, UIWebViewDelegate { - typealias UserHandler = (U?, Swift.Error?, MICUserActionResult?) -> Void + typealias UserHandler = (Result) -> Void var activityIndicatorView: UIActivityIndicatorView! @@ -192,12 +200,19 @@ class MICLoginViewController : UIViewController, WKNavigationDelegate, UIWebView } } - init(redirectURI: URL, userType: U.Type, timeout: TimeInterval? = nil, forceUIWebView: Bool = false, client: Client = sharedClient, completionHandler: @escaping UserHandler) { + init(redirectURI: URL, userType: UserType.Type, timeout: TimeInterval? = nil, forceUIWebView: Bool = false, client: Client = sharedClient, completionHandler: @escaping UserHandler) { self.redirectURI = redirectURI self.timeout = timeout self.forceUIWebView = forceUIWebView self.client = client - self.completionHandler = completionHandler as! UserHandler + self.completionHandler = { + switch $0 { + case .success(let user): + completionHandler(.success(user as! UserType)) + case .failure(let error): + completionHandler(.failure(error)) + } + } super.init(nibName: nil, bundle: nil) } @@ -333,17 +348,17 @@ class MICLoginViewController : UIViewController, WKNavigationDelegate, UIWebView } func closeViewControllerUserInteractionCancel(_ sender: Any) { - closeViewControllerUserInteraction(userActionResult: .cancel) + closeViewControllerUserInteraction(.failure(Error.requestCancelled)) } func closeViewControllerUserInteractionTimeout(_ sender: Any) { - closeViewControllerUserInteraction(userActionResult: .timeout) + closeViewControllerUserInteraction(.failure(Error.requestTimeout)) } - func closeViewControllerUserInteraction(user: User? = nil, error: Swift.Error? = nil, userActionResult: MICUserActionResult? = nil) { + func closeViewControllerUserInteraction(_ result: Result) { timer = nil dismiss(animated: true) { - self.completionHandler(user, error, userActionResult) + self.completionHandler(result) } } @@ -357,10 +372,10 @@ class MICLoginViewController : UIViewController, WKNavigationDelegate, UIWebView func success(code: String) { activityIndicatorView.startAnimating() - MIC.login(redirectURI: redirectURI, code: code, client: client) { user, error in + MIC.login(redirectURI: redirectURI, code: code, client: client) { result in self.activityIndicatorView.stopAnimating() - self.closeViewControllerUserInteraction(user: user, error: error) + self.closeViewControllerUserInteraction(result) } } @@ -369,7 +384,7 @@ class MICLoginViewController : UIViewController, WKNavigationDelegate, UIWebView if url == nil || !MIC.isValid(redirectURI: redirectURI, url: url!) { activityIndicatorView.stopAnimating() - closeViewControllerUserInteraction(error: error) + closeViewControllerUserInteraction(.failure(error)) } } diff --git a/Kinvey/Kinvey/MemoryCache.swift b/Kinvey/Kinvey/MemoryCache.swift index ad956d48b..191d699ac 100644 --- a/Kinvey/Kinvey/MemoryCache.swift +++ b/Kinvey/Kinvey/MemoryCache.swift @@ -53,8 +53,8 @@ class MemoryCache: Cache, CacheType where T: NSObject { let kmd = entity.metadata! return (entity.entityId!, kmd.lmt!) } - for item in array { - results[item.0] = item.1 + for (key, value) in array { + results[key] = value } return results } diff --git a/Kinvey/Kinvey/Persistable.swift b/Kinvey/Kinvey/Persistable.swift index 89ae27402..77e8db198 100644 --- a/Kinvey/Kinvey/Persistable.swift +++ b/Kinvey/Kinvey/Persistable.swift @@ -49,56 +49,65 @@ internal func kinveyMappingType(left: String, right: String) { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout T, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- right.1 + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- map } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout T?, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- right.1 + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- map } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout T!, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- right.1 + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- map } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout T, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- right.1 + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- map } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout T?, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- right.1 + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- map } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout T!, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- right.1 + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- map } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout Transform.Object, right: (String, Map, Transform)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- (right.1, right.2) + let (right, map, transform) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- (map, transform) } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout Transform.Object?, right: (String, Map, Transform)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- (right.1, right.2) + let (right, map, transform) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- (map, transform) } /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: inout Transform.Object!, right: (String, Map, Transform)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) - left <- (right.1, right.2) + let (right, map, transform) = right + kinveyMappingType(left: right, right: map.currentKey!) + left <- (map, transform) } class ListValueTransform: TransformOf, [JsonDictionary]> where T: BaseMappable { @@ -126,12 +135,13 @@ class ListValueTransform: TransformOf, [JsonDictio } public func <-(lhs: List, rhs: (String, Map)) { + let (_, map) = rhs var list = lhs - switch rhs.1.mappingType { + switch map.mappingType { case .fromJSON: - list <- (rhs.1, ListValueTransform(list)) + list <- (map, ListValueTransform(list)) case .toJSON: - list <- (rhs.1, ListValueTransform(list)) + list <- (map, ListValueTransform(list)) } } @@ -159,13 +169,14 @@ class StringValueTransform: TransformOf, [String]> { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: List, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) var list = left - switch right.1.mappingType { + switch map.mappingType { case .toJSON: - list <- (right.1, StringValueTransform()) + list <- (map, StringValueTransform()) case .fromJSON: - list <- (right.1, StringValueTransform()) + list <- (map, StringValueTransform()) left.removeAll() left.append(objectsIn: list) } @@ -195,13 +206,14 @@ class IntValueTransform: TransformOf, [Int]> { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: List, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) var list = left - switch right.1.mappingType { + switch map.mappingType { case .toJSON: - list <- (right.1, IntValueTransform()) + list <- (map, IntValueTransform()) case .fromJSON: - list <- (right.1, IntValueTransform()) + list <- (map, IntValueTransform()) left.removeAll() left.append(objectsIn: list) } @@ -237,13 +249,14 @@ class FloatValueTransform: TransformOf, [Float]> { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: List, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) var list = left - switch right.1.mappingType { + switch map.mappingType { case .toJSON: - list <- (right.1, FloatValueTransform()) + list <- (map, FloatValueTransform()) case .fromJSON: - list <- (right.1, FloatValueTransform()) + list <- (map, FloatValueTransform()) left.removeAll() left.append(objectsIn: list) } @@ -273,13 +286,14 @@ class DoubleValueTransform: TransformOf, [Double]> { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: List, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) var list = left - switch right.1.mappingType { + switch map.mappingType { case .toJSON: - list <- (right.1, DoubleValueTransform()) + list <- (map, DoubleValueTransform()) case .fromJSON: - list <- (right.1, DoubleValueTransform()) + list <- (map, DoubleValueTransform()) left.removeAll() left.append(objectsIn: list) } @@ -309,13 +323,14 @@ class BoolValueTransform: TransformOf, [Bool]> { /// Override operator used during the `propertyMapping(_:)` method. public func <- (left: List, right: (String, Map)) { - kinveyMappingType(left: right.0, right: right.1.currentKey!) + let (right, map) = right + kinveyMappingType(left: right, right: map.currentKey!) var list = left - switch right.1.mappingType { + switch map.mappingType { case .toJSON: - list <- (right.1, BoolValueTransform()) + list <- (map, BoolValueTransform()) case .fromJSON: - list <- (right.1, BoolValueTransform()) + list <- (map, BoolValueTransform()) left.removeAll() left.append(objectsIn: list) } @@ -368,13 +383,13 @@ extension Persistable { static func propertyMappingReverse() -> [String : [String]] { var results = [String : [String]]() - for keyPair in propertyMapping() { - var properties = results[keyPair.1] + for (key, value) in propertyMapping() { + var properties = results[value] if properties == nil { properties = [String]() } - properties!.append(keyPair.0) - results[keyPair.1] = properties + properties!.append(key) + results[value] = properties } guard results[PersistableIdKey] != nil, diff --git a/Kinvey/Kinvey/PurgeOperation.swift b/Kinvey/Kinvey/PurgeOperation.swift index d5bb7376f..435421a14 100644 --- a/Kinvey/Kinvey/PurgeOperation.swift +++ b/Kinvey/Kinvey/PurgeOperation.swift @@ -9,7 +9,7 @@ import Foundation import PromiseKit -internal class PurgeOperation: SyncOperation where T: NSObject { +internal class PurgeOperation: SyncOperation where T: NSObject { internal override init(sync: AnySync?, cache: AnyCache?, client: Client) { super.init(sync: sync, cache: cache, client: client) @@ -66,9 +66,9 @@ internal class PurgeOperation: SyncOperation) -> Void)?) { + func replaceAppDelegateMethods(_ completionHandler: ((Result) -> Void)?) { let app = UIApplication.shared let appDelegate = app.delegate! let appDelegateType = type(of: appDelegate) @@ -142,6 +142,31 @@ open class Push { */ @available(iOS, deprecated: 10.0, message: "Please use registerForNotifications() instead") open func registerForPush(forTypes types: UIUserNotificationType = [.alert, .badge, .sound], categories: Set? = nil, completionHandler: BoolCompletionHandler? = nil) { + registerForPush( + forTypes: types, + categories: categories + ) { (result: Result) in + switch result { + case .success(let granted): + completionHandler?(granted, nil) + case .failure(let error): + completionHandler?(false, error) + } + } + } + + /** + Register for remote notifications. + Call this in your implementation for updating the registration in case the device tokens change. + + ``` + func applicationDidBecomeActive(application: UIApplication) { + Kinvey.sharedClient.push.registerForPush() + } + ``` + */ + @available(iOS, deprecated: 10.0, message: "Please use registerForNotifications() instead") + open func registerForPush(forTypes types: UIUserNotificationType = [.alert, .badge, .sound], categories: Set? = nil, completionHandler: ((Result) -> Void)? = nil) { replaceAppDelegateMethods(completionHandler) let app = UIApplication.shared @@ -155,6 +180,21 @@ open class Push { @available(iOS 10.0, *) open func registerForNotifications(authorizationOptions: UNAuthorizationOptions = [.badge, .sound, .alert, .carPlay], categories: Set? = nil, completionHandler: BoolCompletionHandler? = nil) { + registerForNotifications( + authorizationOptions: authorizationOptions, + categories: categories + ) { (result: Result) in + switch result { + case .success(let granted): + completionHandler?(granted, nil) + case .failure(let error): + completionHandler?(false, error) + } + } + } + + @available(iOS 10.0, *) + open func registerForNotifications(authorizationOptions: UNAuthorizationOptions = [.badge, .sound, .alert, .carPlay], categories: Set? = nil, completionHandler: ((Result) -> Void)? = nil) { UNUserNotificationCenter.current().requestAuthorization(options: authorizationOptions) { granted, error in if granted { if let categories = categories { @@ -163,7 +203,11 @@ open class Push { self.replaceAppDelegateMethods(completionHandler) UIApplication.shared.registerForRemoteNotifications() } else { - completionHandler?(granted, error) + if let error = error { + completionHandler?(.failure(error)) + } else { + completionHandler?(.success(granted)) + } } } } @@ -195,7 +239,7 @@ open class Push { /// Call this method inside your App Delegate method `application(application:didRegisterForRemoteNotificationsWithDeviceToken:completionHandler:)`. #if os(iOS) - fileprivate func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data, completionHandler: BoolCompletionHandler? = nil) { + fileprivate func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data, completionHandler: ((Result) -> Void)? = nil) { self.deviceToken = deviceToken let block: () -> Void = { Promise { fulfill, reject in @@ -208,9 +252,9 @@ open class Push { } }) }.then { success in - completionHandler?(success, nil) + completionHandler?(.success(success)) }.catch { error in - completionHandler?(false, error) + completionHandler?(.failure(error)) } } if let _ = self.client.activeUser { diff --git a/Kinvey/Kinvey/PushOperation.swift b/Kinvey/Kinvey/PushOperation.swift index acc4ed868..1360e981e 100644 --- a/Kinvey/Kinvey/PushOperation.swift +++ b/Kinvey/Kinvey/PushOperation.swift @@ -61,7 +61,7 @@ fileprivate class PushRequest: NSObject, Request { } -internal class PushOperation: SyncOperation where T: NSObject { +internal class PushOperation: SyncOperation where T: NSObject { internal override init(sync: AnySync?, cache: AnyCache?, client: Client) { super.init(sync: sync, cache: cache, client: client) @@ -73,7 +73,11 @@ internal class PushOperation: SyncOperation 0 ? errors : nil) + if errors.isEmpty { + completionHandler?(.success(count)) + } else { + completionHandler?(.failure(errors)) + } } let pendingBlockOperations = operationsQueue.pendingBlockOperations(forCollection: collectionName) diff --git a/Kinvey/Kinvey/ReadOperation.swift b/Kinvey/Kinvey/ReadOperation.swift index 533b673ff..eddac558a 100644 --- a/Kinvey/Kinvey/ReadOperation.swift +++ b/Kinvey/Kinvey/ReadOperation.swift @@ -10,7 +10,7 @@ import Foundation internal class ReadOperation: Operation where T: NSObject { - typealias CompletionHandler = (R?, E?) -> Void + typealias CompletionHandler = (Result) -> Void let readPolicy: ReadPolicy @@ -25,7 +25,7 @@ protocol ReadOperationType { associatedtype SuccessType associatedtype FailureType - typealias CompletionHandler = (SuccessType?, FailureType?) -> Void + typealias CompletionHandler = (Result) -> Void var readPolicy: ReadPolicy { get } @@ -48,8 +48,8 @@ extension ReadOperationType { return executeNetwork(completionHandler) case .both: let request = MultiRequest() - executeLocal() { obj, error in - completionHandler?(obj, nil) + executeLocal() { result in + completionHandler?(result) request.addRequest(self.executeNetwork(completionHandler)) } return request diff --git a/Kinvey/Kinvey/RemoveOperation.swift b/Kinvey/Kinvey/RemoveOperation.swift index c1b2687e6..d25f0cd65 100644 --- a/Kinvey/Kinvey/RemoveOperation.swift +++ b/Kinvey/Kinvey/RemoveOperation.swift @@ -8,7 +8,7 @@ import Foundation -class RemoveOperation: WriteOperation where T: NSObject { +class RemoveOperation: WriteOperation, WriteOperationType where T: NSObject { let query: Query lazy var request: HttpRequest = self.buildRequest() @@ -24,7 +24,7 @@ class RemoveOperation: WriteOperation where T: NSObject fatalError(message) } - override func executeLocal(_ completionHandler: CompletionHandler? = nil) -> Request { + func executeLocal(_ completionHandler: CompletionHandler? = nil) -> Request { let request = LocalRequest() request.execute { () -> Void in var count: Int? @@ -47,21 +47,25 @@ class RemoveOperation: WriteOperation where T: NSObject count = 0 } } - completionHandler?(count, nil) + if let count = count { + completionHandler?(.success(count)) + } else { + completionHandler?(.failure(buildError(client: client))) + } } return request } - override func executeNetwork(_ completionHandler: CompletionHandler? = nil) -> Request { + func executeNetwork(_ completionHandler: CompletionHandler? = nil) -> Request { request.execute() { data, response, error in if let response = response , response.isOK, let results = self.client.responseParser.parse(data), let count = results["count"] as? Int { self.cache?.remove(byQuery: self.query) - completionHandler?(count, nil) + completionHandler?(.success(count)) } else { - completionHandler?(nil, buildError(data, response, error, self.client)) + completionHandler?(.failure(buildError(data, response, error, self.client))) } } return request diff --git a/Kinvey/Kinvey/Result.swift b/Kinvey/Kinvey/Result.swift new file mode 100644 index 000000000..8270ba06f --- /dev/null +++ b/Kinvey/Kinvey/Result.swift @@ -0,0 +1,16 @@ +// +// Result.swift +// Kinvey +// +// Created by Victor Hugo on 2017-04-11. +// Copyright © 2017 Kinvey. All rights reserved. +// + +import Foundation + +public enum Result { + + case success(SuccessType) + case failure(FailureType) + +} diff --git a/Kinvey/Kinvey/SaveOperation.swift b/Kinvey/Kinvey/SaveOperation.swift index 318fd72c4..2c023ce43 100644 --- a/Kinvey/Kinvey/SaveOperation.swift +++ b/Kinvey/Kinvey/SaveOperation.swift @@ -8,7 +8,7 @@ import Foundation -internal class SaveOperation: WriteOperation where T: NSObject { +internal class SaveOperation: WriteOperation, WriteOperationType where T: NSObject { var persistable: T @@ -22,7 +22,7 @@ internal class SaveOperation: WriteOperation where T: NSO super.init(writePolicy: writePolicy, sync: sync, cache: cache, client: client) } - override func executeLocal(_ completionHandler: CompletionHandler?) -> Request { + func executeLocal(_ completionHandler: CompletionHandler?) -> Request { let request = LocalRequest() request.execute { () -> Void in let request = self.client.networkRequestFactory.buildAppDataSave(self.persistable) @@ -35,12 +35,12 @@ internal class SaveOperation: WriteOperation where T: NSO if let sync = self.sync { sync.savePendingOperation(sync.createPendingOperation(request.request, objectId: persistable.entityId)) } - completionHandler?(self.persistable, nil) + completionHandler?(.success(self.persistable)) } return request } - override func executeNetwork(_ completionHandler: CompletionHandler?) -> Request { + func executeNetwork(_ completionHandler: CompletionHandler?) -> Request { let request = client.networkRequestFactory.buildAppDataSave(persistable) if checkRequirements(completionHandler) { request.execute() { data, response, error in @@ -57,18 +57,18 @@ internal class SaveOperation: WriteOperation where T: NSO } self.merge(&self.persistable, json: json) } - completionHandler?(self.persistable, nil) + completionHandler?(.success(self.persistable)) } else { - completionHandler?(nil, buildError(data, response, error, self.client)) + completionHandler?(.failure(buildError(data, response, error, self.client))) } } } return request } - fileprivate func checkRequirements(_ completionHandler: ObjectCompletionHandler?) -> Bool { + fileprivate func checkRequirements(_ completionHandler: CompletionHandler?) -> Bool { guard let _ = client.activeUser else { - completionHandler?(nil, Error.noActiveUser) + completionHandler?(.failure(Error.noActiveUser)) return false } diff --git a/Kinvey/Kinvey/SyncOperation.swift b/Kinvey/Kinvey/SyncOperation.swift index 132c73dfd..c3dedc3c0 100644 --- a/Kinvey/Kinvey/SyncOperation.swift +++ b/Kinvey/Kinvey/SyncOperation.swift @@ -10,7 +10,7 @@ import Foundation internal class SyncOperation: Operation where T: NSObject { - internal typealias CompletionHandler = (R, E) -> Void + internal typealias CompletionHandler = (Result) -> Void let sync: AnySync? diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index e529fe12b..51257edd3 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -54,18 +54,33 @@ open class User: NSObject, Credential, Mappable { internal var client: Client - private class func validate(client: Client) { - guard client.isInitialized() else { - let message = "Client is not initialized. Call Kinvey.sharedClient.initialize(...) to initialize the client before attempting to sign up or login." - log.severe(message) - fatalError(message) + /// Creates a new `User` taking (optionally) a username and password. If no `username` or `password` was provided, random values will be generated automatically. + @discardableResult + open class func signup(username: String? = nil, password: String? = nil, user: U? = nil, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { + return signup( + username: username, + password: password, + user: user, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) + } } } /// Creates a new `User` taking (optionally) a username and password. If no `username` or `password` was provided, random values will be generated automatically. @discardableResult - open class func signup(username: String? = nil, password: String? = nil, user: U? = nil, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { - validate(client: client) + open class func signup(username: String? = nil, password: String? = nil, user: U? = nil, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + if let error = client.validate() { + DispatchQueue.main.async { + completionHandler?(.failure(error)) + } + return LocalRequest() + } let request = client.networkRequestFactory.buildUserSignUp(username: username, password: password, user: user) Promise { fulfill, reject in @@ -78,16 +93,16 @@ open class User: NSObject, Credential, Mappable { } } }.then { user in - completionHandler?(user, nil) + completionHandler?(.success(user)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return request } /// Deletes a `User` by the `userId` property. @discardableResult - open class func destroy(userId: String, hard: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + open class func destroy(userId: String, hard: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserDelete(userId: userId, hard: hard) Promise { fulfill, reject in request.execute() { (data, response, error) in @@ -101,16 +116,16 @@ open class User: NSObject, Credential, Mappable { } } }.then { _ in - completionHandler?(nil) + completionHandler?(.success()) }.catch { error in - completionHandler?(error) + completionHandler?(.failure(error)) } return request } /// Deletes the `User`. @discardableResult - open func destroy(hard: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + open func destroy(hard: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { return User.destroy(userId: userId, hard: hard, client: client, completionHandler: completionHandler) } @@ -123,7 +138,36 @@ open class User: NSObject, Credential, Mappable { */ @discardableResult open class func login(authSource: AuthSource, _ authData: [String : Any], createIfNotExists: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { - validate(client: client) + return login( + authSource: authSource, + authData, + createIfNotExists: createIfNotExists, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /** + Sign in a user with a social identity. + - parameter authSource: Authentication source enum + - parameter authData: Authentication data from the social provider + - parameter client: Define the `Client` to be used for all the requests for the `DataStore` that will be returned. Default value: `Kinvey.sharedClient` + - parameter completionHandler: Completion handler to be called once the response returns from the server + */ + @discardableResult + open class func login(authSource: AuthSource, _ authData: [String : Any], createIfNotExists: Bool = true, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + if let error = client.validate() { + DispatchQueue.main.async { + completionHandler?(.failure(error)) + } + return LocalRequest() + } let requests = MultiRequest() Promise { fulfill, reject in @@ -153,9 +197,9 @@ open class User: NSObject, Credential, Mappable { } requests += request }.then { user in - completionHandler?(user, nil) + completionHandler?(.success(user)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return requests } @@ -163,7 +207,29 @@ open class User: NSObject, Credential, Mappable { /// Sign in a user and set as a current active user. @discardableResult open class func login(username: String, password: String, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { - validate(client: client) + return login( + username: username, + password: password, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Sign in a user and set as a current active user. + @discardableResult + open class func login(username: String, password: String, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { + if let error = client.validate() { + DispatchQueue.main.async { + completionHandler?(.failure(error)) + } + return LocalRequest() + } let request = client.networkRequestFactory.buildUserLogin(username: username, password: password) Promise { fulfill, reject in @@ -176,9 +242,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { user in - completionHandler?(user, nil) + completionHandler?(.success(user)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return request } @@ -193,20 +259,20 @@ open class User: NSObject, Credential, Mappable { - parameter completionHandler: Completion handler to be called once the response returns from the server */ @discardableResult - open class func sendEmailConfirmation(forUsername username: String, client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + open class func sendEmailConfirmation(forUsername username: String, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildSendEmailConfirmation(forUsername: username) Promise { fulfill, reject in request.execute() { (data, response, error) in - if let response = response , response.isOK { + if let response = response, response.isOK { fulfill() } else { reject(buildError(data, response, error, client)) } } }.then { - completionHandler?(nil) + completionHandler?(.success()) }.catch { error in - completionHandler?(error) + completionHandler?(.failure(error)) } return request } @@ -220,7 +286,7 @@ open class User: NSObject, Credential, Mappable { - parameter completionHandler: Completion handler to be called once the response returns from the server */ @discardableResult - open func sendEmailConfirmation(_ client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + open func sendEmailConfirmation(_ client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { guard let username = username else { let message = "Username is required to send the email confirmation" log.severe(message) @@ -235,9 +301,25 @@ open class User: NSObject, Credential, Mappable { return User.sendEmailConfirmation(forUsername: username, client: client, completionHandler: completionHandler) } + /// Sends an email to the user with a link to reset the password @discardableResult + private class func resetPassword(usernameOrEmail: String, client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + return resetPassword( + usernameOrEmail: usernameOrEmail, + client: client + ) { (result: Result) in + switch result { + case .success: + completionHandler?(nil) + case .failure(let error): + completionHandler?(error) + } + } + } + /// Sends an email to the user with a link to reset the password - open class func resetPassword(usernameOrEmail: String, client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + @discardableResult + open class func resetPassword(usernameOrEmail: String, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserResetPassword(usernameOrEmail: usernameOrEmail) Promise { fulfill, reject in request.execute() { (data, response, error) in @@ -248,9 +330,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { - completionHandler?(nil) + completionHandler?(.success()) }.catch { error in - completionHandler?(error) + completionHandler?(.failure(error)) } return request } @@ -271,14 +353,14 @@ open class User: NSObject, Credential, Mappable { /// Sends an email to the user with a link to reset the password. @discardableResult - open func resetPassword(_ client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + open func resetPassword(_ client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { if let email = email { return User.resetPassword(usernameOrEmail: email, client: client, completionHandler: completionHandler) } else if let username = username { return User.resetPassword(usernameOrEmail: username, client: client, completionHandler: completionHandler) } else if let completionHandler = completionHandler { DispatchQueue.main.async(execute: { () -> Void in - completionHandler(Error.userWithoutEmailOrUsername) + completionHandler(.failure(Error.userWithoutEmailOrUsername)) }) } return LocalRequest() @@ -292,6 +374,27 @@ open class User: NSObject, Credential, Mappable { */ @discardableResult open func changePassword(newPassword: String, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { + return changePassword( + newPassword: newPassword, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /** + Changes the password for the current user and automatically updates the session with a new valid session. + - parameter newPassword: A new password for the user + - parameter client: Define the `Client` to be used for all the requests for the `DataStore` that will be returned. Default value: `Kinvey.sharedClient` + - parameter completionHandler: Completion handler to be called once the response returns from the server + */ + @discardableResult + open func changePassword(newPassword: String, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { return save(newPassword: newPassword, client: client, completionHandler: completionHandler) } @@ -302,7 +405,7 @@ open class User: NSObject, Credential, Mappable { - parameter completionHandler: Completion handler to be called once the response returns from the server */ @discardableResult - open class func forgotUsername(email: String, client: Client = Kinvey.sharedClient, completionHandler: VoidHandler? = nil) -> Request { + open class func forgotUsername(email: String, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserForgotUsername(email: email) Promise { fulfill, reject in request.execute() { (data, response, error) in @@ -313,9 +416,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { - completionHandler?(nil) + completionHandler?(.success()) }.catch { error in - completionHandler?(error) + completionHandler?(.failure(error)) } return request } @@ -323,6 +426,22 @@ open class User: NSObject, Credential, Mappable { /// Checks if a `username` already exists or not. @discardableResult open class func exists(username: String, client: Client = Kinvey.sharedClient, completionHandler: BoolHandler? = nil) -> Request { + return exists( + username: username, + client: client + ) { (result: Result) in + switch result { + case .success(let exists): + completionHandler?(exists, nil) + case .failure(let error): + completionHandler?(false, error) + } + } + } + + /// Checks if a `username` already exists or not. + @discardableResult + open class func exists(username: String, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserExists(username: username) Promise { fulfill, reject in request.execute() { (data, response, error) in @@ -333,9 +452,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { exists in - completionHandler?(exists, nil) + completionHandler?(.success(exists)) }.catch { error in - completionHandler?(false, error) + completionHandler?(.failure(error)) } return request } @@ -343,6 +462,22 @@ open class User: NSObject, Credential, Mappable { /// Gets a `User` instance using the `userId` property. @discardableResult open class func get(userId: String, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { + return get( + userId: userId, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Gets a `User` instance using the `userId` property. + @discardableResult + open class func get(userId: String, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserGet(userId: userId) Promise { fulfill, reject in request.execute() { (data, response, error) in @@ -353,9 +488,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { user in - completionHandler?(user, nil) + completionHandler?(.success(user)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return request } @@ -405,6 +540,22 @@ open class User: NSObject, Credential, Mappable { /// Creates or updates a `User`. @discardableResult open func save(newPassword: String? = nil, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) -> Request { + return save( + newPassword: newPassword, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Creates or updates a `User`. + @discardableResult + open func save(newPassword: String? = nil, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserSave(user: self, newPassword: newPassword) Promise { fulfill, reject in request.execute() { (data, response, error) in @@ -416,9 +567,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { user in - completionHandler?(user, nil) + completionHandler?(.success(user)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return request } @@ -428,6 +579,21 @@ open class User: NSObject, Credential, Mappable { */ @discardableResult open func lookup(_ userQuery: UserQuery, client: Client = Kinvey.sharedClient, completionHandler: UsersHandler? = nil) -> Request { + return lookup(userQuery, client: client) { (result: Result<[U], Swift.Error>) in + switch result { + case .success(let users): + completionHandler?(users, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /** + This method allows users to do exact queries for other users restricted to the `UserQuery` attributes. + */ + @discardableResult + open func lookup(_ userQuery: UserQuery, client: Client = Kinvey.sharedClient, completionHandler: ((Result<[U], Swift.Error>) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserLookup(user: self, userQuery: userQuery) Promise<[U]> { fulfill, reject in request.execute() { (data, response, error) in @@ -438,9 +604,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { users in - completionHandler?(users, nil) + completionHandler?(.success(users)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } return request } @@ -462,21 +628,47 @@ open class User: NSObject, Credential, Mappable { Login with MIC using Automated Authorization Grant Flow. We strongly recommend use [Authorization Code Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#authorization-grant) instead of [Automated Authorization Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#automated-authorization-grant) for security reasons. */ open class func login(redirectURI: URL, username: String, password: String, client: Client = sharedClient, completionHandler: UserHandler? = nil) { - MIC.login(redirectURI: redirectURI, username: username, password: password, client: client) { user, error in - DispatchQueue.main.async { - completionHandler?(user, error) + return login( + redirectURI: redirectURI, + username: username, + password: password, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) } } } + + /** + Login with MIC using Automated Authorization Grant Flow. We strongly recommend use [Authorization Code Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#authorization-grant) instead of [Automated Authorization Grant Flow](http://devcenter.kinvey.com/rest/guides/mobile-identity-connect#automated-authorization-grant) for security reasons. + */ + open class func login(redirectURI: URL, username: String, password: String, client: Client = sharedClient, completionHandler: ((Result) -> Void)? = nil) { + MIC.login(redirectURI: redirectURI, username: username, password: password, client: client, completionHandler: completionHandler) + } #if os(iOS) - private static let MICSafariViewControllerNotificationName = NSNotification.Name("Kinvey.User.MICSafariViewController") + private static let MICSafariViewControllerSuccessNotificationName = NSNotification.Name("Kinvey.User.MICSafariViewController.Success") + private static let MICSafariViewControllerFailureNotificationName = NSNotification.Name("Kinvey.User.MICSafariViewController.Failure") - private static var MICSafariViewControllerNotificationObserver: Any? = nil { + private static var MICSafariViewControllerSuccessNotificationObserver: Any? = nil { willSet { - if let token = MICSafariViewControllerNotificationObserver { - NotificationCenter.default.removeObserver(token, name: MICSafariViewControllerNotificationName, object: nil) + if let token = MICSafariViewControllerSuccessNotificationObserver { + NotificationCenter.default.removeObserver(token, name: MICSafariViewControllerSuccessNotificationName, object: nil) + NotificationCenter.default.removeObserver(token, name: MICSafariViewControllerFailureNotificationName, object: nil) + } + } + } + + private static var MICSafariViewControllerFailureNotificationObserver: Any? = nil { + willSet { + if let token = MICSafariViewControllerFailureNotificationObserver { + NotificationCenter.default.removeObserver(token, name: MICSafariViewControllerSuccessNotificationName, object: nil) + NotificationCenter.default.removeObserver(token, name: MICSafariViewControllerFailureNotificationName, object: nil) } } } @@ -484,12 +676,19 @@ open class User: NSObject, Credential, Mappable { /// Performs a login using the MIC Redirect URL that contains a temporary token. open class func login(redirectURI: URL, micURL: URL, client: Client = sharedClient) -> Bool { if let code = MIC.parseCode(redirectURI: redirectURI, url: micURL) { - MIC.login(redirectURI: redirectURI, code: code, client: client) { (user, error) in - let object = UserError(user: user, error: error) - NotificationCenter.default.post( - name: MICSafariViewControllerNotificationName, - object: object - ) + MIC.login(redirectURI: redirectURI, code: code, client: client) { result in + switch result { + case .success(let user): + NotificationCenter.default.post( + name: MICSafariViewControllerSuccessNotificationName, + object: user + ) + case .failure(let error): + NotificationCenter.default.post( + name: MICSafariViewControllerFailureNotificationName, + object: error + ) + } } return true } @@ -504,7 +703,30 @@ open class User: NSObject, Credential, Mappable { /// Presents the MIC View Controller to sign in a user using MIC (Mobile Identity Connect). open class func presentMICViewController(redirectURI: URL, timeout: TimeInterval = 0, micUserInterface: MICUserInterface = .safari, currentViewController: UIViewController? = nil, client: Client = Kinvey.sharedClient, completionHandler: UserHandler? = nil) { - validate(client: client) + presentMICViewController( + redirectURI: redirectURI, + timeout: timeout, + micUserInterface: micUserInterface, + currentViewController: currentViewController, + client: client + ) { (result: Result) in + switch result { + case .success(let user): + completionHandler?(user, nil) + case .failure(let error): + completionHandler?(nil, error) + } + } + } + + /// Presents the MIC View Controller to sign in a user using MIC (Mobile Identity Connect). + open class func presentMICViewController(redirectURI: URL, timeout: TimeInterval = 0, micUserInterface: MICUserInterface = .safari, currentViewController: UIViewController? = nil, client: Client = Kinvey.sharedClient, completionHandler: ((Result) -> Void)? = nil) { + if let error = client.validate() { + DispatchQueue.main.async { + completionHandler?(.failure(error)) + } + return + } Promise { fulfill, reject in var micVC: UIViewController! @@ -514,47 +736,52 @@ open class User: NSObject, Credential, Mappable { let url = MIC.urlForLogin(redirectURI: redirectURI) micVC = SFSafariViewController(url: url) micVC.modalPresentationStyle = .overCurrentContext - MICSafariViewControllerNotificationObserver = NotificationCenter.default.addObserver( - forName: MICSafariViewControllerNotificationName, + MICSafariViewControllerSuccessNotificationObserver = NotificationCenter.default.addObserver( + forName: MICSafariViewControllerSuccessNotificationName, object: nil, queue: OperationQueue.main) { notification in micVC.dismiss(animated: true) { - MICSafariViewControllerNotificationObserver = nil + MICSafariViewControllerSuccessNotificationObserver = nil - if let object = notification.object as? UserError { - if let user = object.user { - fulfill(user) - } else if let error = object.error { - reject(error) - } else { - reject(Error.invalidResponse(httpResponse: nil, data: nil)) - } + if let user = notification.object as? U { + fulfill(user) } else { reject(Error.invalidResponse(httpResponse: nil, data: nil)) } } } - default: - let forceUIWebView = micUserInterface == .uiWebView - let micLoginVC = MICLoginViewController(redirectURI: redirectURI, userType: client.userType, timeout: timeout, forceUIWebView: forceUIWebView, client: client) { user, error, userActionResult in - if let userActionResult = userActionResult { - switch userActionResult { - case .cancel: - reject(Error.requestCancelled) - case .timeout: - reject(Error.requestTimeout) - } - } else { - if let user = user as? U { - fulfill(user) - } else if let error = error { + MICSafariViewControllerFailureNotificationObserver = NotificationCenter.default.addObserver( + forName: MICSafariViewControllerFailureNotificationName, + object: nil, + queue: OperationQueue.main) + { notification in + micVC.dismiss(animated: true) { + MICSafariViewControllerFailureNotificationObserver = nil + + if let error = notification.object as? Swift.Error { reject(error) } else { reject(Error.invalidResponse(httpResponse: nil, data: nil)) } } } + default: + let forceUIWebView = micUserInterface == .uiWebView + let micLoginVC = MICLoginViewController( + redirectURI: redirectURI, + userType: client.userType, + timeout: timeout, + forceUIWebView: forceUIWebView, + client: client + ) { (result) in + switch result { + case .success(let user): + fulfill(user as! U) + case .failure(let error): + reject(error) + } + } micVC = UINavigationController(rootViewController: micLoginVC) } @@ -567,22 +794,15 @@ open class User: NSObject, Credential, Mappable { } viewController?.present(micVC, animated: true) }.then { user in - completionHandler?(user, nil) + completionHandler?(.success(user)) }.catch { error in - completionHandler?(nil, error) + completionHandler?(.failure(error)) } } #endif } -private struct UserError { - - let user: U? - let error: Swift.Error? - -} - public struct UserAuthToken : StaticMappable { var accessToken: String diff --git a/Kinvey/Kinvey/WriteOperation.swift b/Kinvey/Kinvey/WriteOperation.swift index f527aef63..dedddb77a 100644 --- a/Kinvey/Kinvey/WriteOperation.swift +++ b/Kinvey/Kinvey/WriteOperation.swift @@ -10,7 +10,7 @@ import Foundation internal class WriteOperation: Operation where T: NSObject { - typealias CompletionHandler = (R, Swift.Error?) -> Void + typealias CompletionHandler = (Result) -> Void let writePolicy: WritePolicy let sync: AnySync? @@ -21,6 +21,26 @@ internal class WriteOperation: Operation where T: NSObject super.init(cache: cache, client: client) } +} + +protocol WriteOperationType { + + associatedtype SuccessType + associatedtype FailureType + typealias CompletionHandler = (Result) -> Void + + var writePolicy: WritePolicy { get } + + @discardableResult + func executeLocal(_ completionHandler: CompletionHandler?) -> Request + + @discardableResult + func executeNetwork(_ completionHandler: CompletionHandler?) -> Request + +} + +extension WriteOperationType { + @discardableResult func execute(_ completionHandler: CompletionHandler?) -> Request { switch writePolicy { @@ -34,18 +54,4 @@ internal class WriteOperation: Operation where T: NSObject } } - @discardableResult - func executeLocal(_ completionHandler: CompletionHandler?) -> Request { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) - } - - @discardableResult - func executeNetwork(_ completionHandler: CompletionHandler?) -> Request { - let message = "Method \(#function) must be overridden" - log.severe(message) - fatalError(message) - } - } diff --git a/Kinvey/KinveyTests/AclTestCase.swift b/Kinvey/KinveyTests/AclTestCase.swift index c1d586444..3839116a3 100644 --- a/Kinvey/KinveyTests/AclTestCase.swift +++ b/Kinvey/KinveyTests/AclTestCase.swift @@ -145,7 +145,7 @@ class AclTestCase: StoreTestCase { store.push() { count, errors in self.assertThread() - XCTAssertEqual(count, 0) + XCTAssertNil(count) XCTAssertNotNil(errors) if let errors = errors { diff --git a/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift b/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift index a581546b3..a59b1edbb 100644 --- a/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift +++ b/Kinvey/KinveyTests/CacheMigrationTestCaseStep1.swift @@ -63,13 +63,13 @@ class CacheMigrationTestCaseStep1: XCTestCase { let store = DataStore.collection(.sync) - var person = Person() + let person = Person() person.firstName = "Victor" person.lastName = "Barros" weak var expectationSave = expectation(description: "Save") - store.save(&person) { (person, error) in + store.save(person) { (person, error) in XCTAssertNotNil(person) XCTAssertNil(error) diff --git a/Kinvey/KinveyTests/CacheStoreTests.swift b/Kinvey/KinveyTests/CacheStoreTests.swift index 7b0228e67..83761b494 100644 --- a/Kinvey/KinveyTests/CacheStoreTests.swift +++ b/Kinvey/KinveyTests/CacheStoreTests.swift @@ -98,7 +98,7 @@ class CacheStoreTests: StoreTestCase { store.find(byId: temporaryObjectId, readPolicy: .forceLocal) { (person, error) in XCTAssertNil(person) - XCTAssertNil(error) + XCTAssertNotNil(error) expectationFind?.fulfill() } diff --git a/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift b/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift index 9cd728f4f..3b652ffc1 100644 --- a/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift +++ b/Kinvey/KinveyTests/DeltaSetCacheTestCase.swift @@ -186,9 +186,13 @@ class DeltaSetCacheTestCase: KinveyTestCase { weak var expectationCreate = expectation(description: "Create") let createOperation = SaveOperation(persistable: person, writePolicy: .forceNetwork, client: client) - createOperation.execute { (results, error) -> Void in - XCTAssertNotNil(results) - XCTAssertNil(error) + createOperation.execute { result in + switch result { + case .success: + break + case .failure(let error): + XCTFail() + } expectationCreate?.fulfill() } @@ -372,9 +376,13 @@ class DeltaSetCacheTestCase: KinveyTestCase { weak var expectationUpdate = expectation(description: "Update") let updateOperation = SaveOperation(persistable: person, writePolicy: .forceNetwork, client: client) - updateOperation.execute { (results, error) -> Void in - XCTAssertNotNil(results) - XCTAssertNil(error) + updateOperation.execute { result in + switch result { + case .success: + break + case .failure: + XCTFail() + } expectationUpdate?.fulfill() } @@ -529,11 +537,13 @@ class DeltaSetCacheTestCase: KinveyTestCase { let query = Query(format: "personId == %@", personId) query.persistableType = Person.self let createRemove = RemoveByQueryOperation(query: query, writePolicy: .forceNetwork, client: client) - createRemove.execute { (count, error) -> Void in - XCTAssertNotNil(count) - XCTAssertNil(error) - - XCTAssertEqual(count, 1) + createRemove.execute { result in + switch result { + case .success(let count): + XCTAssertEqual(count, 1) + case .failure: + XCTFail() + } expectationDelete?.fulfill() } @@ -632,9 +642,13 @@ class DeltaSetCacheTestCase: KinveyTestCase { weak var expectationCreate = self.expectation(description: "Create") let createOperation = SaveOperation(persistable: person, writePolicy: .forceNetwork, client: self.client) - createOperation.execute { (results, error) -> Void in - XCTAssertNotNil(results) - XCTAssertNil(error) + createOperation.execute { result in + switch result { + case .success: + break + case .failure: + XCTFail() + } expectationCreate?.fulfill() } @@ -948,9 +962,13 @@ class DeltaSetCacheTestCase: KinveyTestCase { weak var expectationCreate = self.expectation(description: "Create") let createOperation = SaveOperation(persistable: person, writePolicy: .forceNetwork, client: self.client) - createOperation.execute { (results, error) -> Void in - XCTAssertNotNil(results) - XCTAssertNil(error) + createOperation.execute { result in + switch result { + case .success: + break + case .failure: + XCTFail() + } expectationCreate?.fulfill() } diff --git a/Kinvey/KinveyTests/FileTestCase.swift b/Kinvey/KinveyTests/FileTestCase.swift index 92f7e1e0a..34dfdb010 100644 --- a/Kinvey/KinveyTests/FileTestCase.swift +++ b/Kinvey/KinveyTests/FileTestCase.swift @@ -25,12 +25,15 @@ class FileTestCase: StoreTestCase { override func setUp() { super.setUp() - while !FileManager.default.fileExists(atPath: caminandes3TrailerURL.path) || !FileManager.default.fileExists(atPath: caminandes3TrailerImageURL.path) { - weak var expectationDownloadVideo = expectation(description: "Download Video") - weak var expectationDownloadImage = expectation(description: "Download Image") + var count = 0 + + while count < 10, !FileManager.default.fileExists(atPath: caminandes3TrailerURL.path) || !FileManager.default.fileExists(atPath: caminandes3TrailerImageURL.path) { + count += 1 + let downloadGroup = DispatchGroup() let url = URL(string: "https://www.youtube.com/get_video_info?video_id=6U1bsPCLLEg&el=info")! let request = URLRequest(url: url) + downloadGroup.enter() let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let responseBody = String(data: data, encoding: .utf8), @@ -41,6 +44,7 @@ class FileTestCase: StoreTestCase { let urlString = URLComponents(string: "parse://?\(urlEncodedFmtStreamMap)")?.queryItems?.filter({ return $0.name == "url" }).first?.value, let url = URL(string: urlString) { + downloadGroup.enter() let downloadTask = URLSession.shared.downloadTask(with: url) { url, response, error in if let url = url, let attrs = try? FileManager.default.attributesOfItem(atPath: url.path), @@ -50,11 +54,9 @@ class FileTestCase: StoreTestCase { try! FileManager.default.moveItem(at: url, to: self.caminandes3TrailerURL) } - expectationDownloadVideo?.fulfill() + downloadGroup.leave() } downloadTask.resume() - } else { - expectationDownloadVideo?.fulfill() } } @@ -62,6 +64,7 @@ class FileTestCase: StoreTestCase { if let iurlmaxres = queryItems.filter({ return $0.name == "iurlmaxres" }).first?.value, let url = URL(string: iurlmaxres) { + downloadGroup.enter() let downloadTask = URLSession.shared.downloadTask(with: url) { url, response, error in if let url = url, let attrs = try? FileManager.default.attributesOfItem(atPath: url.path), @@ -71,22 +74,20 @@ class FileTestCase: StoreTestCase { try! FileManager.default.moveItem(at: url, to: self.caminandes3TrailerImageURL) } - expectationDownloadImage?.fulfill() + downloadGroup.leave() } downloadTask.resume() - } else { - expectationDownloadImage?.fulfill() } } } + + downloadGroup.leave() } dataTask.resume() - waitForExpectations(timeout: defaultTimeout) { error in - expectationDownloadVideo = nil - expectationDownloadImage = nil - } + downloadGroup.wait() } + XCTAssertTrue(FileManager.default.fileExists(atPath: caminandes3TrailerURL.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: caminandes3TrailerImageURL.path)) } @@ -2143,8 +2144,15 @@ class FileTestCase: StoreTestCase { weak var expectationDestroy = expectation(description: "Destroy") - user.destroy() { error in - XCTAssertNil(error) + user.destroy() { + XCTAssertTrue(Thread.isMainThread) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroy?.fulfill() } @@ -2316,4 +2324,169 @@ class FileTestCase: StoreTestCase { } } + func testFind() { + signUp() + + let fileStore = FileStore.getInstance() + + if useMockData { + mockResponse(json: [ + [ + "_id" : UUID().uuidString, + "_acl" : [ + "gr" : true, + "creator" : UUID().uuidString + ], + "_filename" : "file.txt", + "_public" : true, + "mimeType" : "plain/txt", + "size" : 100, + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ], + "_downloadURL" : "https://storage.googleapis.com/file.txt" + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationFind = expectation(description: "Find") + + fileStore.find() { files, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(files) + XCTAssertNil(error) + + if let files = files { + XCTAssertEqual(files.count, 1) + } + + expectationFind?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationFind = nil + } + } + + func testRefresh() { + signUp() + + let fileStore = FileStore.getInstance() + var _file: File? = nil + + do { + if useMockData { + mockResponse(json: [ + [ + "_id" : UUID().uuidString, + "_filename" : "image.png", + "size" : 4096, + "mimeType" : "image/png", + "_acl" : [ + "gr" : true, + "creator" : UUID().uuidString + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ], + "_downloadURL" : "https://storage.googleapis.com/image.png", + "_expiresAt" : Date(timeIntervalSinceNow: 3).toString() + ] + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationFind = expectation(description: "Find") + + fileStore.find(ttl: (5, .second)) { files, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(files) + XCTAssertNil(error) + + if var files = files { + files = files.filter { $0.expiresAt != nil } + XCTAssertEqual(files.count, 1) + + if let file = files.first { + _file = file + let expiresInSeconds = file.expiresAt?.timeIntervalSinceNow + XCTAssertNotNil(expiresInSeconds) + if let expiresInSeconds = expiresInSeconds { + XCTAssertGreaterThan(expiresInSeconds, 0) + XCTAssertLessThanOrEqual(expiresInSeconds, 5) + } + } + } + + expectationFind?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationFind = nil + } + } + + XCTAssertNotNil(_file) + + if let file = _file { + if useMockData { + mockResponse(json: [ + "_id" : UUID().uuidString, + "_filename" : "image.png", + "size" : 4096, + "mimeType" : "image/png", + "_acl" : [ + "gr" : true, + "creator" : UUID().uuidString + ], + "_kmd" : [ + "lmt" : Date().toString(), + "ect" : Date().toString() + ], + "_downloadURL" : "https://storage.googleapis.com/image.png", + "_expiresAt" : Date(timeIntervalSinceNow: 3).toString() + ]) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationRefresh = expectation(description: "Refresh") + + fileStore.refresh(file, ttl: (5, .second)) { file, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(file) + XCTAssertNil(error) + + if let file = file { + let expiresInSeconds = file.expiresAt?.timeIntervalSinceNow + XCTAssertNotNil(expiresInSeconds) + if let expiresInSeconds = expiresInSeconds { + XCTAssertGreaterThan(expiresInSeconds, 0) + XCTAssertLessThanOrEqual(expiresInSeconds, 5) + } + } + + expectationRefresh?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationRefresh = nil + } + } + } + } diff --git a/Kinvey/KinveyTests/KinveyTestCase.swift b/Kinvey/KinveyTests/KinveyTestCase.swift index e6d7ce430..b31fcaf4d 100644 --- a/Kinvey/KinveyTests/KinveyTestCase.swift +++ b/Kinvey/KinveyTests/KinveyTestCase.swift @@ -48,6 +48,14 @@ struct HttpResponse { let statusCode: Int? let headerFields: [String : String]? let chunks: [ChunkData]? + let error: Swift.Error? + + init(error: Swift.Error) { + statusCode = nil + headerFields = nil + chunks = nil + self.error = error + } init(statusCode: Int? = nil, headerFields: [String : String]? = nil, chunks: [ChunkData]? = nil) { var headerFields = headerFields ?? [:] @@ -59,6 +67,7 @@ struct HttpResponse { self.statusCode = statusCode self.headerFields = headerFields self.chunks = chunks + error = nil } init(statusCode: Int? = nil, headerFields: [String : String]? = nil, data: Data? = nil) { @@ -175,17 +184,21 @@ extension XCTestCase { override func startLoading() { let responseObj = MockURLProtocol.completionHandler!(self.request) - let response = HTTPURLResponse(url: self.request.url!, statusCode: responseObj.statusCode ?? 200, httpVersion: "HTTP/1.1", headerFields: responseObj.headerFields) - self.client!.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed) - if let chunks = responseObj.chunks { - for chunk in chunks { - self.client!.urlProtocol(self, didLoad: chunk.data) - if let delay = chunk.delay { - RunLoop.current.run(until: Date(timeIntervalSinceNow: delay)) + if let error = responseObj.error { + self.client!.urlProtocol(self, didFailWithError: error) + } else { + let response = HTTPURLResponse(url: self.request.url!, statusCode: responseObj.statusCode ?? 200, httpVersion: "HTTP/1.1", headerFields: responseObj.headerFields) + self.client!.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed) + if let chunks = responseObj.chunks { + for chunk in chunks { + self.client!.urlProtocol(self, didLoad: chunk.data) + if let delay = chunk.delay { + RunLoop.current.run(until: Date(timeIntervalSinceNow: delay)) + } } } + self.client!.urlProtocolDidFinishLoading(self) } - self.client!.urlProtocolDidFinishLoading(self) } override func stopLoading() { @@ -447,9 +460,15 @@ class KinveyTestCase: XCTestCase { weak var expectationDestroyUser = expectation(description: "Destroy User") - user.destroy { (error) -> Void in + user.destroy { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroyUser?.fulfill() } diff --git a/Kinvey/KinveyTests/SyncStoreTests.swift b/Kinvey/KinveyTests/SyncStoreTests.swift index 8692564cf..f951ff1b8 100644 --- a/Kinvey/KinveyTests/SyncStoreTests.swift +++ b/Kinvey/KinveyTests/SyncStoreTests.swift @@ -198,8 +198,13 @@ class SyncStoreTests: StoreTestCase { XCTAssertNil(count) XCTAssertNotNil(error) - if let error = error as? NSError { - XCTAssertEqual(error, Kinvey.Error.invalidDataStoreType as NSError) + if let error = error as? Kinvey.Error { + switch error { + case .invalidDataStoreType: + break + default: + XCTFail() + } } expectationPurge?.fulfill() @@ -224,7 +229,7 @@ class SyncStoreTests: StoreTestCase { let query = Query(format: "acl.creator == %@", client.activeUser!.userId) store.purge(query) { (count, error) -> Void in - XCTAssertNotNil(count) + XCTAssertNil(count) XCTAssertNotNil(error) expectationPurge?.fulfill() @@ -338,7 +343,7 @@ class SyncStoreTests: StoreTestCase { store.sync() { count, results, error in self.assertThread() - XCTAssertEqual(count, 0) + XCTAssertNil(count) XCTAssertNil(results) XCTAssertNotNil(error) @@ -354,7 +359,8 @@ class SyncStoreTests: StoreTestCase { func testSyncNoCompletionHandler() { save() - let request = store.sync() + let request = store.sync { (_, _, _) in + } XCTAssertTrue(wait(toBeTrue: !request.executing)) } @@ -441,7 +447,8 @@ class SyncStoreTests: StoreTestCase { func testPushNoCompletionHandler() { save() - let request = store.push() + let request = store.push { (_, _) in + } XCTAssertTrue(wait(toBeTrue: !request.executing)) } diff --git a/Kinvey/KinveyTests/URLProtocols.swift b/Kinvey/KinveyTests/URLProtocols.swift index 010d6cf3c..d41182e14 100644 --- a/Kinvey/KinveyTests/URLProtocols.swift +++ b/Kinvey/KinveyTests/URLProtocols.swift @@ -8,6 +8,8 @@ import Foundation +let timeoutError = NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil) + class TimeoutErrorURLProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { diff --git a/Kinvey/KinveyTests/UserTests.swift b/Kinvey/KinveyTests/UserTests.swift index 6ad251cd8..d8aa15b7e 100644 --- a/Kinvey/KinveyTests/UserTests.swift +++ b/Kinvey/KinveyTests/UserTests.swift @@ -84,9 +84,15 @@ class UserTests: KinveyTestCase { if let user = client.activeUser { weak var expectationDestroyUser = expectation(description: "Destroy User") - user.destroy(client: client, completionHandler: { (error) -> Void in + user.destroy(client: client, completionHandler: { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroyUser?.fulfill() }) @@ -117,9 +123,16 @@ class UserTests: KinveyTestCase { userId = user.userId weak var expectationDestroyUser = expectation(description: "Destroy User") - user.destroy(hard: true, completionHandler: { (error) -> Void in + user.destroy(hard: true, completionHandler: { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } + expectationDestroyUser?.fulfill() }) @@ -171,9 +184,15 @@ class UserTests: KinveyTestCase { if let user = client.activeUser { weak var expectationDestroyUser = expectation(description: "Destroy User") - User.destroy(userId: user.userId, completionHandler: { (error) -> Void in + User.destroy(userId: user.userId, completionHandler: { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroyUser?.fulfill() }) @@ -201,9 +220,15 @@ class UserTests: KinveyTestCase { weak var expectationDestroyUser = expectation(description: "Destroy User") - User.destroy(userId: user.userId, hard: true, completionHandler: { (error) -> Void in + User.destroy(userId: user.userId, hard: true, completionHandler: { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroyUser?.fulfill() }) @@ -216,6 +241,42 @@ class UserTests: KinveyTestCase { } } + func testSignUpAndDestroyHardClassFuncTimeout() { + signUp() + + if let user = client.activeUser { + if useMockData { + setURLProtocol(TimeoutErrorURLProtocol.self) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationDestroyUser = expectation(description: "Destroy User") + + User.destroy(userId: user.userId, hard: true, completionHandler: { + XCTAssertTrue(Thread.isMainThread) + + switch $0 { + case .success: + XCTFail() + case .failure: + break + } + + expectationDestroyUser?.fulfill() + }) + + waitForExpectations(timeout: defaultTimeout) { error in + expectationDestroyUser = nil + } + + XCTAssertNotNil(client.activeUser) + } + } + func testSignUpAndDestroyClientClassFunc() { guard !useMockData else { return @@ -226,9 +287,16 @@ class UserTests: KinveyTestCase { if let user = client.activeUser { weak var expectationDestroyUser = expectation(description: "Destroy User") - User.destroy(userId: user.userId, client: client, completionHandler: { (error) -> Void in + User.destroy(userId: user.userId, client: client, completionHandler: { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroyUser?.fulfill() }) @@ -690,9 +758,15 @@ class UserTests: KinveyTestCase { if let activeUser = client.activeUser { weak var expectationDestroy = expectation(description: "Destroy") - activeUser.destroy { (error) -> Void in + activeUser.destroy { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroy?.fulfill() } @@ -757,9 +831,15 @@ class UserTests: KinveyTestCase { if let activeUser = client.activeUser { weak var expectationDestroy = expectation(description: "Destroy") - activeUser.destroy { (error) -> Void in + activeUser.destroy { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationDestroy?.fulfill() } @@ -887,9 +967,15 @@ class UserTests: KinveyTestCase { if let activeUser = client.activeUser { weak var expectationDestroy = expectation(description: "Destroy") - activeUser.destroy { (error) -> Void in + activeUser.destroy { XCTAssertTrue(Thread.isMainThread) - XCTAssertNotNil(error) + + switch $0 { + case .success: + XCTFail() + case .failure: + break + } expectationDestroy?.fulfill() } @@ -901,69 +987,59 @@ class UserTests: KinveyTestCase { } func testSendEmailConfirmation() { - guard !useMockData else { - return - } - - signUp() + signUp(username: UUID().uuidString) XCTAssertNotNil(client.activeUser) if let user = client.activeUser { - weak var expectationSave = expectation(description: "Save") - - user.email = "\(user.username!)@kinvey.com" - - user.save() { user, error in - XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) - XCTAssertNotNil(user) - - expectationSave?.fulfill() - } - - waitForExpectations(timeout: defaultTimeout) { error in - expectationSave = nil - } - - class Mock204URLProtocol: URLProtocol { - - override class func canInit(with request: URLRequest) -> Bool { - return true + do { + var json = user.toJSON() + json["email"] = "victor@kinvey.com" + mockResponse(json: json) + defer { + setURLProtocol(nil) } - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } + weak var expectationSave = expectation(description: "Save") - fileprivate override func startLoading() { - let response = HTTPURLResponse(url: request.url!, statusCode: 204, httpVersion: "HTTP/1.1", headerFields: [:])! - client!.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - client!.urlProtocol(self, didLoad: Data()) - client!.urlProtocolDidFinishLoading(self) + user.email = "victor@kinvey.com" + user.save() { user, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNotNil(user) + XCTAssertNil(error) + + expectationSave?.fulfill() } - fileprivate override func stopLoading() { + waitForExpectations(timeout: defaultTimeout) { error in + expectationSave = nil } - } - setURLProtocol(Mock204URLProtocol.self) - defer { - setURLProtocol(nil) - } - - weak var expectationSendEmailConfirmation = expectation(description: "Send Email Confirmation") - - user.sendEmailConfirmation { error in - XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + do { + mockResponse(statusCode: 204, data: Data()) + defer { + setURLProtocol(nil) + } - expectationSendEmailConfirmation?.fulfill() - } - - waitForExpectations(timeout: defaultTimeout) { error in - expectationSendEmailConfirmation = nil + weak var expectationSendEmailConfirmation = expectation(description: "Send Email Confirmation") + + user.sendEmailConfirmation { + XCTAssertTrue(Thread.isMainThread) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } + + expectationSendEmailConfirmation?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationSendEmailConfirmation = nil + } } } } @@ -1090,9 +1166,15 @@ class UserTests: KinveyTestCase { weak var expectationResetPassword = expectation(description: "Reset Password") - user.resetPassword { error in + user.resetPassword { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationResetPassword?.fulfill() } @@ -1115,9 +1197,15 @@ class UserTests: KinveyTestCase { if let user = client.activeUser { weak var expectationResetPassword = expectation(description: "Reset Password") - user.resetPassword { error in + user.resetPassword { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationResetPassword?.fulfill() } @@ -1156,9 +1244,15 @@ class UserTests: KinveyTestCase { weak var expectationResetPassword = expectation(description: "Reset Password") - user.resetPassword { error in + user.resetPassword { XCTAssertTrue(Thread.isMainThread) - XCTAssertNotNil(error) + + switch $0 { + case .success: + XCTFail() + case .failure: + break + } expectationResetPassword?.fulfill() } @@ -1197,9 +1291,15 @@ class UserTests: KinveyTestCase { weak var expectationResetPassword = expectation(description: "Reset Password") - user.resetPassword { error in + user.resetPassword { XCTAssertTrue(Thread.isMainThread) - XCTAssertNotNil(error) + + switch $0 { + case .success: + XCTFail() + case .failure: + break + } expectationResetPassword?.fulfill() } @@ -1217,9 +1317,15 @@ class UserTests: KinveyTestCase { weak var expectationForgotUsername = expectation(description: "Forgot Username") - User.forgotUsername(email: "\(UUID().uuidString)@kinvey.com") { error in + User.forgotUsername(email: "\(UUID().uuidString)@kinvey.com") { XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) + + switch $0 { + case .success: + break + case .failure: + XCTFail() + } expectationForgotUsername?.fulfill() } @@ -1234,9 +1340,15 @@ class UserTests: KinveyTestCase { weak var expectationForgotUsername = expectation(description: "Forgot Username") - User.forgotUsername(email: "\(UUID().uuidString)@kinvey.com") { error in + User.forgotUsername(email: "\(UUID().uuidString)@kinvey.com") { XCTAssertTrue(Thread.isMainThread) - XCTAssertNotNil(error) + + switch $0 { + case .success: + XCTFail() + case .failure: + break + } expectationForgotUsername?.fulfill() } @@ -1325,6 +1437,151 @@ class UserTests: KinveyTestCase { client.activeUser = nil } + func testFacebookLoginTimeout() { + setURLProtocol(TimeoutErrorURLProtocol.self) + defer { + setURLProtocol(nil) + } + + weak var expectationFacebookLogin = expectation(description: "Facebook Login") + + let fakeFacebookData = [ + "access_token": "AAAD30ogoDZCYBAKS50rOwCxMR7tIX8F90YDyC3vp63j0IvyCU0MELE2QMLnsWXKo2LcRgwA51hFr1UUpqXkSHu4lCj4VZCIuGG7DHZAHuZArzjvzTZAwQ", + "expires": "5105388" + ] + User.login(authSource: .facebook, fakeFacebookData) { user, error in + XCTAssertNil(user) + XCTAssertNotNil(error) + + if let error = error { + let error = error as NSError + XCTAssertEqual(error.domain, NSURLErrorDomain) + XCTAssertEqual(error.code, NSURLErrorTimedOut) + } + + expectationFacebookLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationFacebookLogin = nil + } + + client.activeUser = nil + } + + func testFacebookLoginWithoutClientInitialization() { + setURLProtocol(TimeoutErrorURLProtocol.self) + defer { + setURLProtocol(nil) + } + + let client = Client() + + weak var expectationFacebookLogin = expectation(description: "Facebook Login") + + let fakeFacebookData = [ + "access_token": "AAAD30ogoDZCYBAKS50rOwCxMR7tIX8F90YDyC3vp63j0IvyCU0MELE2QMLnsWXKo2LcRgwA51hFr1UUpqXkSHu4lCj4VZCIuGG7DHZAHuZArzjvzTZAwQ", + "expires": "5105388" + ] + User.login(authSource: .facebook, fakeFacebookData, client: client) { user, error in + XCTAssertNil(user) + XCTAssertNotNil(error) + + if let error = error { + XCTAssertTrue(error is Kinvey.Error) + + if let error = error as? Kinvey.Error { + switch error { + case .clientNotInitialized: + break + default: + XCTFail() + } + } + } + + expectationFacebookLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationFacebookLogin = nil + } + + client.activeUser = nil + } + + func testLoginWithUsernameAndPasswordTimeout() { + setURLProtocol(TimeoutErrorURLProtocol.self) + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + User.login( + username: UUID().uuidString, + password: UUID().uuidString + ) { user, error in + XCTAssertNil(user) + XCTAssertNotNil(error) + + if let error = error { + let error = error as NSError + XCTAssertEqual(error.domain, NSURLErrorDomain) + XCTAssertEqual(error.code, NSURLErrorTimedOut) + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationLogin = nil + } + + client.activeUser = nil + } + + func testLoginWithUsernameAndPasswordWithoutClientInitialization() { + setURLProtocol(TimeoutErrorURLProtocol.self) + defer { + setURLProtocol(nil) + } + + let client = Client() + + weak var expectationLogin = expectation(description: "Login") + + User.login( + username: UUID().uuidString, + password: UUID().uuidString, + client: client + ) { user, error in + XCTAssertNil(user) + XCTAssertNotNil(error) + + if let error = error { + XCTAssertTrue(error is Kinvey.Error) + + if let error = error as? Kinvey.Error { + switch error { + case .clientNotInitialized: + break + default: + XCTFail() + } + } + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationLogin = nil + } + + client.activeUser = nil + } + func testMICLoginWKWebView() { guard !useMockData else { return @@ -2086,5 +2343,254 @@ class UserTests: KinveyTestCase { } } } + + func testClientNotInitialized() { + let client = Client() + + weak var expectationSignUp = expectation(description: "Sign Up") + + User.signup(client: client) { (result: Result) in + XCTAssertTrue(Thread.isMainThread) + + switch result { + case .success: + XCTFail() + case .failure(let error): + XCTAssertTrue(error is Kinvey.Error) + if let error = error as? Kinvey.Error { + switch error { + case .clientNotInitialized: + XCTAssertEqual(error.description, "Client is not initialized. Please call the initialize() method to initialize the client and try again.") + break + default: + XCTFail() + } + } + } + + expectationSignUp?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationSignUp = nil + } + } + + func testMICParseCode() { + let redirectURI = URL(string: "myCustomURIScheme://")! + let url = URL(string: "myCustomURIScheme://?code_not_present=1234")! + let code = MIC.parseCode(redirectURI: redirectURI, url: url) + XCTAssertNil(code) + } + + func testMICLoginTimeout() { + setURLProtocol(TimeoutErrorURLProtocol.self) + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, code: "1234") { result in + XCTAssertTrue(Thread.isMainThread) + + switch result { + case .success: + XCTFail() + case .failure(let error): + let error = error as NSError + XCTAssertEqual(error.domain, NSURLErrorDomain) + XCTAssertEqual(error.code, NSURLErrorTimedOut) + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationLogin = nil + } + } + + func testMICLoginTimeoutDuringLoginCall() { + var count = 0 + mockResponse { (request) -> HttpResponse in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse( + statusCode: 200, + json: [ + "_socialIdentity" : [ + "kinveyAuth" : [ + "access_token" : UUID().uuidString, + "token_type" : "Bearer", + "expires_in" : 59, + "refresh_token" : UUID().uuidString + ] + ] + ] + ) + case 1: + return HttpResponse(error: timeoutError) + default: + XCTFail() + return HttpResponse(statusCode: 200, data: Data()) + } + } + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, code: "1234") { result in + XCTAssertTrue(Thread.isMainThread) + + switch result { + case .success: + XCTFail() + case .failure(let error): + let error = error as NSError + XCTAssertEqual(error.domain, NSURLErrorDomain) + XCTAssertEqual(error.code, NSURLErrorTimedOut) + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationLogin = nil + } + } + + func testMICLoginUsernamePasswordTimeout() { + setURLProtocol(TimeoutErrorURLProtocol.self) + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString) { result in + XCTAssertTrue(Thread.isMainThread) + + switch result { + case .success: + XCTFail() + case .failure(let error): + let error = error as NSError + XCTAssertEqual(error.domain, NSURLErrorDomain) + XCTAssertEqual(error.code, NSURLErrorTimedOut) + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationLogin = nil + } + } + + func testMICLoginUsernamePasswordTimeoutDuringTempLoginURI() { + var count = 0 + mockResponse { (request) -> HttpResponse in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse( + statusCode: 200, + json: [ + "temp_login_uri" : "https://auth.kinvey.com/oauth/authenticate/\(UUID().uuidString)" + ] + ) + case 1: + return HttpResponse(error: timeoutError) + default: + XCTFail() + return HttpResponse(statusCode: 200, data: Data()) + } + } + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString) { result in + XCTAssertTrue(Thread.isMainThread) + + switch result { + case .success: + XCTFail() + case .failure(let error): + let error = error as NSError + XCTAssertEqual(error.domain, NSURLErrorDomain) + XCTAssertEqual(error.code, NSURLErrorTimedOut) + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationLogin = nil + } + } + + func testMICLoginUsernamePasswordTimeoutDuringLoginCall() { + var count = 0 + mockResponse { (request) -> HttpResponse in + defer { + count += 1 + } + switch count { + case 0: + return HttpResponse( + statusCode: 200, + json: [ + "temp_login_uri" : "https://auth.kinvey.com/oauth/authenticate/\(UUID().uuidString)" + ] + ) + case 1: + return HttpResponse( + statusCode: 302, + headerFields: ["Location" : "myCustomURIScheme://?code=1234"], + data: Data() + ) + case 2: + return HttpResponse(error: timeoutError) + default: + XCTFail() + return HttpResponse(statusCode: 200, data: Data()) + } + } + defer { + setURLProtocol(nil) + } + + weak var expectationLogin = expectation(description: "Login") + + MIC.login(redirectURI: URL(string: "myCustomURIScheme://")!, username: UUID().uuidString, password: UUID().uuidString) { result in + XCTAssertTrue(Thread.isMainThread) + + switch result { + case .success: + XCTFail() + case .failure(let error): + let error = error as NSError + XCTAssertEqual(error.domain, NSURLErrorDomain) + XCTAssertEqual(error.code, NSURLErrorTimedOut) + } + + expectationLogin?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { (error) in + expectationLogin = nil + } + } } From 4c79d344dd3ca3b8cdc4fc6bbab122f9dbc94f1a Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Thu, 20 Apr 2017 16:59:59 -0700 Subject: [PATCH 05/11] MLIBZ-1788: user refresh Former-commit-id: bf379b70513b0d1ecc293c34d0831886af19fe97 --- Kinvey/Kinvey/User.swift | 22 ++++++++++++ Kinvey/KinveyTests/UserTests.swift | 57 ++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index e529fe12b..dfa336893 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -360,6 +360,28 @@ open class User: NSObject, Credential, Mappable { return request } + /// Gets a `User` instance using the `userId` property. + @discardableResult + open func refresh(completionHandler: VoidHandler? = nil) -> Request { + let request = client.networkRequestFactory.buildUserGet(userId: userId) + Promise { fulfill, reject in + request.execute() { (data, response, error) in + if let response = response, response.isOK, let json = self.client.responseParser.parse(data) { + let map = Map(mappingType: .fromJSON, JSON: json) + self.mapping(map: map) + fulfill() + } else { + reject(buildError(data, response, error, self.client)) + } + } + }.then { user in + completionHandler?(nil) + }.catch { error in + completionHandler?(error) + } + return request + } + /// Default Constructor. public init(userId: String? = nil, acl: Acl? = nil, metadata: UserMetadata? = nil, client: Client = Kinvey.sharedClient) { self._userId = userId diff --git a/Kinvey/KinveyTests/UserTests.swift b/Kinvey/KinveyTests/UserTests.swift index 6ad251cd8..21cdd6583 100644 --- a/Kinvey/KinveyTests/UserTests.swift +++ b/Kinvey/KinveyTests/UserTests.swift @@ -303,22 +303,29 @@ class UserTests: KinveyTestCase { } func testGet() { - guard !useMockData else { - return - } - signUp() + XCTAssertNotNil(client.activeUser) + if let user = client.activeUser { + if useMockData { + mockResponse(json: user.toJSON()) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + weak var expectationUserExists = expectation(description: "User Exists") - User.get(userId: user.userId, completionHandler: { (user, error) -> Void in + User.get(userId: user.userId) { user, error in XCTAssertTrue(Thread.isMainThread) XCTAssertNil(error) XCTAssertNotNil(user) expectationUserExists?.fulfill() - }) + } waitForExpectations(timeout: defaultTimeout) { error in expectationUserExists = nil @@ -348,6 +355,44 @@ class UserTests: KinveyTestCase { } } + func testRefresh() { + signUp() + + XCTAssertNotNil(client.activeUser) + + if let user = client.activeUser { + XCTAssertNil(user.email) + + let emailTest = "me@kinvey.com" + + if useMockData { + var json = user.toJSON() + json["email"] = emailTest + mockResponse(json: json) + } + defer { + if useMockData { + setURLProtocol(nil) + } + } + + weak var expectationRefresh = expectation(description: "Refresh") + + user.refresh() { error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertNil(error) + + XCTAssertEqual(user.email, emailTest) + + expectationRefresh?.fulfill() + } + + waitForExpectations(timeout: defaultTimeout) { error in + expectationRefresh = nil + } + } + } + func testLookup() { guard !useMockData else { return From 8b7cc6c3b4199106099ba3483d1813536b0d5fd4 Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Fri, 21 Apr 2017 10:29:27 -0700 Subject: [PATCH 06/11] Fixing the comment Former-commit-id: 17eaf10bfbb3404002d8e3bc4796252c58b1495d --- Kinvey/Kinvey/User.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index dfa336893..dfa23d158 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -360,7 +360,7 @@ open class User: NSObject, Credential, Mappable { return request } - /// Gets a `User` instance using the `userId` property. + /// Refresh the user's data. @discardableResult open func refresh(completionHandler: VoidHandler? = nil) -> Request { let request = client.networkRequestFactory.buildUserGet(userId: userId) From e34701092bc11bfbdd93ebb9b96af4a2e54261dd Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Fri, 21 Apr 2017 10:38:21 -0700 Subject: [PATCH 07/11] MLIBZ-1788: user refresh Former-commit-id: 963857b7b30425476241ce5f06e21242503a1045 --- Kinvey/Kinvey/User.swift | 1 + Kinvey/KinveyTests/UserTests.swift | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index dfa23d158..542519394 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -369,6 +369,7 @@ open class User: NSObject, Credential, Mappable { if let response = response, response.isOK, let json = self.client.responseParser.parse(data) { let map = Map(mappingType: .fromJSON, JSON: json) self.mapping(map: map) + self.client.activeUser = self fulfill() } else { reject(buildError(data, response, error, self.client)) diff --git a/Kinvey/KinveyTests/UserTests.swift b/Kinvey/KinveyTests/UserTests.swift index 21cdd6583..606766206 100644 --- a/Kinvey/KinveyTests/UserTests.swift +++ b/Kinvey/KinveyTests/UserTests.swift @@ -384,6 +384,13 @@ class UserTests: KinveyTestCase { XCTAssertEqual(user.email, emailTest) + let keychain = Keychain(appKey: self.client.appKey!, client: self.client) + let user = keychain.user + XCTAssertNotNil(user) + if let user = user { + XCTAssertEqual(user.email, emailTest) + } + expectationRefresh?.fulfill() } From 77657b9953e3ca311074ae5d7823fdc9836b3a1f Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Fri, 21 Apr 2017 11:30:26 -0700 Subject: [PATCH 08/11] MLIBZ-1788: user refresh mix with result enum Former-commit-id: 526fcd1209b09f6144b7e9bc0ea839ab9231313f --- Kinvey/Kinvey/User.swift | 6 +++--- Kinvey/KinveyTests/UserTests.swift | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index ca8c6d4d1..97fcefaac 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -497,7 +497,7 @@ open class User: NSObject, Credential, Mappable { /// Refresh the user's data. @discardableResult - open func refresh(completionHandler: VoidHandler? = nil) -> Request { + open func refresh(completionHandler: ((Result) -> Void)? = nil) -> Request { let request = client.networkRequestFactory.buildUserGet(userId: userId) Promise { fulfill, reject in request.execute() { (data, response, error) in @@ -511,9 +511,9 @@ open class User: NSObject, Credential, Mappable { } } }.then { user in - completionHandler?(nil) + completionHandler?(.success()) }.catch { error in - completionHandler?(error) + completionHandler?(.failure(error)) } return request } diff --git a/Kinvey/KinveyTests/UserTests.swift b/Kinvey/KinveyTests/UserTests.swift index 9a3a835eb..0cde74f1d 100644 --- a/Kinvey/KinveyTests/UserTests.swift +++ b/Kinvey/KinveyTests/UserTests.swift @@ -446,17 +446,21 @@ class UserTests: KinveyTestCase { weak var expectationRefresh = expectation(description: "Refresh") - user.refresh() { error in + user.refresh() { result in XCTAssertTrue(Thread.isMainThread) - XCTAssertNil(error) - - XCTAssertEqual(user.email, emailTest) - let keychain = Keychain(appKey: self.client.appKey!, client: self.client) - let user = keychain.user - XCTAssertNotNil(user) - if let user = user { + switch result { + case .success: XCTAssertEqual(user.email, emailTest) + + let keychain = Keychain(appKey: self.client.appKey!, client: self.client) + let user = keychain.user + XCTAssertNotNil(user) + if let user = user { + XCTAssertEqual(user.email, emailTest) + } + case .failure: + XCTFail() } expectationRefresh?.fulfill() From afe64ddcab2ab7c36fbe8cb66c08f6920f8c43a3 Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Fri, 21 Apr 2017 12:36:37 -0700 Subject: [PATCH 09/11] MLIBZ-1788: user refresh fix Former-commit-id: 0bb14cf2a0bb1ded14a89e9ced9343c466426664 --- Kinvey/Kinvey/User.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Kinvey/Kinvey/User.swift b/Kinvey/Kinvey/User.swift index 97fcefaac..4bbc922f4 100644 --- a/Kinvey/Kinvey/User.swift +++ b/Kinvey/Kinvey/User.swift @@ -504,7 +504,9 @@ open class User: NSObject, Credential, Mappable { if let response = response, response.isOK, let json = self.client.responseParser.parse(data) { let map = Map(mappingType: .fromJSON, JSON: json) self.mapping(map: map) - self.client.activeUser = self + if self == self.client.activeUser { + self.client.activeUser = self + } fulfill() } else { reject(buildError(data, response, error, self.client)) From c17330b71a2761d07f0149848b85af11a90bbfab Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Fri, 21 Apr 2017 13:04:47 -0700 Subject: [PATCH 10/11] Updating dependencies Former-commit-id: 5503af0498abf2a7d682138fe4fb3e17e6da4240 --- Cartfile.resolved | 4 ++-- Kinvey/Kinvey.xcodeproj/project.pbxproj | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Cartfile.resolved b/Cartfile.resolved index 5008ac5ed..d084c5ae7 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,7 @@ github "kif-framework/KIF" "v3.5.1" github "kishikawakatsumi/KeychainAccess" "v3.0.2" github "tjboneman/NSPredicate-MongoDB-Adaptor" "2444d4a790527eb5c9fcb4e4f7b4af417048ae18" -github "Hearst-DD/ObjectMapper" "2.2.5" +github "Hearst-DD/ObjectMapper" "2.2.6" github "mxcl/PromiseKit" "4.1.8" github "DaveWoodCom/XCGLogger" "4.0.0" -github "realm/realm-cocoa" "v2.5.1" +github "realm/realm-cocoa" "v2.6.2" diff --git a/Kinvey/Kinvey.xcodeproj/project.pbxproj b/Kinvey/Kinvey.xcodeproj/project.pbxproj index f5768f5cb..8881ac1be 100644 --- a/Kinvey/Kinvey.xcodeproj/project.pbxproj +++ b/Kinvey/Kinvey.xcodeproj/project.pbxproj @@ -559,7 +559,6 @@ 57136F621D5D23BF00731DDB /* MockKinveyBackend.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockKinveyBackend.swift; sourceTree = ""; }; 5714EBAF1CCECE35001E3ECF /* AclTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AclTestCase.swift; sourceTree = ""; }; 5714EBB11CCEEAF9001E3ECF /* RemoveByIdOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveByIdOperation.swift; sourceTree = ""; }; - 5719905A1CB301D500070CDA /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; 571991081CB45EEE00070CDA /* Person.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = ""; }; 572005C91D342B2800AE9AC5 /* Book.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Book.swift; sourceTree = ""; }; 5728212C1C63E0F500373EC8 /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; @@ -629,9 +628,7 @@ 5781D1301CE3ADBC00369F40 /* ErrorTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorTestCase.swift; sourceTree = ""; }; 5781D1351CE3D0BA00369F40 /* FileTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileTestCase.swift; sourceTree = ""; }; 5783B5061C1910B00077F8A6 /* JsonObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JsonObject.swift; sourceTree = ""; }; - 5783B6661C1A13CB0077F8A6 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 57873DEB1DFF3FDC002C87BF /* PushTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushTestCase.swift; sourceTree = ""; }; - 57873DED1DFFCEEF002C87BF /* CwlPreconditionTesting.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = CwlPreconditionTesting.framework; sourceTree = ""; }; 578870901DD52EC70087FE78 /* SSOApp1.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SSOApp1.app; sourceTree = BUILT_PRODUCTS_DIR; }; 578870921DD52EC80087FE78 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 578870941DD52EC80087FE78 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = ../Sources/ViewController.swift; sourceTree = ""; }; @@ -723,7 +720,6 @@ 57B768801D10C0C70086AA38 /* Entity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Entity.swift; sourceTree = ""; }; 57BB56B31C4D8D2B00F6B548 /* LocalRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalRequest.swift; sourceTree = ""; }; 57BB56B51C4D8E8400F6B548 /* LocalResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalResponse.swift; sourceTree = ""; }; - 57BB8EEF1DED5070000D8A69 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; 57BEAE2C1C98805600479206 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 57BEAE2E1C98805E00479206 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 57BEAE341C98AB3900479206 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; @@ -1202,7 +1198,6 @@ 57B0C1DA1CDCE88900492D6C /* Realm.framework */, 57B0C1DC1CDCE88900492D6C /* RealmSwift.framework */, 57B0C1D21CDCE88900492D6C /* KIF.framework */, - 57873DED1DFFCEEF002C87BF /* CwlPreconditionTesting.framework */, ); name = iOS; path = ../Carthage/Build/iOS; @@ -1274,12 +1269,9 @@ DBD4AA6B25714F7AAA213880 /* Frameworks */ = { isa = PBXGroup; children = ( - 57BB8EEF1DED5070000D8A69 /* libsqlite3.tbd */, 5795AB0F1DD3B894001FC808 /* SafariServices.framework */, - 5719905A1CB301D500070CDA /* Security.framework */, 57BEAE2E1C98805E00479206 /* QuartzCore.framework */, 57BEAE2C1C98805600479206 /* CoreGraphics.framework */, - 5783B6661C1A13CB0077F8A6 /* libsqlite3.tbd */, 57B0C1C11CDCE88900492D6C /* iOS */, 57B0C1DE1CDCE88900492D6C /* Mac */, 57B0C1EB1CDCE88900492D6C /* tvOS */, From 9eca2ca474608eb698d4515c28618cd76521542b Mon Sep 17 00:00:00 2001 From: Victor Barros Date: Fri, 21 Apr 2017 14:48:27 -0700 Subject: [PATCH 11/11] Bump version to 3.5.0 Former-commit-id: 333b7b2f2595fbe3ae6108a3fc4768567182e46b --- Kinvey.podspec | 2 +- Kinvey/Kinvey/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Kinvey.podspec b/Kinvey.podspec index f377e97ce..1afb892e5 100644 --- a/Kinvey.podspec +++ b/Kinvey.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "Kinvey" - s.version = "3.4.0" + s.version = "3.5.0" s.summary = "Kinvey iOS SDK" # This description is used to generate tags and improve search results. diff --git a/Kinvey/Kinvey/Info.plist b/Kinvey/Kinvey/Info.plist index a8f98d8fe..2bf762f69 100644 --- a/Kinvey/Kinvey/Info.plist +++ b/Kinvey/Kinvey/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.4.0 + 3.5.0 CFBundleSignature ???? CFBundleVersion