Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
// 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"))
}
@eli007s

This comment has been minimized.

Copy link

eli007s commented Dec 11, 2017

how do you get the results from the client.request(API.getCustomers())

@anilabsinc-ajay

This comment has been minimized.

Copy link

anilabsinc-ajay commented Jan 5, 2018

Could you create a sample project, It will much better to understand with a project.

@pzmudzinski

This comment has been minimized.

Copy link

pzmudzinski commented Apr 9, 2018

This is perfect, but... how to unit test your requests using mock responses? 👍

@olivierto

This comment has been minimized.

Copy link

olivierto commented Apr 12, 2018

what about API error handling? i'm pretty new to decodable and i'd like to decode errors after validation.
Any help would be much appreciated.

@DanielCardonaRojas

This comment has been minimized.

Copy link

DanielCardonaRojas commented Nov 29, 2018

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.

@hiteshjoshi

This comment has been minimized.

Copy link

hiteshjoshi commented Dec 7, 2018

I am getting errors like

load failed with error Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=
@kdawgwilk

This comment has been minimized.

Copy link

kdawgwilk commented Mar 18, 2019

How would go about adding support for Encodable Parameters?

@paweljankowski

This comment has been minimized.

Copy link

paweljankowski commented Mar 21, 2020

Great example! But how to return Response with arrays of codable items? :)

@HareshGediya

This comment has been minimized.

Copy link

HareshGediya commented Mar 24, 2020

Awesome! How would to add support for custom JSON initialiser?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.