Last active
January 25, 2019 18:04
-
-
Save miguelfermin/505eaf55d81057c0fd12bc7ea4c6de57 to your computer and use it in GitHub Desktop.
Simple Networking Protocol Extension
This file contains hidden or 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
// | |
// Networker.swift | |
// SES Admin | |
// | |
// Created by Miguel Fermin on 8/03/18. | |
// Updated by Miguel Fermin on 8/22/18. | |
// Copyright © 2018 MAF Software LLC. All rights reserved. | |
// | |
import Foundation | |
let authRequiredNotification = NSNotification.Name("AuthenticationRequired") | |
protocol Networker { | |
func send<T: Decodable>(_ request: Request, completion: @escaping (Result<T>) ->Void) | |
} | |
extension Networker { | |
func send<T: Decodable>(_ request: Request, completion: @escaping (Result<T>) ->Void) { | |
request.printCurl() | |
let urlRequest = request.urlRequest | |
if let data = request.data, request.httpMethod != .get { | |
upload(request: urlRequest, data: data, completion: completion) | |
} else { | |
download(request: urlRequest, completion: completion) | |
} | |
} | |
} | |
private extension Networker { | |
func upload<T: Decodable>(request: URLRequest, data: Data, completion: @escaping (Result<T>) ->Void) { | |
let session = URLSessionProvider.shared.session | |
session.uploadTask(with: request, from: data) { (data, response, error) in | |
self.handle(data: data, response: response, error: error, completion: completion) | |
}.resume() | |
} | |
func download<T: Decodable>(request: URLRequest, completion: @escaping (Result<T>) ->Void) { | |
let session = URLSessionProvider.shared.session | |
session.dataTask(with: request){ (data, response, error) in | |
self.handle(data: data, response: response, error: error, completion: completion) | |
}.resume() | |
} | |
func handle<T: Decodable>(data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Result<T>) ->Void) { | |
if error != nil { | |
let err = APIError(code: .unknownError, text: error.debugDescription) | |
DispatchQueue.main.async { | |
completion(.failure(err)) | |
} | |
return | |
} | |
guard let statusCode = response?.code else { | |
let err = APIError(code: .unknownError, text: "No HTTPURLResponse") | |
DispatchQueue.main.async { | |
completion(.failure(err)) | |
} | |
return | |
} | |
if statusCode.rawValue == 401 { | |
DispatchQueue.main.async { | |
// NOTE: order of op matters, if flpped app crashes on fetch users | |
NotificationCenter.default.post(name: authRequiredNotification, object: nil) | |
completion(.failure(APIError(code: .unauthorized, text: "Unauthorized") )) | |
} | |
} | |
if (200...299).contains(statusCode.rawValue) == false { | |
let err = APIError(code: statusCode, text: "server error", data: data) | |
DispatchQueue.main.async { | |
completion(.failure(err)) | |
} | |
return | |
} | |
guard let payload = data else { | |
let err = APIError(code: .noPayload, text: "No Response Data", data: data) | |
DispatchQueue.main.async { | |
completion(.failure(err)) | |
} | |
return | |
} | |
do { | |
let decoder = JSONDecoder() | |
let result = try decoder.decode(T.self, from: payload) | |
DispatchQueue.main.async { | |
completion(.success(result)) | |
} | |
} catch { | |
let err = APIError(code: .decodeFailed, text: "Data decoder failed: \(error.localizedDescription)", data: data) | |
DispatchQueue.main.async { | |
completion(.failure(err)) | |
} | |
} | |
} | |
} | |
// MARK: - URLSessionProvider | |
class URLSessionProvider { | |
let sessionId: String | |
let session: URLSession | |
static let shared: URLSessionProvider = { | |
return URLSessionProvider() | |
}() | |
private init() { | |
sessionId = UUID().uuidString | |
session = URLSession(configuration: URLSessionConfiguration.default) | |
} | |
} | |
// MARK: - HttpMethod | |
enum HttpMethod { | |
case get | |
case post(Data) | |
case put(Data) | |
case delete(Data) | |
var string: String { | |
switch self { | |
case .get: return "GET" | |
case .post(_): return "POST" | |
case .put(_): return "PUT" | |
case .delete(_): return "DELETE" | |
} | |
} | |
} | |
// MARK: - AccessToken | |
struct AccessToken: Codable { | |
let id: String | |
let issued: String | |
let expires: String | |
} | |
// MARK: - APIError | |
struct APIError: Error { | |
let code: StatusCode | |
let text: String | |
let data: Data? | |
init(code: StatusCode, text: String, data: Data? = nil) { | |
self.code = code | |
self.text = text | |
self.data = data | |
} | |
} | |
// MARK: - Request | |
struct Request { | |
let url: URL | |
let httpMethod: HttpMethod | |
let accessToken: AccessToken? | |
let headers: [String: String]? | |
init(url: URL, httpMethod: HttpMethod = .get, accessToken: AccessToken? = nil, headers: [String: String]? = nil) { | |
self.url = url | |
self.httpMethod = httpMethod | |
self.accessToken = accessToken | |
self.headers = headers | |
} | |
var data: Data? { | |
switch httpMethod { | |
case .get: | |
return nil | |
case .post(let data), .put(let data), .delete(let data): | |
return data | |
} | |
} | |
var urlRequest: URLRequest { | |
var urlRequest = URLRequest(url: url) | |
urlRequest.httpMethod = httpMethod.string | |
if let headers = headers { | |
urlRequest.allHTTPHeaderFields = headers | |
} | |
else if let token = accessToken { | |
let headers = ["Content-Type": "application/json", "Authorization": "Token \(token.id)"] | |
urlRequest.allHTTPHeaderFields = headers | |
} | |
else { | |
urlRequest.allHTTPHeaderFields = ["Content-Type": "application/json"] | |
} | |
return urlRequest | |
} | |
func printCurl() { | |
#if DEBUG | |
guard let url = urlRequest.url?.absoluteString, let method = urlRequest.httpMethod else { | |
print("Request doesn't have URL nor HTTP Method to print cUrl") | |
return | |
} | |
var curl = "curl -X \(method) \\\n" | |
curl.append("\(url) \\\n") | |
if let headers = urlRequest.allHTTPHeaderFields { | |
for (key, val) in headers { | |
curl.append("-H '\(key): \(val)' \\\n") | |
} | |
} | |
if let data = data { | |
guard let dict = try? JSONSerialization.jsonObject(with: data, options: []), | |
let logData = try? JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted), | |
let body = String(data: logData, encoding: .utf8) else { | |
if let body = try? JSONSerialization.jsonObject(with: data, options: []) { | |
curl.append("-d '\(body)'") | |
} | |
print(curl) | |
return | |
} | |
curl.append("-d '\(body)'") | |
} | |
print(curl) | |
#endif | |
} | |
} | |
// MARK: - Response | |
struct Response<T: Decodable> { | |
let error: APIError? | |
let model: T | |
} | |
// MARK: - URLResponse+StatusCode | |
extension URLResponse { | |
var code: StatusCode { | |
guard let code = (self as? HTTPURLResponse)?.statusCode else { return .unknownError } | |
return StatusCode(rawValue: code) ?? .unknownError | |
} | |
} | |
// MARK: - Encodable+Helper | |
extension Encodable { | |
var encoded: Data? { return try? JSONEncoder().encode(self) } | |
} | |
// MARK: - StatusCode | |
public enum StatusCode: Int { | |
/// RFC 7231, 6.2.1 | |
case `continue` = 100 | |
/// RFC 7231, 6.2.2 | |
case switchingProtocols = 101 | |
/// RFC 2518, 10.1 | |
case processing = 102 | |
/// RFC 7231, 6.3.1 | |
case ok = 200 | |
/// RFC 7231, 6.3.2 | |
case created = 201 | |
/// RFC 7231, 6.3.3 | |
case accepted = 202 | |
/// RFC 7231, 6.3.4 | |
case nonAuthoritativeInfo = 203 | |
/// RFC 7231, 6.3.5 | |
case noContent = 204 | |
/// RFC 7231, 6.3.6 | |
case resetContent = 205 | |
/// RFC 7233, 4.1 | |
case partialContent = 206 | |
/// RFC 4918, 11.1 | |
case multiStatus = 207 | |
/// RFC 5842, 7.1 | |
case alreadyReported = 208 | |
/// RFC 3229, 10.4.1 | |
case IMUsed = 226 | |
/// RFC 7231, 6.4.1 | |
case multipleChoices = 300 | |
/// RFC 7231, 6.4.2 | |
case movedPermanently = 301 | |
/// RFC 7231, 6.4.3 | |
case found = 302 | |
/// RFC 7231, 6.4.4 | |
case seeOther = 303 | |
/// RFC 7232, 4.1 | |
case notModified = 304 | |
/// RFC 7231, 6.4.5 | |
case useProxy = 305 | |
/// RFC 7231, 6.4.7 | |
case temporaryRedirect = 307 | |
/// RFC 7538, 3 | |
case permanentRedirect = 308 | |
/// RFC 7231, 6.5.1 | |
case badRequest = 400 | |
/// RFC 7235, 3.1 | |
case unauthorized = 401 | |
/// RFC 7231, 6.5.2 | |
case paymentRequired = 402 | |
/// RFC 7231, 6.5.3 | |
case forbidden = 403 | |
/// RFC 7231, 6.5.4 | |
case notFound = 404 | |
/// RFC 7231, 6.5.5 | |
case methodNotAllowed = 405 | |
/// RFC 7231, 6.5.6 | |
case notAcceptable = 406 | |
/// RFC 7235, 3.2 | |
case proxyAuthRequired = 407 | |
/// RFC 7231, 6.5.7 | |
case requestTimeout = 408 | |
/// RFC 7231, 6.5.8 | |
case conflict = 409 | |
/// RFC 7231, 6.5.9 | |
case gone = 410 | |
/// RFC 7231, 6.5.10 | |
case lengthRequired = 411 | |
/// RFC 7232, 4.2 | |
case preconditionFailed = 412 | |
/// RFC 7231, 6.5.11 | |
case requestEntityTooLarge = 413 | |
/// RFC 7231, 6.5.12 | |
case requestURITooLong = 414 | |
/// RFC 7231, 6.5.13 | |
case unsupportedMediaType = 415 | |
/// RFC 7233, 4.4 | |
case requestedRangeNotSatisfiable = 416 | |
/// RFC 7231, 6.5.14 | |
case expectationFailed = 417 | |
/// RFC 7168, 2.3.3 | |
case teapot = 418 | |
/// RFC 4918, 11.2 | |
case unprocessableEntity = 422 | |
/// RFC 4918, 11.3 | |
case locked = 423 | |
/// RFC 4918, 11.4 | |
case failedDependency = 424 | |
/// RFC 7231, 6.5.15 | |
case upgradeRequired = 426 | |
/// RFC 6585, 3 | |
case preconditionRequired = 428 | |
/// RFC 6585, 4 | |
case tooManyRequests = 429 | |
/// RFC 6585, 5 | |
case requestHeaderFieldsTooLarge = 431 | |
/// RFC 7725, 3 | |
case unavailableForLegalReasons = 451 | |
/// RFC 7231, 6.6.1 | |
case internalServerError = 500 | |
/// RFC 7231, 6.6.2 | |
case notImplemented = 501 | |
/// RFC 7231, 6.6.3 | |
case badGateway = 502 | |
/// RFC 7231, 6.6.4 | |
case serviceUnavailable = 503 | |
/// RFC 7231, 6.6.5 | |
case gatewayTimeout = 504 | |
/// RFC 7231, 6.6.6 | |
case httpVersionNotSupported = 505 | |
/// RFC 2295, 8.1 | |
case variantAlsoNegotiates = 506 | |
/// RFC 4918, 11.5 | |
case insufficientStorage = 507 | |
/// RFC 5842, 7.2 | |
case loopDetected = 508 | |
/// RFC 2774, 7 | |
case notExtended = 510 | |
/// RFC 6585, 6 | |
case networkAuthenticationRequired = 511 | |
/// The 520 error is used as a "catch-all response for when the origin server returns something | |
/// unexpected", listing connection resets, large headers, and empty or invalid responses as | |
/// common triggers. | |
case unknownError = 520 | |
// MARK: Custom Error Codes | |
/// Expected response's body not received. | |
case noPayload = 600 | |
/// JSONDecoder failed to decode response body. | |
case decodeFailed = 601 | |
/// Access is limited to admins only | |
case adminsOnly = 602 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment