// The MIT License (MIT) | |
// | |
// Copyright (c) 2017 Alexander Grebenyuk (github.com/kean). | |
import Foundation | |
import Alamofire | |
import RxSwift | |
import RxCocoa | |
protocol ClientProtocol { | |
func request<Response>(_ endpoint: Endpoint<Response>) -> Single<Response> | |
} | |
final class Client: ClientProtocol { | |
private let manager: Alamofire.SessionManager | |
private let baseURL = URL(string: "<your_server_base_url>")! | |
private let queue = DispatchQueue(label: "<your_queue_label>") | |
init(accessToken: String) { | |
var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders | |
defaultHeaders["Authorization"] = "Bearer \(accessToken)" | |
let configuration = URLSessionConfiguration.default | |
// Add `Auth` header to the default HTTP headers set by `Alamofire` | |
configuration.httpAdditionalHeaders = defaultHeaders | |
self.manager = Alamofire.SessionManager(configuration: configuration) | |
self.manager.retrier = OAuth2Retrier() | |
} | |
func request<Response>(_ endpoint: Endpoint<Response>) -> Single<Response> { | |
return Single<Response>.create { observer in | |
let request = self.manager.request( | |
self.url(path: endpoint.path), | |
method: httpMethod(from: endpoint.method), | |
parameters: endpoint.parameters | |
) | |
request | |
.validate() | |
.responseData(queue: self.queue) { response in | |
let result = response.result.flatMap(endpoint.decode) | |
switch result { | |
case let .success(val): observer(.success(val)) | |
case let .failure(err): observer(.error(err)) | |
} | |
} | |
return Disposables.create { | |
request.cancel() | |
} | |
} | |
} | |
private func url(path: Path) -> URL { | |
return baseURL.appendingPathComponent(path) | |
} | |
} | |
private func httpMethod(from method: Method) -> Alamofire.HTTPMethod { | |
switch method { | |
case .get: return .get | |
case .post: return .post | |
case .put: return .put | |
case .patch: return .patch | |
case .delete: return .delete | |
} | |
} | |
private class OAuth2Retrier: Alamofire.RequestRetrier { | |
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { | |
if (error as? AFError)?.responseCode == 401 { | |
// TODO: implement your Auth2 refresh flow | |
// See https://github.com/Alamofire/Alamofire#adapting-and-retrying-requests | |
} | |
completion(false, 0) | |
} | |
} |
// The MIT License (MIT) | |
// | |
// Copyright (c) 2017 Alexander Grebenyuk (github.com/kean). | |
import Foundation | |
// MARK: Defines | |
typealias Parameters = [String: Any] | |
typealias Path = String | |
enum Method { | |
case get, post, put, patch, delete | |
} | |
// MARK: Endpoint | |
final class Endpoint<Response> { | |
let method: Method | |
let path: Path | |
let parameters: Parameters? | |
let decode: (Data) throws -> Response | |
init(method: Method = .get, | |
path: Path, | |
parameters: Parameters? = nil, | |
decode: @escaping (Data) throws -> Response) { | |
self.method = method | |
self.path = path | |
self.parameters = parameters | |
self.decode = decode | |
} | |
} | |
// MARK: Convenience | |
extension Endpoint where Response: Swift.Decodable { | |
convenience init(method: Method = .get, | |
path: Path, | |
parameters: Parameters? = nil) { | |
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) { | |
self.init( | |
method: method, | |
path: path, | |
parameters: parameters, | |
decode: { _ in () } | |
) | |
} | |
} |
// The MIT License (MIT) | |
// | |
// Copyright (c) 2017 Alexander Grebenyuk (github.com/kean). | |
import Foundation | |
// MARK: Defining Endpoints | |
enum API {} | |
extension API { | |
static func getCustomer() -> Endpoint<Customer> { | |
return Endpoint(path: "customer/profile") | |
} | |
static func patchCustomer(name: String) -> Endpoint<Customer> { | |
return Endpoint( | |
method: .patch, | |
path: "customer/profile", | |
parameters: ["name" : name] | |
) | |
} | |
} | |
final class Customer: Decodable { | |
let name: String | |
} | |
// MARK: Using Endpoints | |
func test() { | |
let client = Client(accessToken: "<access_token>") | |
_ = client.request(API.getCustomer()) | |
_ = client.request(API.patchCustomer(name: "Alex")) | |
} |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Could you create a sample project, It will much better to understand with a project. |
This comment has been minimized.
This comment has been minimized.
This is perfect, but... how to unit test your requests using mock responses? |
This comment has been minimized.
This comment has been minimized.
what about API error handling? i'm pretty new to decodable and i'd like to decode errors after validation. |
This comment has been minimized.
This comment has been minimized.
I really like this abstracion. I thought I'd share an enhancement that can be really useful: Implement map on Endpoint (make it a functor) like this: 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, decode: newDecodingFuntion)
} This enables transforming the initial expectation of the Endpoint (its Response generic parameter), in some cases you could reuse an endpoint already created. |
This comment has been minimized.
This comment has been minimized.
I am getting errors like
|
This comment has been minimized.
This comment has been minimized.
How would go about adding support for |
This comment has been minimized.
This comment has been minimized.
Great example! But how to return Response with arrays of codable items? :) |
This comment has been minimized.
This comment has been minimized.
Awesome! How would to add support for custom JSON initialiser? |
This comment has been minimized.
This comment has been minimized.
I can't understand how you can Chain two requests with this code. |
This comment has been minimized.
how do you get the results from the
client.request(API.getCustomers())