Last active
February 21, 2018 22:55
-
-
Save NSExceptional/09c59c5dccd3c3218c923cf079094416 to your computer and use it in GitHub Desktop.
A simple class to automate the parsing of an NSURLSessionTask response.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Copyright © 2016 Tanner Bennett. All rights reserved. | |
// | |
import Foundation | |
typealias ResponseParserBlock = (ResponseParser) -> Void | |
class ResponseParser { | |
// MARK: Response information | |
private(set) var response: HTTPURLResponse? | |
private(set) var data: Data | |
private(set) var error: Error? | |
var contentType: String? { | |
return self.response?.allHeaderFields[HTTPHeader.contentType] as! String? | |
} | |
// MARK: Response data helper accessors | |
private(set) var JSONDictionary: [String : Any]? | |
private(set) var JSONArray: [Any]? | |
private(set) var text: String? | |
var HTML: String? { | |
return hasHTML ? self.text : nil | |
} | |
var XML: String? { | |
return hasXML ? self.text : nil | |
} | |
var javascript: String? { | |
return hasJavascript ? self.text : nil | |
} | |
// MARK: Initializers, misc | |
convenience init(error: Error) { | |
self.init(data:nil, response: nil, error: error) | |
} | |
/// Use to conveniently call a callback closure on the main thread with a `ResponseParser`. | |
class func parse(_ response: HTTPURLResponse?, data: Data?, error: Error?, callback: @escaping ResponseParserBlock) { | |
DispatchQueue.main.async { | |
callback(ResponseParser(data: data, response: response, error: error)) | |
} | |
} | |
init(data: Data?, response: HTTPURLResponse?, error: Error?) { | |
self.data = data ?? Data() | |
self.response = response | |
self.error = error | |
if let contentType = self.contentType, !contentType.isEmpty, !self.data.isEmpty { | |
let hasJSON = contentType.hasPrefix(ContentType.JSON) | |
self.hasHTML = contentType.hasPrefix(ContentType.HTML) | |
self.hasXML = contentType.hasPrefix(ContentType.XML) | |
self.hasJavascript = contentType.hasPrefix(ContentType.javascript) | |
// You could add more types here as needed and the properties at the bottom | |
// Has some kind of text | |
if contentType.hasPrefix("text") || hasJSON || self.hasHTML || self.hasXML || self.hasJavascript { | |
self.text = String(data: self.data, encoding: .utf8) | |
} | |
if hasJSON { | |
try? self.forceDecodeJSON() | |
} | |
} | |
if error == nil, let code = self.response?.statusCode, code >= 400 { | |
self.error = ResponseParser.error(HTTPStatusCodeDescription(code), code: code) | |
} | |
} | |
/// Attempt to decode the response to JSON, regardless of the Content-Type. | |
/// | |
/// Useful if an API isn't using the right Content-Type. | |
func forceDecodeJSON() throws { | |
let json = try JSONSerialization.jsonObject(with: self.data, options: []) | |
if json is NSDictionary { | |
JSONDictionary = (json as! [String : Any]) | |
} else { | |
JSONArray = (json as! [Any]) | |
} | |
} | |
/// Convenience method to create an NSError | |
class func error(_ message: String, domain: String = "ResponseParser", code: Int) -> NSError { | |
return NSError(domain: domain, code: code, | |
userInfo: [NSLocalizedDescriptionKey: NSLocalizedString(message, comment: ""), | |
NSLocalizedFailureReasonErrorKey: NSLocalizedString(message, comment: "")]) | |
} | |
private var hasHTML = false | |
private var hasXML = false | |
private var hasJavascript = false | |
} | |
// MARK: Headers and content types | |
struct ContentType { | |
static let CSS = "text/css" | |
static let formURLEncoded = "application/x-www-form-urlencoded" | |
static let GZIP = "application/gzip" | |
static let HTML = "text/html" | |
static let javascript = "application/javascript" | |
static let JSON = "application/json" | |
static let JWT = "application/jwt" | |
static let markdown = "text/markdown" | |
static let multipartFormData = "multipart/form-data" | |
static let multipartEncrypted = "multipart/encrypted" | |
static let plainText = "text/plain" | |
static let rtf = "text/rtf" | |
static let textXML = "text/xml" | |
static let XML = "application/xml" | |
static let ZIP = "application/zip" | |
static let ZLIB = "application/zlib" | |
} | |
struct HTTPHeader { | |
static let accept = "Accept" | |
static let acceptEncoding = "Accept-Encoding" | |
static let acceptLanguage = "Accept-Language" | |
static let acceptLocale = "Accept-Locale" | |
static let authorization = "Authorization" | |
static let cacheControl = "Cache-Control" | |
static let contentLength = "Content-Length" | |
static let contentType = "Content-Type" | |
static let date = "Date" | |
static let expires = "Expires" | |
static let setCookie = "Set-Cookie" | |
static let status = "Status" | |
static let userAgent = "User-Agent" | |
} | |
// MARK: Status codes | |
enum HTTPStatusCode: Int { | |
/// Force unwraps code, you have been warned | |
init(_ code: Int) { | |
self.init(rawValue: code)! | |
} | |
case Continue = 100 | |
case SwitchProtocol | |
case OK = 200 | |
case Created | |
case Accepted | |
case NonAuthorativeInfo | |
case NoContent | |
case ResetContent | |
case PartialContent | |
case MultipleChoice = 300 | |
case MovedPermanently | |
case Found | |
case SeeOther | |
case NotModified | |
case UseProxy | |
case Unused | |
case TemporaryRedirect | |
case PermanentRedirect | |
case BadRequest = 400 | |
case Unauthorized | |
case PaymentRequired | |
case Forbidden | |
case NotFound | |
case MethodNotAllowed | |
case NotAcceptable | |
case ProxyAuthRequired | |
case RequestTimeout | |
case Conflict | |
case Gone | |
case LengthRequired | |
case PreconditionFailed | |
case PayloadTooLarge | |
case URITooLong | |
case UnsupportedMediaType | |
case RequestedRangeUnsatisfiable | |
case ExpectationFailed | |
case ImATeapot | |
case MisdirectedRequest = 421 | |
case UpgradeRequired = 426 | |
case PreconditionRequired = 428 | |
case TooManyRequests | |
case RequestHeaderFieldsTooLarge = 431 | |
case InternalServerError = 500 | |
case NotImplemented | |
case BadGateway | |
case ServiceUnavailable | |
case GatewayTimeout | |
case HTTPVersionUnsupported | |
case VariantAlsoNegotiates | |
case AuthenticationRequired = 511 | |
} | |
func HTTPStatusCodeDescription(_ code: Int) -> String { | |
guard let status = HTTPStatusCode(rawValue: code) else { | |
return "Unknown Error (code \(code)" | |
} | |
switch status { | |
case .Continue: | |
return "Continue" | |
case .SwitchProtocol: | |
return "Switch Protocol" | |
case .OK: | |
return "OK" | |
case .Created: | |
return "Created" | |
case .Accepted: | |
return "Accepted" | |
case .NonAuthorativeInfo: | |
return "Non Authorative Info" | |
case .NoContent: | |
return "No content" | |
case .ResetContent: | |
return "Reset Content" | |
case .PartialContent: | |
return "Partial Content" | |
case .MultipleChoice: | |
return "Multiple Choice" | |
case .MovedPermanently: | |
return "Moved Permanently" | |
case .Found: | |
return "Found" | |
case .SeeOther: | |
return "See Other" | |
case .NotModified: | |
return "Not Modified" | |
case .UseProxy: | |
return "Use Proxy" | |
case .Unused: | |
return "Unused" | |
case .TemporaryRedirect: | |
return "Temporary Redirect" | |
case .PermanentRedirect: | |
return "Permanent Redirect" | |
case .BadRequest: | |
return "Bad Request" | |
case .Unauthorized: | |
return "Unauthorized" | |
case .PaymentRequired: | |
return "" | |
case .Forbidden: | |
return "Forbidden" | |
case .NotFound: | |
return "Not Found" | |
case .MethodNotAllowed: | |
return "Method Not Allowed" | |
case .NotAcceptable: | |
return "Not Acceptable" | |
case .ProxyAuthRequired: | |
return "Proxy Authentication Required" | |
case .RequestTimeout: | |
return "Request Timeout" | |
case .Conflict: | |
return "Conflict" | |
case .Gone: | |
return "Gone" | |
case .LengthRequired: | |
return "Length Required" | |
case .PreconditionFailed: | |
return "Precondition Failed" | |
case .PayloadTooLarge: | |
return "Payload Too Large" | |
case .URITooLong: | |
return "URI Too Long" | |
case .UnsupportedMediaType: | |
return "Unsupported Media Type" | |
case .RequestedRangeUnsatisfiable: | |
return "Requested Range Unsatisfiable" | |
case .ExpectationFailed: | |
return "Expectation Failed" | |
case .ImATeapot: | |
return "???" | |
case .MisdirectedRequest: | |
return "Misdirected Request" | |
case .UpgradeRequired: | |
return "Upgrade Required" | |
case .PreconditionRequired: | |
return "Precondition Required" | |
case .TooManyRequests: | |
return "Too many requests" | |
case .RequestHeaderFieldsTooLarge: | |
return "Request Header Fields Too Large" | |
case .InternalServerError: | |
return "Internal Server Error" | |
case .NotImplemented: | |
return "Not Implemented" | |
case .BadGateway: | |
return "Bad gateway" | |
case .ServiceUnavailable: | |
return "Service Unavailable" | |
case .GatewayTimeout: | |
return "Gateway timeout" | |
case .HTTPVersionUnsupported: | |
return "HTTP Version Unsupported" | |
case .VariantAlsoNegotiates: | |
return "Variant Also Negotiates" | |
case .AuthenticationRequired: | |
return "Authentication Required" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment