APIKit 2 (1): レスポンスに応じた独自のエラーを投げる
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ではリクエストの送信は、Session
のsendRequest<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
などは安全な操作だけで済むようになり、アプリの品質も上がるかもしれません。