Last active
April 25, 2018 07:16
-
-
Save ChrisXu/1f0cf0b601c87dccf8d4c1a3cdef9407 to your computer and use it in GitHub Desktop.
Generic Backend class
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
import Foundation | |
public enum BackendError: Error { | |
case invalid(String) | |
case notFound(String) | |
} | |
public protocol Backend { | |
var baseURL: String { get } | |
var urlSession: URLSession { get } | |
@discardableResult | |
func perform(_ request: Request, completion: ((Result<Data>) -> Void)?) -> URLSessionTask? | |
} | |
public extension Backend { | |
@discardableResult | |
public func perform(_ request: Request, completion: ((Result<Data>) -> Void)? = nil) -> URLSessionTask? { | |
let urlString = baseURL + request.path | |
guard let url = URLComponents(string: urlString)?.url else { | |
completion?(Result.failure(BackendError.invalid("Invalid url \(urlString)"))) | |
return nil | |
} | |
var urlRequest = URLRequest(url: url) | |
urlRequest.httpMethod = request.httpMethod | |
urlRequest.httpBody = request.httpBody | |
// set custom headers if there is any | |
if let customHeaders = request.headers { | |
customHeaders.forEach { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) } | |
} | |
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in | |
do { | |
if let error = error { | |
throw error | |
} | |
guard let httpURLResponse = response as? HTTPURLResponse else { | |
throw BackendError.invalid("Invalid API response \(String(describing: response))") | |
} | |
guard httpURLResponse.isValidResponse else { | |
throw BackendError.invalid("Invalid statusCode: \(httpURLResponse.statusCode)") | |
} | |
guard let data = data else { | |
throw BackendError.notFound("Data can't be found") | |
} | |
completion?(.success(data)) | |
} catch { | |
completion?(.failure(error)) | |
} | |
} | |
task.resume() | |
return task | |
} | |
@discardableResult | |
public func perform<T>(_ request: Request, type: T.Type, keyPath: String? = nil, completion: ((Result<T>) -> Void)?) -> URLSessionTask? where T: Decodable { | |
let decoder = JSONDecoder() | |
return perform(request) { result in | |
do { | |
switch result { | |
case .success(let data): | |
let object: T | |
if let keyPath = keyPath { | |
let topLevelObject = try decoder.decode([String: T].self, from: data) | |
guard let value = topLevelObject[keyPath] else { | |
throw BackendError.notFound("Data can't be found at keyPath: \(keyPath)") | |
} | |
object = value | |
} else { | |
object = try decoder.decode(T.self, from: data) | |
} | |
completion?(Result.success(object)) | |
case .failure(let error): | |
throw error | |
} | |
} catch { | |
completion?(Result.failure(error)) | |
} | |
} | |
} | |
} | |
public enum Result<T> { | |
case success(T) | |
case failure(Error) | |
} | |
public struct Request { | |
public let httpMethod: String | |
public let path: String | |
public var headers: [String: String]? | |
public var httpBody: Data? | |
public init(httpMethod: String, path: String, headers: [String: String]? = nil, httpBody: Data? = nil) { | |
self.httpMethod = httpMethod | |
self.path = path | |
self.headers = headers | |
self.httpBody = httpBody | |
} | |
} | |
public extension HTTPURLResponse { | |
public var isValidResponse: Bool { | |
switch statusCode { | |
case 200 ... 299: | |
return true | |
default: | |
return false | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment