Skip to content

A lightweight networking client abstraction over URLSession

License

Notifications You must be signed in to change notification settings

DanielCardonaRojas/APIClient

Repository files navigation

API Client

APIClientTests codecov

A simple networking abstraction inspired by: http://kean.github.io/post/api-client

Features

  • Provides RequestBuilder easily create your requests
  • Declarative definition of endpoints
  • Define your endpoints mostly with relative paths to a base URL.
  • Easily adaptable to use with common reactive frameworks (RxSwift, PromiseKit) via extensions
  • Comes with Combine support.
  • Chain multiple requests easily
  • Mocks responses easily

Documentation

Checkout the docs here

Installation

Carthage

github "DanielCardonaRojas/APIClient" ~> 1.0.1

Cocoapods

pod 'APIClient', :git => 'https://github.com/DanielCardonaRojas/APIClient', :tag => '1.0.1', :branch => 'master'

SwiftPM

.package(url: "https://github.com/DanielCardonaRojas/APIClient", .upToNextMajor(from: "1.0.0"))

Usage

  1. Create a client object pointing to some base url
lazy var client: APIClient = {
	let configuration = URLSessionConfiguration.default
	let client = APIClient(baseURL: "https://jsonplaceholder.typicode.com", configuration: configuration)
	return client
}()
  1. Define a declerative API
struct Todo: Codable {
    let title: String
    let completed: Bool
}

enum API {
    enum Todos {
        static func get() -> Endpoint<Todo> {
            return Endpoint<Todo>(method: .get, path: "/todos/1")
        }
    }
}
  1. Consume the API (Comes with Callback and combine API), refer to the section below to integrate with PromiseKit or RxSwift

Callback API

client.request(endpoint, success: { item in
    print("\(item)")
}, fail: { error in
    print("Error \(error.localizedDescription)")
})

Combine API

// Combine API
let publisher: AnyPublisher<Todo, Error> = client.request(endpoint)

publisher.sink(receiveCompletion: { completion in
    if case let .failure(error) = completion {
        print("Error \(error.localizedDescription)")
    }
}, receiveValue: { value in
    print("\(value)")
}).store(in: &disposables)

Chaining multiple request

Alternatively to using the regular combine API of the APIClient class, APIClientPublisher creates a custom publisher from a APIClient and allows to easily chain multiple endpoints creating a sequence dependent requests.

let endpoint: Endpoint<[Post]> = Endpoint(method: .get, path: "/posts")

APIClientPublisher(client: client, endpoint: endpoint).chain({
    Endpoint<PostDetail>(method: .get, path: "/posts/\($0.first!.id)")
}).receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in

}, receiveValue: { _ in
    expectation.fulfill()
}).store(in: &disposables)

Retrying a request

There is no built interceptors in this package but retrying requests and other related effects can be accomplished, using combine built in facilities.

Retrying requests can be accomplished using tryCatch and has been documented by many authors, give this a read for more details

  // Copied from https://www.donnywals.com/retrying-a-network-request-with-a-delay-in-combine/
  .tryCatch({ error -> AnyPublisher<(data: Data, response: URLResponse), Error> in
    print("In the tryCatch")

    switch error {
    case DataTaskError.rateLimitted, DataTaskError.serverBusy:
      return dataTaskPublisher
        .delay(for: 3, scheduler: DispatchQueue.global())
        .eraseToAnyPublisher()
    default:
      throw error
    }
  })
  .retry(2)

Mocking Responses

It is easy to add fake responses that will bypass any http calls.

let client: APIClient = ...


MockDataClientHijacker.sharedInstance.registerSubstitute(User.fake(), requestThatMatches: .path(#"/posts/*"#))

client.hijacker = MockDataClientHijacker.sharedInstance


let endpoint = Endpoint<User>(method: .get, path: "/")

client.request(endpoint) // Will return fake User

PromiseKit Integration

Integrating PromiseKit can be done through the following extension:

import PromiseKit

extension APIClient {
    func request<Response, T>(_ requestConvertible: T,
                              additionalHeaders headers: [String: String]? = nil,
                              additionalQuery queryParameters: [String: String]? = nil,
                              baseUrl: URL? = nil) -> Promise<T.Result>
        where T: URLResponseCapable, T: URLRequestConvertible, T.Result == Response {
            return Promise { seal in
                self.request(requestConvertible, additionalHeaders: headers, additionalQuery: queryParameters, success: { response in
                    seal.fulfill(response)
                }, fail: { error in
                    seal.reject(error)
                })

            }
    }
}

RxSwift Integration

Use this extension

import RxSwift

extension APIClient {

    func request<Response, T>(_ requestConvertible: T,
                              additionalHeaders headers: [String: String]? = nil,
                              additionalQuery queryParameters: [String: String]? = nil,
                              baseUrl: URL? = nil) -> Observable<T.Result>
        where T: URLResponseCapable, T: URLRequestConvertible, T.Result == Response {

            return Observable.create({ observer in
                let dataTask = self.request(requestConvertible, additionalHeaders: headers, additionalQuery: queryParameters, baseUrl: baseUrl, success: { response in
                    observer.onNext(response)
                    observer.onCompleted()
                }, fail: {error in
                    observer.onError(error)
                })

                return Disposables.create {
                    dataTask?.cancel()
                }
            })
    }
}

About

A lightweight networking client abstraction over URLSession

Resources

License

Stars

Watchers

Forks

Packages

No packages published