- 
      
- 
        Save DanielCardonaRojas/2e0eccf1027d82c9f0273e6cd7b896d1 to your computer and use it in GitHub Desktop. 
| 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) | |
| } | 
Hi, this looks nice and I just stumbled upon the original kean article + saw your gist. Do you have a usage example? Thx
I create an API enum with endpoints
struct Todo: Codable {
    let title: String
    let completed: Bool
}
enum API {
    enum Todos {
        static func get() -> Endpoint<Todo> {
            return Endpoint<Todo>(method: .get, path: "/todos/1")
        }
    }
}then i execute the request passing in an Endpoint object
class ViewController: UIViewController {
    
    lazy var client: APIClient = {
        let configuration = URLSessionConfiguration.default
        let client = APIClient(baseURL: "https://jsonplaceholder.typicode.com", configuration: configuration)
        return client
    }()
    override func viewDidLoad() {
        super.viewDidLoad()
        let endpoint = API.Todos.get()
        
        client.request(endpoint, success: { item in
            print("\(item)")
        }, fail: { error in
            print("Error \(error.localizedDescription)")
        })
    }
}Hi,
I found this to be really helpful. The one thing I could not get my hands on when trying to avoid adding dependencies like Alamofire was the the request retrier and global assignment/refresh of token header in all requests with only NSURLSession.
Would you have some example or source for such use case?
Thanks @overlord21,
I'm actually not sure how to implement this but it does sound like a very interesting feature. I suppose one approach would be to use the facilities of either PromiseKit or RxSwift to chain a an additional request.
But I'm not sure how to go about the retrier. Doing a quick search I found this which could be useful:
https://medium.com/ios-os-x-development/retry-in-the-wild-8154042ae207
By the way, I've actually put all the code above under a small framework.
Added some extra enhancements to http://kean.github.io/post/api-client definition
For instance:
Endpoint is a functor