Skip to content

Commit

Permalink
'Cancel' for PromiseKit -- provides the ability to cancel promises an…
Browse files Browse the repository at this point in the history
…d promise chains
  • Loading branch information
dougzilla32 committed Oct 6, 2018
1 parent ba1b7d5 commit 8eb95fc
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 13 deletions.
3 changes: 2 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
github "mxcl/PromiseKit" ~> 6.0
#github "mxcl/PromiseKit" ~> 6.0
github "dougzilla32/PromiseKit" "CoreCancel"
github "Alamofire/Alamofire" ~> 4.0
4 changes: 2 additions & 2 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -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" "718e39fee8690f79a0688d1dc4ed1a01e97ab036"
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
79 changes: 70 additions & 9 deletions Sources/Alamofire+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -95,7 +103,7 @@ extension Alamofire.DataRequest {
- Parameter decoder: JSONDecoder, by default JSONDecoder()
*/
public func responseDecodable<T: Decodable>(queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> Promise<T> {
return Promise { seal in
return Promise<T>(cancellableTask: self) { seal in
responseData(queue: queue) { response in
switch response.result {
case .success(let value):
Expand All @@ -119,7 +127,7 @@ extension Alamofire.DataRequest {
- Parameter decoder: JSONDecoder, by default JSONDecoder()
*/
public func responseDecodable<T: Decodable>(_ type: T.Type, queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> Promise<T> {
return Promise { seal in
return Promise<T>(cancellableTask: self) { seal in
responseData(queue: queue) { response in
switch response.result {
case .success(let value):
Expand All @@ -139,7 +147,7 @@ extension Alamofire.DataRequest {

extension Alamofire.DownloadRequest {
public func response(_: PMKNamespacer, queue: DispatchQueue? = nil) -> Promise<DefaultDownloadResponse> {
return Promise { seal in
return Promise<DefaultDownloadResponse>(cancellableTask: self) { seal in
response(queue: queue) { response in
if let error = response.error {
seal.reject(error)
Expand All @@ -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<DownloadResponse<Data>> {
return Promise { seal in
return Promise<DownloadResponse<Data>>(cancellableTask: self) { seal in
responseData(queue: queue) { response in
switch response.result {
case .success:
Expand Down Expand Up @@ -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<Decodable>. The Decodable is used to decode the incoming JSON data.
public func cancellableResponseDecodable<T: Decodable>(queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> CancellablePromise<T> {
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<T: Decodable>(_ type: T.Type, queue: DispatchQueue? = nil, decoder: JSONDecoder = JSONDecoder()) -> CancellablePromise<T> {
return cancellable(responseDecodable(type, queue: queue, decoder: decoder))
}
#endif
}

extension Alamofire.DownloadRequest {
/// Wraps Alamofire.Reponse.DefaultDownloadResponse from Alamofire.DownloadRequest.response(queue:) as CancellablePromise<Alamofire.Reponse.DefaultDownloadResponse>
public func cancellableResponse(_: PMKNamespacer, queue: DispatchQueue? = nil) -> CancellablePromise<DefaultDownloadResponse> {
return cancellable(response(.promise, queue: queue))
}

/// Wraps Alamofire.Reponse.DownloadResponse<Data> from Alamofire.DownloadRequest.responseData(queue:) as CancellablePromise<Alamofire.Reponse.DownloadResponse<Data>>
public func cancellableResponseData(queue: DispatchQueue? = nil) -> CancellablePromise<DownloadResponse<Data>> {
return cancellable(responseData(queue: queue))
}
}
72 changes: 72 additions & 0 deletions Tests/TestAlamofire.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Fixture> {
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
}

0 comments on commit 8eb95fc

Please sign in to comment.