Skip to content

Instantly share code, notes, and snippets.

@khanlou
Last active April 25, 2022 13:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save khanlou/a6fdd28eac939d6634e0b72c2e539e39 to your computer and use it in GitHub Desktop.
Save khanlou/a6fdd28eac939d6634e0b72c2e539e39 to your computer and use it in GitHub Desktop.
import Foundation
public enum Method: String {
case GET
case POST
case PUT
case PATCH
case DELETE
}
public protocol HTTPRequest {
var method: Method { get }
var path: String { get }
var parameters: [String: Any] { get}
}
extension HTTPRequest {
public var method: Method {
return .GET
}
public var path: String {
return ""
}
public var parameters: [String: Any] {
return [:]
}
}
public protocol TypedRequest: HTTPRequest {
associatedtype OutputType: JSONThrowing
}
import Foundation
public protocol JSONInitializable {
init?(dictionary: [String: Any])
}
public struct StatusCodeError: Error {
let code: Int
let json: [String: Any]
var message: String {
return json["message"] as? String ?? "Something went wrong. Status code: \(code)"
}
}
public struct HTTPResponse<OutputType: JSONThrowing> {
public let data: Data
public let result: OutputType
public let httpResponse: HTTPURLResponse
public var statusCode: Int {
return httpResponse.statusCode
}
public init(data: Data, httpResponse: HTTPURLResponse) throws {
let untypedJSON = try JSONSerialization.jsonObject(with: data, options: [])
let json = try (untypedJSON as? [String: Any]).unwrap()
if !(200..<300).contains(httpResponse.statusCode) {
throw StatusCodeError(code: httpResponse.statusCode, json: json)
}
let result = try OutputType(parser: Parser(dictionary: json))
self.data = data
self.result = result
self.httpResponse = httpResponse
}
public func asAnyResponse() -> AnyResponse {
return AnyResponse(data: data, result: result, httpResponse: httpResponse)
}
}
public struct AnyResponse {
public let data: Data
public let result: Any
public let httpResponse: HTTPURLResponse
public var statusCode: Int {
return httpResponse.statusCode
}
}
import Foundation
public struct APIConstants {
static let qualifiedURL = "https://avail2.herokuapp.com/"
static let localURL = "http://localhost:8080/"
}
enum SharedNetworkClient {
static let main: NetworkClient = {
let behavior = CombinationRequestBehavior(behaviors: [
AuthTokenHeaderBehavior(),
JSONMIMETypesBehavior(),
JSONHeaderBehavior(),
APIVersionHeaderBehavior(),
])
let configuration = RequestConfiguration(baseURLString: APIConstants.qualifiedURL, defaultRequestBehavior: behavior)
return NetworkClient(configuration: configuration)
}()
}
public struct RequestConfiguration {
public let baseURLString: String
public let defaultRequestBehavior: RequestBehavior
public init(baseURLString: String, defaultRequestBehavior: RequestBehavior = EmptyRequestBehavior()) {
self.baseURLString = baseURLString
self.defaultRequestBehavior = defaultRequestBehavior
}
}
public class NetworkClient {
var session: URLSession
let configuration: RequestConfiguration
public init(session: URLSession = URLSession.shared, configuration: RequestConfiguration) {
self.session = session
self.configuration = configuration
}
@discardableResult
public func send<RequestType: TypedRequest>(request: RequestType, behavior: RequestBehavior = EmptyRequestBehavior()) -> Promise<RequestType.OutputType> {
let comboBehavior = CombinationRequestBehavior(behaviors: [behavior, configuration.defaultRequestBehavior])
let optionalURLRequest = RequestBuilder(request: request, baseURL: configuration.baseURLString, requestBehavior: comboBehavior).urlRequest
guard let urlRequest = optionalURLRequest else { return Promise<RequestType.OutputType>(error: NilError()) }
let sessionPromise = session.data(with: urlRequest)
.then { data, response in
return try HTTPResponse<RequestType.OutputType>(data: data, httpResponse: response)
}
return execute(requestBehavior: comboBehavior, on: sessionPromise).then({
return $0.result
})
}
private func execute<OutputType>(requestBehavior: RequestBehavior, on promise: Promise<HTTPResponse<OutputType>>) -> Promise<HTTPResponse<OutputType>> {
let anyResponsePromise = promise.then({ response in response.asAnyResponse() })
return requestBehavior
.handleRequest(promise: anyResponsePromise)
.then{ _ in return promise }
}
}
public protocol RequestBehavior {
var additionalHeaders: [String: String] { get }
func modify(request: URLRequest) -> URLRequest
func handleRequest(promise: Promise<AnyResponse>) -> Promise<AnyResponse>
}
extension RequestBehavior {
public var additionalHeaders: [String: String] {
return [:]
}
public func modify(request: URLRequest) -> URLRequest {
return request
}
public func handleRequest(promise: Promise<AnyResponse>) -> Promise<AnyResponse> {
return promise
}
}
struct CombinationRequestBehavior: RequestBehavior {
let behaviors: [RequestBehavior]
var additionalHeaders: [String : String] {
return behaviors.reduce([String: String](), { sum, behavior in
return sum.merged(with: behavior.additionalHeaders)
})
}
func modify(request: URLRequest) -> URLRequest {
return behaviors.reduce(request, { request, behavior in
return behavior.modify(request: request)
})
}
func handleRequest(promise: Promise<AnyResponse>) -> Promise<AnyResponse> {
return behaviors.reduce(promise, { promise, behavior in
return behavior.handleRequest(promise: promise)
})
}
}
struct EmptyRequestBehavior: RequestBehavior {
}
struct AuthTokenHeaderBehavior: RequestBehavior {
var additionalHeaders: [String: String] {
if let authToken = DataStorage.shared.currentUser?.authToken {
return [ "X-Auth-Token" : authToken ]
}
return [:]
}
}
struct JSONMIMETypesBehavior: RequestBehavior {
var additionalHeaders: [String: String] {
return [ "Content-Type" : "application/json" ]
}
}
struct JSONHeaderBehavior: RequestBehavior {
var additionalHeaders: [String: String] {
return [ "Accept" : "application/json" ]
}
}
struct APIVersionHeaderBehavior: RequestBehavior {
var additionalHeaders: [String: String] {
return [ "X-Beacon-Version" : "v4" ]
}
}
import Foundation
public class RequestBuilder {
let request: HTTPRequest
fileprivate let requestBehavior: RequestBehavior
var urlComponents: URLComponents?
public init(request: HTTPRequest, baseURL: String, requestBehavior: RequestBehavior = EmptyRequestBehavior()) {
self.request = request
self.requestBehavior = requestBehavior
self.urlComponents = URLComponents(string: baseURL + request.path)
}
var method: Method {
return request.method
}
var parameters: [String: Any] {
return request.parameters
}
public var urlRequest: Foundation.URLRequest? {
guard var localComponents = urlComponents, localComponents.host != nil else { return nil }
localComponents.queryItems = self.queryItems
guard let baseURL = localComponents.url else { return nil }
var request = URLRequest(url: baseURL)
request.httpMethod = method.rawValue
request.httpBody = body
for pair in requestBehavior.additionalHeaders {
request.addValue(pair.1, forHTTPHeaderField: pair.0)
}
return requestBehavior.modify(request: request)
}
var queryItems: [URLQueryItem]? {
guard method == .GET else {
return nil
}
let queryItems = (urlComponents?.queryItems ?? [])
guard queryItems.count + parameters.count != 0 else {
return nil
}
return queryItems + parameters.map({ key, value in URLQueryItem(name: key, value: "\(value)") })
}
var body: Data? {
guard method != .GET else {
return nil
}
return try? JSONSerialization.data(withJSONObject: parameters, options: [])
}
}
import Foundation
import Dispatch
extension URLSession {
func data(with request: URLRequest) -> Promise<(Data, HTTPURLResponse)> {
return Promise<(Data, HTTPURLResponse)>(work: { fulfill, reject in
self.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
reject(error)
} else if let data = data, let response = response as? HTTPURLResponse {
fulfill((data, response))
} else {
fatalError("Something has gone horribly wrong.")
}
}).resume()
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment