Skip to content

Instantly share code, notes, and snippets.

@ChrisXu
Last active April 25, 2018 07:16
Show Gist options
  • Save ChrisXu/1f0cf0b601c87dccf8d4c1a3cdef9407 to your computer and use it in GitHub Desktop.
Save ChrisXu/1f0cf0b601c87dccf8d4c1a3cdef9407 to your computer and use it in GitHub Desktop.
Generic Backend class
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