Skip to content

Instantly share code, notes, and snippets.

@miguelfermin
Last active January 25, 2019 18:04
Show Gist options
  • Save miguelfermin/505eaf55d81057c0fd12bc7ea4c6de57 to your computer and use it in GitHub Desktop.
Save miguelfermin/505eaf55d81057c0fd12bc7ea4c6de57 to your computer and use it in GitHub Desktop.
Simple Networking Protocol Extension
//
// 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