From 14a37f1c5f53b0d08db37b146f1b1a741a971441 Mon Sep 17 00:00:00 2001 From: dougzilla32 Date: Sun, 7 Oct 2018 15:21:26 +0900 Subject: [PATCH] 'Cancel' for PromiseKit -- provides the ability to cancel promises and promise chains --- Cartfile | 3 +- Cartfile.resolved | 4 +- Package.swift | 4 +- Sources/Alamofire+Promise.swift | 79 +++++++++++++++++++++++++++++---- Tests/TestAlamofire.swift | 72 ++++++++++++++++++++++++++++++ 5 files changed, 149 insertions(+), 13 deletions(-) diff --git a/Cartfile b/Cartfile index a5567cd..99f29cd 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,3 @@ -github "mxcl/PromiseKit" ~> 6.0 +#github "mxcl/PromiseKit" ~> 6.0 +github "dougzilla32/PromiseKit" "CoreCancel" github "Alamofire/Alamofire" ~> 4.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index b8ec46f..873caf2 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,3 @@ -github "Alamofire/Alamofire" "4.7.2" +github "Alamofire/Alamofire" "4.7.3" github "AliSoftware/OHHTTPStubs" "6.1.0" -github "mxcl/PromiseKit" "6.3.3" +github "dougzilla32/PromiseKit" "087b3cf470890ff9ea841212e2f3e285fecf3988" diff --git a/Package.swift b/Package.swift index 984e8c1..3b1fe22 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,9 @@ import PackageDescription let package = Package( name: "PMKAlamofire", dependencies: [ - .Package(url: "https://github.com/mxcl/PromiseKit.git", majorVersion: 4), +// Switch this back before integrating: +// .Package(url: "https://github.com/mxcl/PromiseKit.git", majorVersion: 4), + .Package(url: "https://github.com/dougzilla32/PromiseKitCoreCancel.git", majorVersion: 6), .Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4) ], exclude: ["Tests"] diff --git a/Sources/Alamofire+Promise.swift b/Sources/Alamofire+Promise.swift index f5f7b59..602ed64 100644 --- a/Sources/Alamofire+Promise.swift +++ b/Sources/Alamofire+Promise.swift @@ -4,6 +4,14 @@ import Foundation import PromiseKit #endif +/// Extend Request to be a CancellableTask +extension Request: CancellableTask { + /// `true` if the Alamofire request was successfully cancelled, `false` otherwise + public var isCancelled: Bool { + return task?.state == .canceling + } +} + /** To import the `Alamofire` category: @@ -17,7 +25,7 @@ import PromiseKit extension Alamofire.DataRequest { /// Adds a handler to be called once the request has finished. public func response(_: PMKNamespacer, queue: DispatchQueue? = nil) -> Promise<(URLRequest, HTTPURLResponse, Data)> { - return Promise { seal in + return Promise<(URLRequest, HTTPURLResponse, Data)>(cancellableTask: self) { seal in response(queue: queue) { rsp in if let error = rsp.error { seal.reject(error) @@ -32,7 +40,7 @@ extension Alamofire.DataRequest { /// Adds a handler to be called once the request has finished. public func responseData(queue: DispatchQueue? = nil) -> Promise<(data: Data, response: PMKAlamofireDataResponse)> { - return Promise { seal in + return Promise<(data: Data, response: PMKAlamofireDataResponse)>(cancellableTask: self) { seal in responseData(queue: queue) { response in switch response.result { case .success(let value): @@ -46,7 +54,7 @@ extension Alamofire.DataRequest { /// Adds a handler to be called once the request has finished. public func responseString(queue: DispatchQueue? = nil) -> Promise<(string: String, response: PMKAlamofireDataResponse)> { - return Promise { seal in + return Promise<(string: String, response: PMKAlamofireDataResponse)>(cancellableTask: self) { seal in responseString(queue: queue) { response in switch response.result { case .success(let value): @@ -60,7 +68,7 @@ extension Alamofire.DataRequest { /// Adds a handler to be called once the request has finished. public func responseJSON(queue: DispatchQueue? = nil, options: JSONSerialization.ReadingOptions = .allowFragments) -> Promise<(json: Any, response: PMKAlamofireDataResponse)> { - return Promise { seal in + return Promise<(json: Any, response: PMKAlamofireDataResponse)>(cancellableTask: self) { seal in responseJSON(queue: queue, options: options) { response in switch response.result { case .success(let value): @@ -74,7 +82,7 @@ extension Alamofire.DataRequest { /// Adds a handler to be called once the request has finished. public func responsePropertyList(queue: DispatchQueue? = nil, options: PropertyListSerialization.ReadOptions = PropertyListSerialization.ReadOptions()) -> Promise<(plist: Any, response: PMKAlamofireDataResponse)> { - return Promise { seal in + return Promise<(plist: Any, response: PMKAlamofireDataResponse)>(cancellableTask: self) { seal in responsePropertyList(queue: queue, options: options) { response in switch response.result { case .success(let value): @@ -95,7 +103,7 @@ extension Alamofire.DataRequest { - Parameter decoder: JSONDecoder, by default JSONDecoder() */ public func responseDecodable(queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> Promise { - return Promise { seal in + return Promise(cancellableTask: self) { seal in responseData(queue: queue) { response in switch response.result { case .success(let value): @@ -119,7 +127,7 @@ extension Alamofire.DataRequest { - Parameter decoder: JSONDecoder, by default JSONDecoder() */ public func responseDecodable(_ type: T.Type, queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> Promise { - return Promise { seal in + return Promise(cancellableTask: self) { seal in responseData(queue: queue) { response in switch response.result { case .success(let value): @@ -139,7 +147,7 @@ extension Alamofire.DataRequest { extension Alamofire.DownloadRequest { public func response(_: PMKNamespacer, queue: DispatchQueue? = nil) -> Promise { - return Promise { seal in + return Promise(cancellableTask: self) { seal in response(queue: queue) { response in if let error = response.error { seal.reject(error) @@ -152,7 +160,7 @@ extension Alamofire.DownloadRequest { /// Adds a handler to be called once the request has finished. public func responseData(queue: DispatchQueue? = nil) -> Promise> { - return Promise { seal in + return Promise>(cancellableTask: self) { seal in responseData(queue: queue) { response in switch response.result { case .success: @@ -187,3 +195,56 @@ public struct PMKAlamofireDataResponse { /// The timeline of the complete lifecycle of the request. public let timeline: Timeline } + +//////////////////////////////////////////////////////////// Cancellable wrappers + +extension Alamofire.DataRequest { + /// Wraps Alamofire.Response from Alamofire.response(queue:) as CancellablePromise<(Foundation.URLRequest, Foundation.HTTPURLResponse, Foundation.Data)> + public func cancellableResponse(_: PMKNamespacer, queue: DispatchQueue? = nil) -> CancellablePromise<(URLRequest, HTTPURLResponse, Data)> { + return cancellable(response(.promise, queue: queue)) + } + + /// Wraps Alamofire.DataResponse from Alamofire.responseData(queue:) as CancellablePromise<(Foundation.Data, PromiseKit.PMKAlamofireDataResponse)> + public func cancellableResponseData(queue: DispatchQueue? = nil) -> CancellablePromise<(data: Data, response: PMKAlamofireDataResponse)> { + return cancellable(responseData(queue: queue)) + } + + /// Wraps the response from Alamofire.responseString(queue:) as CancellablePromise<(String, PromiseKit.PMKAlamofireDataResponse)>. Uses the default encoding to decode the string data. + public func cancellableResponseString(queue: DispatchQueue? = nil) -> CancellablePromise<(string: String, response: PMKAlamofireDataResponse)> { + return cancellable(responseString(queue: queue)) + } + + /// Wraps the response from Alamofire.responseJSON(queue:options:) as CancellablePromise<(Any, PromiseKit.PMKAlamofireDataResponse)>. By default, the JSON decoder allows fragments, therefore 'Any' can be any standard JSON type (NSArray, NSDictionary, NSString, NSNumber, or NSNull). If the received JSON is not a fragment then 'Any' will be either an NSArray or NSDictionary. + public func cancellableResponseJSON(queue: DispatchQueue? = nil, options: JSONSerialization.ReadingOptions = .allowFragments) -> CancellablePromise<(json: Any, response: PMKAlamofireDataResponse)> { + return cancellable(responseJSON(queue: queue, options: options)) + } + + /// Wraps the response from Alamofire.responsePropertyList(queue:options:) as CancellablePromise<(Any, PromiseKit.PMKAlamofireDataResponse)>. Uses Foundation.PropertyListSerialization to deserialize the property list. 'Any' is an NSArray or NSDictionary containing only the types NSData, NSString, NSArray, NSDictionary, NSDate, and NSNumber. + public func cancellableResponsePropertyList(queue: DispatchQueue? = nil, options: PropertyListSerialization.ReadOptions = PropertyListSerialization.ReadOptions()) -> CancellablePromise<(plist: Any, response: PMKAlamofireDataResponse)> { + return cancellable(responsePropertyList(queue: queue, options: options)) + } + + #if swift(>=3.2) + /// Wraps the response from Alamofire.responseDecodable(queue:) as CancellablePromise. The Decodable is used to decode the incoming JSON data. + public func cancellableResponseDecodable(queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> CancellablePromise { + return cancellable(responseDecodable(queue: queue, decoder: decoder)) + } + + /// Wraps the response from Alamofire.responseDecodable() as CancellablePromise<(Decodable)>. The Decodable is used to decode the incoming JSON data. + public func cancellableResponseDecodable(_ type: T.Type, queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> CancellablePromise { + return cancellable(responseDecodable(type, queue: queue, decoder: decoder)) + } + #endif +} + +extension Alamofire.DownloadRequest { + /// Wraps Alamofire.Reponse.DefaultDownloadResponse from Alamofire.DownloadRequest.response(queue:) as CancellablePromise + public func cancellableResponse(_: PMKNamespacer, queue: DispatchQueue? = nil) -> CancellablePromise { + return cancellable(response(.promise, queue: queue)) + } + + /// Wraps Alamofire.Reponse.DownloadResponse from Alamofire.DownloadRequest.responseData(queue:) as CancellablePromise> + public func cancellableResponseData(queue: DispatchQueue? = nil) -> CancellablePromise> { + return cancellable(responseData(queue: queue)) + } +} diff --git a/Tests/TestAlamofire.swift b/Tests/TestAlamofire.swift index 043ee8a..f835976 100644 --- a/Tests/TestAlamofire.swift +++ b/Tests/TestAlamofire.swift @@ -72,3 +72,75 @@ class AlamofireTests: XCTestCase { } #endif } + +//////////////////////////////////////////////////////////// Cancellation + +extension AlamofireTests { + func testCancel() { + let json: NSDictionary = ["key1": "value1", "key2": ["value2A", "value2B"]] + + OHHTTPStubs.stubRequests(passingTest: { $0.url!.host == "example.com" }) { _ in + return OHHTTPStubsResponse(jsonObject: json, statusCode: 200, headers: nil) + } + + let ex = expectation(description: "") + + firstly { + cancellable(Alamofire.request("http://example.com", method: .get).responseJSON()) + }.done { _ in + XCTFail("failed to cancel request") + }.catch(policy: .allErrors) { error in + error.isCancelled ? ex.fulfill() : XCTFail("Error: \(error)") + }.cancel() + + waitForExpectations(timeout: 1) + } + +#if swift(>=3.2) + func testCancelDecodable1() { + + func getFixture() -> CancellablePromise { + return cancellable(Alamofire.request("http://example.com", method: .get).responseDecodable(queue: nil)) + } + + let json: NSDictionary = ["key1": "value1", "key2": ["value2A", "value2B"]] + + OHHTTPStubs.stubRequests(passingTest: { $0.url!.host == "example.com" }) { _ in + return OHHTTPStubsResponse(jsonObject: json, statusCode: 200, headers: nil) + } + + let ex = expectation(description: "") + + getFixture().done { fixture in + XCTAssert(fixture.key1 == "value1", "Value1 found") + XCTFail("failed to cancel request") + }.catch(policy: .allErrors) { error in + error.isCancelled ? ex.fulfill() : XCTFail("Error: \(error)") + }.cancel() + + waitForExpectations(timeout: 1) + } + + func testCancelDecodable2() { + let json: NSDictionary = ["key1": "value1", "key2": ["value2A", "value2B"]] + + OHHTTPStubs.stubRequests(passingTest: { $0.url!.host == "example.com" }) { _ in + return OHHTTPStubsResponse(jsonObject: json, statusCode: 200, headers: nil) + } + + let ex = expectation(description: "") + + firstly { + Alamofire.request("http://example.com", method: .get).cancellableResponseDecodable(Fixture.self) + }.done { fixture in + XCTAssert(fixture.key1 == "value1", "Value1 found") + XCTFail("failed to cancel request") + }.catch(policy: .allErrors) { error in + error.isCancelled ? ex.fulfill() : XCTFail("Error: \(error)") + }.cancel() + + waitForExpectations(timeout: 1) + + } + #endif +}