Forked from DanielCardonaRojas/APIClient+PromiseKit.swift
Created
April 30, 2020 23:32
-
-
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
This file contains hidden or 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 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) | |
}) | |
} | |
} | |
} |
This file contains hidden or 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 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() | |
} | |
}) | |
} | |
} |
This file contains hidden or 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 | |
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 | |
} | |
} |
This file contains hidden or 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
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 () }) | |
} | |
} |
This file contains hidden or 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
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