RxSwift/RxCocoa/Common/Observables/NSURLSession+Rx.swift

239 lines
7.9 KiB
Swift

//
// 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
: Swift.Error
, CustomDebugStringConvertible {
/**
Unknown error occurred.
*/
case unknown
/**
Response is not NSHTTPURLResponse
*/
case nonHTTPResponse(response: URLResponse)
/**
Response is not successful. (not in `200 ..< 300` range)
*/
case httpRequestFailed(response: HTTPURLResponse, data: Data?)
/**
Deserialization error.
*/
case deserializationError(error: Swift.Error)
}
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.replacingOccurrences(of: "\"", with: "\\\"", options:[], range: nil)
}
func convertURLRequestToCurlCommand(_ request: URLRequest) -> String {
let method = request.httpMethod ?? "GET"
var returnValue = "curl -X \(method) "
if request.httpMethod == "POST" && request.httpBody != nil {
let maybeBody = NSString(data: request.httpBody!, encoding: String.Encoding.utf8.rawValue) as? String
if let body = maybeBody {
returnValue += "-d \"\(escapeTerminalString(body))\" "
}
}
for (key, value) in request.allHTTPHeaderFields ?? [:] {
let escapedKey = escapeTerminalString((key as String) ?? "")
let escapedValue = escapeTerminalString((value as String) ?? "")
returnValue += "\n -H \"\(escapedKey): \(escapedValue)\" "
}
let URLString = request.url?.absoluteString ?? "<unknown url>"
returnValue += "\n\"\(escapeTerminalString(URLString))\""
returnValue += " -i -v"
return returnValue
}
func convertResponseToString(_ data: Data!, _ response: URLResponse!, _ error: NSError!, _ interval: TimeInterval) -> String {
let ms = Int(interval * 1000)
if let response = response as? HTTPURLResponse {
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 "<Unhandled response from server>"
}
extension URLSession {
/**
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: URLRequest) -> Observable<(Data, HTTPURLResponse)> {
return Observable.create { observer in
// smart compiler should be able to optimize this out
var d: Date?
if Logging.URLRequests(request) {
d = Date()
}
let task = self.dataTask(with: request) { (data, response, error) in
if Logging.URLRequests(request) {
let interval = Date().timeIntervalSince(d ?? Date())
print(convertURLRequestToCurlCommand(request))
print(convertResponseToString(data, response, error, interval))
}
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse 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: URLRequest) -> Observable<Data> {
return rx_response(request).map { (data, response) -> Data in
if 200 ..< 300 ~= response.statusCode {
return data
}
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: URLRequest) -> Observable<AnyObject> {
return rx_data(request).map { (data) -> AnyObject in
do {
return try JSONSerialization.jsonObject(with: data, 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: Foundation.URL) -> Observable<AnyObject> {
return rx_JSON(URLRequest(url: URL))
}
}