-
-
Save kean/64b9fc0963fd430594fdb3eb848bccf3 to your computer and use it in GitHub Desktop.
// The MIT License (MIT) | |
// | |
// Copyright (c) 2017 Alexander Grebenyuk (github.com/kean). | |
import Foundation | |
import Alamofire | |
import RxSwift | |
import RxCocoa | |
// This post is **archived**. For a modern version that uses Async/Await and Actors, see the new article | |
// [Web API Client in Swift](/post/new-api-client) (Nov 2021). | |
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")) | |
} |
Could you create a sample project, It will much better to understand with a project.
This is perfect, but... how to unit test your requests using mock responses? 👍
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.
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.
I am getting errors like
load failed with error Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=
How would go about adding support for Encodable
Parameters?
Great example! But how to return Response with arrays of codable items? :)
Awesome! How would to add support for custom JSON initialiser?
I can't understand how you can Chain two requests with this code.
Please, could you help me with an answer.
Thank you
Great example! But how to return Response with arrays of codable items? :)
Hey! Did you get a way to solve this?
The original post and the associated code are archived. For a modern version that uses Async/Await and Actors, see the new article Web API Client in Swift (Nov 2021).
how do you get the results from the
client.request(API.getCustomers())