// // NSURLSession+Rx.swift // RxCocoa // // Created by Krunoslav Zaher on 3/23/15. // Copyright © 2015 Krunoslav Zaher. All rights reserved. // import Foundation #if !RX_NO_MODULE import RxSwift #endif /** RxCocoa URL errors. */ public enum RxCocoaURLError : ErrorType , CustomDebugStringConvertible { /** Unknown error occurred. */ case Unknown /** Response is not NSHTTPURLResponse */ case NonHTTPResponse(response: NSURLResponse) /** Response is not successful. (not in `200 ..< 300` range) */ case HTTPRequestFailed(response: NSHTTPURLResponse, data: NSData?) /** Deserialization error. */ case DeserializationError(error: ErrorType) } public extension RxCocoaURLError { /** A textual representation of `self`, suitable for debugging. */ public var debugDescription: String { switch self { case .Unknown: return "Unknown error has occurred." case let .NonHTTPResponse(response): return "Response is not NSHTTPURLResponse `\(response)`." case let .HTTPRequestFailed(response, _): return "HTTP request failed with `\(response.statusCode)`." case let .DeserializationError(error): return "Error during deserialization of the response: \(error)" } } } func escapeTerminalString(value: String) -> String { return value.stringByReplacingOccurrencesOfString("\"", withString: "\\\"", options:[], range: nil) } func convertURLRequestToCurlCommand(request: NSURLRequest) -> String { let method = request.HTTPMethod ?? "GET" var returnValue = "curl -i -v -X \(method) " if request.HTTPMethod == "POST" && request.HTTPBody != nil { let maybeBody = NSString(data: request.HTTPBody!, encoding: NSUTF8StringEncoding) as? String if let body = maybeBody { returnValue += "-d \"\(body)\"" } } for (key, value) in request.allHTTPHeaderFields ?? [:] { let escapedKey = escapeTerminalString((key as String) ?? "") let escapedValue = escapeTerminalString((value as String) ?? "") returnValue += "-H \"\(escapedKey): \(escapedValue)\" " } let URLString = request.URL?.absoluteString ?? "" returnValue += "\"\(escapeTerminalString(URLString))\"" return returnValue } func convertResponseToString(data: NSData!, _ response: NSURLResponse!, _ error: NSError!, _ interval: NSTimeInterval) -> String { let ms = Int(interval * 1000) if let response = response as? NSHTTPURLResponse { if 200 ..< 300 ~= response.statusCode { return "Success (\(ms)ms): Status \(response.statusCode)" } else { return "Failure (\(ms)ms): Status \(response.statusCode)" } } if let error = error { if error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled { return "Cancelled (\(ms)ms)" } return "Failure (\(ms)ms): NSError > \(error)" } return "" } extension NSURLSession { /** Observable sequence of responses for URL request. Performing of request starts after observer is subscribed and not after invoking this method. **URL requests will be performed per subscribed observer.** Any error during fetching of the response will cause observed sequence to terminate with error. - parameter request: URL request. - returns: Observable sequence of URL responses. */ @warn_unused_result(message="http://git.io/rxs.uo") public func rx_response(request: NSURLRequest) -> Observable<(NSData, NSHTTPURLResponse)> { return Observable.create { observer in // smart compiler should be able to optimize this out var d: NSDate? if Logging.URLRequests(request) { d = NSDate() } let task = self.dataTaskWithRequest(request) { (data, response, error) in if Logging.URLRequests(request) { let interval = NSDate().timeIntervalSinceDate(d ?? NSDate()) print(convertURLRequestToCurlCommand(request)) print(convertResponseToString(data, response, error, interval)) } guard let response = response, data = data else { observer.on(.Error(error ?? RxCocoaURLError.Unknown)) return } guard let httpResponse = response as? NSHTTPURLResponse else { observer.on(.Error(RxCocoaURLError.NonHTTPResponse(response: response))) return } observer.on(.Next(data, httpResponse)) observer.on(.Completed) } let t = task t.resume() return AnonymousDisposable { task.cancel() } } } /** Observable sequence of response data for URL request. Performing of request starts after observer is subscribed and not after invoking this method. **URL requests will be performed per subscribed observer.** Any error during fetching of the response will cause observed sequence to terminate with error. If response is not HTTP response with status code in the range of `200 ..< 300`, sequence will terminate with `(RxCocoaErrorDomain, RxCocoaError.NetworkError)`. - parameter request: URL request. - returns: Observable sequence of response data. */ @warn_unused_result(message="http://git.io/rxs.uo") public func rx_data(request: NSURLRequest) -> Observable { return rx_response(request).map { (data, response) -> NSData in if 200 ..< 300 ~= response.statusCode { return data ?? NSData() } else { throw RxCocoaURLError.HTTPRequestFailed(response: response, data: data) } } } /** Observable sequence of response JSON for URL request. Performing of request starts after observer is subscribed and not after invoking this method. **URL requests will be performed per subscribed observer.** Any error during fetching of the response will cause observed sequence to terminate with error. If response is not HTTP response with status code in the range of `200 ..< 300`, sequence will terminate with `(RxCocoaErrorDomain, RxCocoaError.NetworkError)`. If there is an error during JSON deserialization observable sequence will fail with that error. - parameter request: URL request. - returns: Observable sequence of response JSON. */ @warn_unused_result(message="http://git.io/rxs.uo") public func rx_JSON(request: NSURLRequest) -> Observable { return rx_data(request).map { (data) -> AnyObject in do { return try NSJSONSerialization.JSONObjectWithData(data ?? NSData(), options: []) } catch let error { throw RxCocoaURLError.DeserializationError(error: error) } } } /** Observable sequence of response JSON for GET request with `URL`. Performing of request starts after observer is subscribed and not after invoking this method. **URL requests will be performed per subscribed observer.** Any error during fetching of the response will cause observed sequence to terminate with error. If response is not HTTP response with status code in the range of `200 ..< 300`, sequence will terminate with `(RxCocoaErrorDomain, RxCocoaError.NetworkError)`. If there is an error during JSON deserialization observable sequence will fail with that error. - parameter URL: URL of `NSURLRequest` request. - returns: Observable sequence of response JSON. */ @warn_unused_result(message="http://git.io/rxs.uo") public func rx_JSON(URL: NSURL) -> Observable { return rx_JSON(NSURLRequest(URL: URL)) } }