APIKit 2がリリースされました🎉

これから数日に分けて、このバージョンの特徴をいくつか紹介しようと思います。

Web APIのエラーレスポンス

大抵のWeb APIでは、エラーレスポンスが定義されています。たとえば、GitHub APIではHTTPステータスコードに応じて次のようなレスポンスが返されます。

400 Bad Request

{"message":"Problems parsing JSON"}

422 Unprocessable Entity

{
  "message": "Validation Failed",
  "errors": [
    {
      "resource": "Issue",
      "field": "title",
      "code": "missing_field"
    }
  ]
}

今回は、このようなエラーをリクエストの呼び出し側に伝える方法を説明します。なお、この説明はDefining Request Protocol for Web Serviceのエラーの扱いをもう少し詳しく説明したものとなります。

サービス用のリクエストプロトコル

APIKitでは、特定のサービス(Web API)向けのリクエストの特徴をまとめるために、サービス用のリクエストプロトコルを定義します。今回はGitHub APIを例としているので、baseURLのデフォルト値がhttps://api.github.comとなっているGitHubRequestTypeを定義しました。

protocol GitHubRequestType: RequestType {

}

extension GitHubRequestType {
    var baseURL: NSURL {
        return NSURL(string: "https://api.github.com")!
    }
}

本記事のタイトルの”レスポンスに応じた独自のエラーを投げる”という動作は、このリクエストプロトコル上に定義します。動作を定義する前に、まずは投げる対象のエラーの型を定義します。

エラーレスポンスの構造体

GitHub APIのエラーレスポンスは次のような構造体で表します。

// 話を単純にするために422の`errors`は省略。
struct GitHubError: ErrorType {
    let message: String

    init(object: AnyObject) {
        message = object["message"] as? String ?? "Unknown Error"
    }
}

レスポンスに応じた独自のエラーの生成

APIKit 2では、interceptObject(_:URLResponse:)でレスポンスのAnyObjectを横取りすることができます。次の例では、HTTPステータスコードが200..<300なら通常通りにレスポンスのAnyObjectを返し、400または422ならAnyObjectからGitHubErrorを生成してthrowしています。

extension GitHubRequestType {
    func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject {
        switch URLResponse.statusCode {
        case 200..<300:
            return object

        case 400, 422:
            throw GitHubError(object)

        default:
            throw RequestError.UnacceptableStatusCode(URLResponse.statusCode)
        }
    }
}

リクエストの結果の受け取り

APIKitではリクエストの送信は、SessionsendRequest<Request>(_:)で行い、その結果はResult<Request.Response, SessionTaskError>として受け取ります。エラーの型であるSessionTaskErrorは、次のように定義された列挙型です。

public enum SessionTaskError: ErrorType {
    case ConnectionError(ErrorType)
    case RequestError(ErrorType)
    case ResponseError(ErrorType)
}

SessionTaskErrorはエラーの発生箇所を表し、実際に何が起きたかはそれぞれの連想値が表します。interceptObject(_:URLResponse:)で投げたエラーはレスポンス由来のエラーなので、ResponseErrorの連想値に入ります。

独自のエラーのマッチング

いざリクエストを実行してみると、通信に失敗したり、JSONが壊れていたり、サーバーが変なレスポンスを返したりと結果はさまざまです。その中から、想定内のエラーレスポンスGitHubErrorをマッチングするには、以下のようにswitch文を2度書きます。

let request = GitHubAPI.SearchRepositoriesRequest(query: "APIKit")

Session.sendRequest(request) { result in
    switch result {
    case .Success(let response):
        print("response: ", response)

    case .Failure(let error):
        switch error {
        case .ResponseError(let gitHubError as GitHubError):
            print("GitHub API Error: \(gitHubError.message)")

        default:
            print("Unknown Error: \(error)")
        }
    }
}

コード中のgitHubErrorという定数の型はGitHubErrorになっているので、GitHubErrorのプロパティであるmessageにもアクセスできるというわけです。

こうして、独自に定義したエラーをリクエストの呼び出し側に伝えることができました。

まとめ

AnyObjectへのサブスクリプトやNSHTTPURLResponseのステータスコードの比較など、より原始的(?)な型を扱うコードはミスを犯しやすいです。今回の例では、こういった類のコードをプロトコル側にまとめることができました。結果として、sendRequest(_:)を実行するUIViewControllerなどは安全な操作だけで済むようになり、アプリの品質も上がるかもしれません。