Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ruddfawcett/4e059755d682ccb0e28236279a77154f to your computer and use it in GitHub Desktop.
Save ruddfawcett/4e059755d682ccb0e28236279a77154f to your computer and use it in GitHub Desktop.
Custom API Client Abstractions inspired by http://kean.github.io/post/api-client but with no third party dependencies. As a lib over here -> https://github.com/DanielCardonaRojas/APIClient
import PromiseKit
extension APIClient {
func request<Response, T>(_ requestConvertible: T,
additionalHeaders headers: [String: String]? = nil,
additionalQuery queryParameters: [String: String]? = nil,
baseUrl: URL? = nil) -> Promise<T.Result>
where T: URLResponseCapable, T: URLRequestConvertible, T.Result == Response {
return Promise { seal in
self.request(requestConvertible, additionalHeaders: headers, additionalQuery: queryParameters, success: { response in
seal.fulfill(response)
}, fail: { error in
seal.reject(error)
})
}
}
}
import RxSwift
extension APIClient {
func request<Response, T>(_ requestConvertible: T,
additionalHeaders headers: [String: String]? = nil,
additionalQuery queryParameters: [String: String]? = nil,
baseUrl: URL? = nil) -> Observable<T.Result>
where T: URLResponseCapable, T: URLRequestConvertible, T.Result == Response {
return Observable.create({ observer in
let dataTask = self.request(requestConvertible, additionalHeaders: headers, additionalQuery: queryParameters, baseUrl: baseUrl, success: { response in
observer.onNext(response)
observer.onCompleted()
}, fail: {error in
observer.onError(error)
})
return Disposables.create {
dataTask?.cancel()
}
})
}
}
import Foundation
protocol URLRequestConvertible {
func asURLRequest(baseURL: URL) throws -> URLRequest
}
protocol URLResponseCapable {
associatedtype Result
func handle(data: Data) throws -> Result
}
class APIClient {
private var baseURL: URL?
lazy var session: URLSession = {
return URLSession(configuration: .default)
}()
init(baseURL: String, configuration: URLSessionConfiguration? = nil) {
if let config = configuration {
self.session = URLSession(configuration: config)
}
self.baseURL = URL(string: baseURL)
}
@discardableResult
func request<Response, T>(_ requestConvertible: T,
additionalHeaders headers: [String: String]? = nil,
additionalQuery queryParameters: [String: String]? = nil,
baseUrl: URL? = nil,
success: @escaping (Response) -> Void,
fail: @escaping (Error) -> Void) -> URLSessionDataTask?
where T: URLResponseCapable, T: URLRequestConvertible, T.Result == Response {
guard let base = baseUrl ?? self.baseURL else {
return nil
}
do {
var httpRequest = try requestConvertible.asURLRequest(baseURL: base)
let additionalQueryItems = queryParameters?.map({ (k, v) in URLQueryItem(name: k, value: v) }) ?? []
httpRequest.allHTTPHeaderFields = headers
httpRequest.addQueryItems(additionalQueryItems)
let task: URLSessionDataTask = session.dataTask(with: httpRequest) { (data: Data?, response: URLResponse?, error: Error?) in
if let data = data {
do {
let parsedResponse = try requestConvertible.handle(data: data)
success(parsedResponse)
} catch (let parsingError) {
fail(parsingError)
}
} else if let error = error {
fail(error)
}
}
task.resume()
return task
} catch(let encodingError) {
fail(encodingError)
}
return nil
}
}
extension URLRequest {
mutating func addQueryItems(_ items: [URLQueryItem]) {
guard let url = self.url, items.count > 0 else {
return
}
var cmps = URLComponents(string: url.absoluteString)
let currentItems = cmps?.queryItems ?? []
cmps?.queryItems = currentItems + items
self.url = cmps?.url
}
}
final class Endpoint<Response>: CustomStringConvertible, CustomDebugStringConvertible {
let method: Method
let path: Path
private (set) var parameters: MixedLocationParams = [:]
let decode: (Data) throws -> Response
let encoding: ParameterEncoding
var description: String {
return "Endpoint \(method.rawValue) \(path) expecting: \(Response.self)"
}
var debugDescription: String {
let params = parameters.map({ (k, v) in "\(k.rawValue): \(v)" }).joined(separator: "|")
return self.description + " \(params)"
}
init(method: Method = .get,
path: Path,
parameters: MixedLocationParams,
encoding: ParameterEncoding = .methodDependent,
decode: @escaping (Data) throws -> Response) {
self.method = method
self.path = path.hasPrefix("/") ? path : "/" + path
self.parameters = parameters
self.decode = decode
self.encoding = encoding
}
init(method: Method = .get,
path: Path,
parameters: Parameters? = nil,
encoding: ParameterEncoding = .methodDependent,
decode: @escaping (Data) throws -> Response) {
self.method = method
self.path = path.hasPrefix("/") ? path : "/" + path
self.decode = decode
self.encoding = encoding
if let params = parameters {
self.addParameters(params)
}
}
func addParameters(_ params: Parameters, location: ParameterEncoding.Location? = nil) {
let loc = location ?? ParameterEncoding.Location.defaultLocation(for: self.method)
if let currentParams = parameters[loc] {
let updated = currentParams.merging(params, uniquingKeysWith: { (k1, k2) in k1 })
self.parameters[loc] = updated
} else {
self.parameters[loc] = params
}
}
func map<N>(_ f: @escaping ((Response) throws -> N)) -> Endpoint<N> {
let newDecodingFuntion: (Data) throws -> N = { data in
return try f(self.decode(data))
}
return Endpoint<N>(method: self.method, path: self.path, parameters: self.parameters, encoding: self.encoding, decode: newDecodingFuntion)
}
}
// MARK: - URLRequestConvertible
extension Endpoint: URLResponseCapable {
typealias Result = Response
func handle(data: Data) throws -> Response {
return try self.decode(data)
}
}
extension Endpoint: URLRequestConvertible {
func asURLRequest(baseURL: URL) throws -> URLRequest {
var urlComponents = URLComponents(string: baseURL.absoluteString)
let path = urlComponents.map { $0.path + self.path } ?? self.path
urlComponents?.path = path
let bodyEncoding = encoding.bodyEncoding
let bodyParameters = parameters[.httpBody]
let queryParameters = parameters[.queryString]
if let queryParams = queryParameters as? [String: String] {
let queryItems = queryParams.map({ (k, v) in URLQueryItem(name: k, value: v) })
urlComponents?.queryItems = queryItems
}
var request = URLRequest(url: urlComponents!.url!)
request.httpMethod = method.rawValue
if let contentType = bodyEncoding.contentType {
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
}
if let params = bodyParameters, bodyEncoding == .jsonEncoded {
let data = try JSONSerialization.data(withJSONObject: params as Any, options: [])
request.httpBody = data
} else if let params = bodyParameters as? [String: String], bodyEncoding == .formUrlEncoded {
let formUrlData: String? = params.map { (k, v) in
let escapedKey = k.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? k
let escapedValue = v.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? v
return "\(escapedKey)=\(escapedValue)"
}.joined(separator: "&")
request.httpBody = formUrlData?.data(using: .utf8)
}
return request
}
}
// MARK: - Conviniences
extension Endpoint where Response: Swift.Decodable {
convenience init(method: Method = .get, path: Path, parameters: Parameters? = nil, encoding: ParameterEncoding = .methodDependent) {
self.init(method: method, path: path, parameters: parameters) {
try JSONDecoder().decode(Response.self, from: $0)
}
}
}
extension Endpoint where Response == Void {
convenience init(method: Method = .get, path: Path, parameters: Parameters? = nil, encoding: ParameterEncoding = .methodDependent) {
self.init( method: method, path: path, parameters: parameters, decode: { _ in () })
}
}
typealias Parameters = [String: Any]
typealias MixedLocationParams = [ParameterEncoding.Location: Parameters]
typealias Path = String
enum Method: String {
case get = "GET", post = "POST", put = "PUT", patch = "PATCH", delete = "DELETE"
}
struct ParameterEncoding {
enum Location: String {
case queryString, httpBody
static func defaultLocation(for method: Method) -> Location {
switch method {
case .get:
return .queryString
default:
return .httpBody
}
}
}
enum BodyEncoding {
case formUrlEncoded, jsonEncoded
var contentType: String? {
switch self {
case .formUrlEncoded:
return "application/x-www-form-urlencoded; charset=utf-8"
case .jsonEncoded:
return "application/json; charset=UTF-8"
}
}
}
let location: Location?
let bodyEncoding: BodyEncoding
init(preferredBodyEncoding: BodyEncoding = .jsonEncoded, location: Location? = nil) {
self.location = location
self.bodyEncoding = preferredBodyEncoding
}
static func preferredBodyEncoding(_ encoding: BodyEncoding) -> ParameterEncoding {
return ParameterEncoding(preferredBodyEncoding: encoding, location: nil)
}
static let methodDependent = ParameterEncoding(preferredBodyEncoding: .jsonEncoded, location: nil)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment