Most web APIs have common configurations such as base URL, authorization header fields and MIME type to accept. For example, GitHub API has common base URL https://api.github.com
, authorization header field Authorization
and MIME type application/json
. Protocol to express such common interfaces and default implementations is useful in defining many request types.
We define GitHubRequest
to give common configuration for example.
First of all, we give default implementation for baseURL
.
import APIKit
protocol GitHubRequest: Request {
}
extension GitHubRequest {
var baseURL: URL {
return URL(string: "https://api.github.com")!
}
}
There are several JSON mapping library such as Himotoki, Argo and Unbox. These libraries provide protocol that define interface to decode Any
into JSON model type. If you adopt one of them, you can give default implementation to response(from:urlResponse:)
. Here is an example of default implementation with Himotoki:
import Himotoki
extension GitHubRequest where Response: Decodable {
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
return try Response.decodeValue(object)
}
}
Since GitHubRequest
has default implementations of baseURL
and response(from:urlResponse:)
, all you have to implement to conform to GitHubRequest
are 3 components, Response
, method
and path
.
import APIKit
import Himotoki
final class GitHubAPI {
struct RateLimitRequest: GitHubRequest {
typealias Response = RateLimit
var method: HTTPMethod {
return .get
}
var path: String {
return "/rate_limit"
}
}
struct SearchRepositoriesRequest: GitHubRequest {
let query: String
// MARK: Request
typealias Response = SearchResponse<Repository>
var method: HTTPMethod {
return .get
}
var path: String {
return "/search/repositories"
}
var parameters: Any? {
return ["q": query]
}
}
}
struct RateLimit: Decodable {
let limit: Int
let remaining: Int
static func decode(_ e: Extractor) throws -> RateLimit {
return try RateLimit(
limit: e.value(["rate", "limit"]),
remaining: e.value(["rate", "remaining"]))
}
}
struct Repository: Decodable {
let id: Int64
let name: String
static func decode(_ e: Extractor) throws -> Repository {
return try Repository(
id: e.value("id"),
name: e.value("name"))
}
}
struct SearchResponse<Item: Decodable>: Decodable {
let items: [Item]
let totalCount: Int
static func decode(_ e: Extractor) throws -> SearchResponse {
return try SearchResponse(
items: e.array("items"),
totalCount: e.value("total_count"))
}
}
It is useful for code completion to nest request types in a utility class like GitHubAPI
above.
Most web APIs define error response to notify what happened on the server. For example, GitHub API defines errors like this. interceptObject(_:URLResponse:)
in Request
gives us a chance to determine if the response is an error. If the response is an error, you can create custom error object from the response object and throw the error in interceptObject(_:URLResponse:)
.
Here is an example of handling GitHub API errors:
// https://developer.github.com/v3/#client-errors
struct GitHubError: Error {
let message: String
init(object: Any) {
let dictionary = object as? [String: Any]
message = dictionary?["message"] as? String ?? "Unknown error occurred"
}
}
extension GitHubRequest {
func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any {
guard 200..<300 ~= urlResponse.statusCode else {
throw GitHubError(object: object)
}
return object
}
}
The custom error you throw in intercept(object:urlResponse:)
can be retrieved from call-site as .failure(.responseError(GitHubError))
.
let request = GitHubAPI.SearchRepositoriesRequest(query: "swift")
Session.send(request) { result in
switch result {
case .success(let response):
print(response)
case .failure(let error):
self.printError(error)
}
}
func printError(_ error: SessionTaskError) {
switch error {
case .responseError(let error as GitHubError):
print(error.message) // Prints message from GitHub API
case .connectionError(let error):
print("Connection error: \(error)")
default:
print("System error :bow:")
}
}