Skip to content

folio-sec/APIClient

Repository files navigation

Build Status codecov Carthage compatible

APIClient

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.

BEFORE

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)
    }
    ...

AFTER

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.

Usage

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):
        ...
    }
}

Installation

github "folio-sec/APIClient"

Then run carthage update.

Follow the current instructions in Carthage's README for up to date installation instructions.

Advanced

Interceptor

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()]
...

Authenticator

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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages