APIClient is a client library for OpenAPI. It makes OpenAPI generated code remarkably more straightforward than the default one.
The generated code by Open API is a strongly tied scheme definition and networking code. It makes debugging and logging difficult. This library separates networking code from OpenAPI generated code, and you can depend on only schema and model definitions.
import Foundation
import Alamofire
open class PetAPI {
open class func getPetById(petId: Int64, completion: @escaping ((_ data: Pet?,_ error: Error?) -> Void)) {
getPetByIdWithRequestBuilder(petId: petId).execute { (response, error) -> Void in
completion(response?.body, error)
}
}
open class func getPetByIdWithRequestBuilder(petId: Int64) -> RequestBuilder<Pet> {
var path = "/pet/{petId}"
let petIdPreEscape = "\(petId)"
let petIdPostEscape = petIdPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? ""
path = path.replacingOccurrences(of: "{petId}", with: petIdPostEscape, options: .literal, range: nil)
let URLString = PetstoreAPI.basePath + path
let parameters: [String:Any]? = nil
let url = URLComponents(string: URLString)
let requestBuilder: RequestBuilder<Pet>.Type = PetstoreAPI.requestBuilderFactory.getBuilder()
return requestBuilder.init(method: "GET", URLString: (url?.string ?? URLString), parameters: parameters, isBody: false)
}
...
import Foundation
open class PetAPI {
open class func getPetById(petId: Int64) -> RequestProvider<Pet> {
var path = "/pet/{petId}"
let petIdPreEscape = "\(petId)"
let petIdPostEscape = petIdPreEscape.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? ""
path = path.replacingOccurrences(of: "{petId}", with: petIdPostEscape, options: .literal, range: nil)
return RequestProvider<Pet>(endpoint: path, method: "GET")
}
...
RequestProvider<Response>
just encodes an endpoint (path), parameters (query, form or JSON), an HTTP method and a response type.
Add an extension to convert OpenAPI's RequestProvider<Response>
to APIClient's Request<Response>
.
import Foundation
import APIClient
import Petstore
extension RequestProvider {
func request() -> Request<Response> {
if let parameters = parameters {
switch parameters {
case .query(let raw):
return Request(endpoint: endpoint, method: method, parameters: Request.Parameters(raw))
case .form(let raw):
return Request(endpoint: endpoint, method: method, parameters: Request.Parameters(raw))
case .json(let raw):
return Request(endpoint: endpoint, method: method, parameters: Request.Parameters(raw))
}
}
return Request(endpoint: endpoint, method: method)
}
}
Initialize Client
instance.
let client = Client(baseURL: URL(string: "https://petstore.swagger.io/v2")!)
...
Then you can call Client.perform
with Request<Response>
object.
client.perform(request: PetAPI.getPetById(petId: 1000).request()) {
switch $0 {
case .success(let response):
let pet = response.body
...
case .failure(let error):
...
}
}
github "folio-sec/APIClient"
Then run carthage update
.
Follow the current instructions in Carthage's README for up to date installation instructions.
APIClient supports request and response interceptors.
The following example is a logger interceptor.
import Foundation
import APIClient
public struct Logger: Intercepting {
public init() {}
public func intercept(client: Client, request: URLRequest) -> URLRequest {
print("\(requestToCurl(client: client, request: request))")
return request
}
// swiftlint:disable large_tuple
public func intercept(client: Client, request: URLRequest, response: URLResponse?, data: Data?, error: Error?) -> (URLResponse?, Data?, Error?) {
if let response = response as? HTTPURLResponse {
let path = request.url?.path ?? ""
print("\(request.httpMethod?.uppercased() ?? "") \(path) \(response.statusCode)")
} else if let error = error {
print("\(error)")
}
return (response, data, error)
}
private func requestToCurl(client: Client, request: URLRequest) -> String {
...
}
}
client.interceptors = [Logger()]
...
The Authenticator has an opportunity to retry when it receives a 401 response. It will be used to seamlessly refresh access tokens.
import Foundation
import APIClient
struct Authenticator: Intercepting, Authenticating {
private let credentials: Credentials
init(credentials: Credentials) {
self.credentials = credentials
}
func intercept(client: Client, request: URLRequest) -> URLRequest {
return sign(request: request)
}
func authenticate(client: Client, request: URLRequest, response: HTTPURLResponse, data: Data?, completion: @escaping (AuthenticationResult) -> Void) {
switch response.statusCode {
case 401:
if let url = request.url, !url.path.hasSuffix("/login"), let refreshToken = credentials.fetch()?.refreshToken {
client.perform(request: AuthenticationAPI.authorize(refreshToken: refreshToken).request()) {
switch $0 {
case .success(let response):
let body = response.body
self.credentials.update(token: Token(accessToken: body.accessToken, refreshToken: body.refreshToken, expiry: Date().addingTimeInterval(TimeInterval(body.expiresIn))))
completion(.success(self.sign(request: request)))
return
case .failure(let error):
switch error {
case .networkError, .decodingError:
completion(.failure(error))
return
case .responseError(let code, _, _):
switch code {
case 400...499:
self.credentials.update(token: nil)
completion(.failure(error))
return
case 500...499:
completion(.failure(error))
return
default:
break
}
}
completion(.failure(error))
return
}
}
} else {
completion(.cancel)
return
}
default:
completion(.cancel)
return
}
}
private func sign(request: URLRequest) -> URLRequest {
var request = request
if let url = request.url, !url.path.hasSuffix("/login") {
if let accessToken = credentials.fetch()?.accessToken {
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
}
return request
}
}
let authenticator = Authenticator(credentials: credentials)
client.authenticator = authenticator
client.interceptors = [authenticator] + client.interceptors // for signing all requests
...