-
-
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