Home

Awesome

SimpleApiClient

Swift CI Status Version License Platform

A configurable api client based on Alamofire4 and RxSwift4 for iOS

Requirements

Table of Contents

Installation

SimpleApiClient is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'SimpleApiClient'

<a name=basic_usage>Basic Usage</a>

Step 1

Configurate the api client

let config = SimpleApiClient.Config(
  baseUrl: "https://api.github.com",
  defaultParameters: ["foo": "bar"],
  defaultHeaders: ["foo": "bar"],
  timeout: 120, // default is 60s
  certificatePins: [
    CertificatePin(hostname: "https://api.github.com", certificateUrl: Bundle.main.url(forResource: "serverCert", withExtension: "cer")!)
  ],
  errorMessageKeyPath: "message",
  jsonDecoder: JSONDecoder(),  // default is JSONDecoder()
  isMockResponseEnabled: true, // default is false
  logHandler: { request, response in
    ...
  },
  errorHandler: { error in
    // you can centralize the handling of general error here
    switch error {
    case .authenticationError(let code, let message):
      ...
    case .clientError(let code, let message):
      ...
    case .serverError(let code, let message):
      ...
    case .networkError(let source):
      ...
    case .sslError(let source):
      ...
    case .uncategorizedError(let source):
      ...
    }
  }
)

let githubClient = SimpleApiClient(config: config)

Step 2

Create the API

import SimpleApiClient

struct GetRepoApi: SimpleApi {
  let user: String
  let repo: String
  
  var path: String {
    return "/repos/\(user)/\(repo)"
  }
  
  var method: HTTPMethod {
    return .get
  }
  
  // optional
  var parameters: Parameters {
    return [:]
  }
  
  // optional
  var headers: HTTPHeaders {
    return [:]
  }
}

extension SimpleApiClient {
  func getRepo(user: String, repo: String) -> Observable<Repo> {
    return request(api: GetRepoApi(user: user, repo: repo))
  }
}

Step 3

Use observe() to enqueue the call, do your stuff in corresponding parameter block. All blocks run on main thread by default and are optional.

githubClient.getRepo(user: "foo", repo: "bar")
  .observe(
    onStart: { print("show loading") },
    onEnd: { print("hide loading") },
    onSuccess: { print("sucess: \($0)") },
    onError: { print("error: \($0)" }
  )

Model

The library uses JSONDecoder to deserialize JSON to model object, so the model should conform to Decodable or Codable

struct User: Decodable {
  let name: String
  let profileUrl: URL
  // if the serialized name is different from the property name
  private enum CodingKeys: String, CodingKey {
    case name = "login"
    case profileUrl = "avatar_url"
  }
}

<a name=unwrap_keypath>Unwrap Response by KeyPath</a>

Sometimes the api response includes metadata that we don't need, but in order to map the response we create a wrapper class and make the function return that wrapper class. This approach leaks the implementation of service to calling code.

Assuming the response json looks like the following:

{
  total_count: 33909,
  incomplete_results: false,
  foo: {
    bar: {
      items: [
        {
          login: "foo",
          ...
        }
        ...
      ]
    }
  }
}

And you only need the items part, implement Unwrappable to indicate which part of response you want.

struct GetUsersApi: SimpleApi, Unwrappable {
  ... 
  
  var responseKeyPath: String {
    return "foo.bar.items"
  }
}

// then your response will be a list of User
extension SimpleApiClient {
  func getUsers(query: String) -> Observable<[User]> {
    return request(api: GetUsersApi(query: query))
  }
}

<a name=upload>Upload File(s)</a>

To upload file(s), make the API implements Uploadable to provide Multiparts

struct UploadImageApi: SimpleApi, Uploadable {
  ...
  
  var multiParts: [MultiPart] {
    let multiPart = MultiPart(data: UIImageJPEGRepresentation(image, 1)!, name: "imagefile", filename: "image.jpg", mimeType: "image/jpeg")
    return [multiPart]
  }
}

extension SimpleApiClient {
  func uploadImage(image: UIImage) -> Observable<Image> {
    return request(api: UploadImageApi(image))
  }
}

<a name=serial_parallel_calls>Serial / Parallel Calls</a>

Serial

githubClient.foo()
  .then { foo in githubClient.bar(foo.name) }
  .observe(...)

Serial then Parallel

githubClient.foo()
  .then { foo in githubClient.bar(foo.name) }
  .thenAll { bar in 
    (githubClient.baz(bar.name), githubClient.qux(bar.name)) // return a tuple
  }
  .observe(...)

Parallel

SimpleApiClient.all(
  githubApi.foo(),
  githubApi.bar()
)
.observe(...)

Parallel then Serial

SimpleApiClient.all(
  githubApi.foo(),
  githubApi.bar()
).then { array -> // the return type is Array<Any>, you should cast them, e.g. let foo = array[0] as! Foo
  githubApi.baz()
}.observe(...)

<a name=retry>Retry Interval / Exponential backoff</a>

githubClient.getUsers("foo")
  .retry(delay: 5, maxRetryCount: 3) // retry up to 3 times, each time delays 5 seconds
  .retry(exponentialDelay: 5, maxRetryCount: 3) // retry up to 3 times, each time delays 5^n seconds, where n = {1,2,3}
  .observe(...)

<a name=call_cancel>Call Cancellation</a>

Auto Call Cancellation

The call will be cancelled when the object is deallocated.

githubClient.getUsers("foo")
  .cancel(when: self.rx.deallocated)
  .observe(...)

Cancel call manually

let call = githubClient.getUsers("foo").observe(...)

call.cancel()

<a name=mock_response>Mock Response</a>

To enable response mocking, set SimpleApiClient.Config.isMockResponseEnabled to true and make the API implements Mockable to provide MockResponse.

Mock sample json data

To make the api return a successful response with provided json

struct GetUsersApi: SimpleApi, Mockable {
  ...
  
  var mockResponse: MockResponse {
    let file = Bundle.main.url(forResource: "get_users", withExtension: "json")!
    return MockResponse(jsonFile: file)
  }
}

Mock status

To make the api return a client side error with provided json

struct GetUsersApi: SimpleApi, Mockable {
  ...
  
  var mockResponse: MockResponse {
    let file = Bundle.main.url(forResource: "get_users_error", withExtension: "json")!
    return MockResponse(jsonFile: file, status: .clientError)
  }
}

the parameter jsonFile of MockResponse is optional, you can set the status only, then you receive empty string.

Possible Status values:

public enum Status {
  case success
  case authenticationError
  case clientError
  case serverError
  case networkError
  case sslError
}

To mock a response with success status only, you should return Observable<Nothing>.

struct DeleteRepoApi: SimpleApi, Mockable {
  ...
  
  var mockResponse: MockResponse {
    return MockResponse(status: .success)
  }
}

extension SimpleApiClient {
  func deleteRepo(id: String) -> Observable<Nothing> {
    return request(api: DeleteRepoApi(id: id))
  }
}

Author

jaychang0917, jaychang0917@gmail.com

License

SimpleApiClient is available under the MIT license. See the LICENSE file for more info.