はじめに
Web APIをアプリから利用する場合、そのAPIリクエストの仕様をSwiftで型として扱いたい場合があります。それでAPI仕様書と照らし合わせてミスがないか、齟齬がないかを確認したりトラブル時のきりわけをしたかったりするわけです(私は)。そういう場合の型の定義についてAlamofireを利用してどうやるかを書いておきます。他に良いやり方があるかもしれないのでそういう場合はコメントをもらえれば助かります。
具体的には何らかのWebAPIリクエストをつぎのようにできるのが良いんじゃないかという話です。
// MARK: - リクエスト型の定義側
struct 何らかのWebAPIリクエスト: WebAPIRequest { // protocol WebAPIRequest: URLRequestConvertible { ... }
typealias Response = User // struct User: Decodable { ... }
// MARK: - WebAPIRequest実装とリクエストパラメータ
let baseURL: URL
let accessToken: String?
var path: String { "..." }
let method = HTTPMethod.put
var parameters: Parameters {
[/*key*/: 何らかのパラメータ]
}
// MARK: -
let 何らかのパラメータ: String
init(baseURL: URL, accessToken: String, 何らかのパラメータ: String) {
...
}
// MARK: - 想定されているAPIごとのエラーコードとそのエラー
enum ResponseError: Error {
case message(String)
case server
static func error(from statusCode: Int, _ data: Data?) -> ResponseError? {
switch statusCode {
case ...:
case ...:
default:
assertionFailure(/*API仕様が間違ってるかサーバの動作異常*/)
return nil
}
}
// MARK: - ステータスコードによってエラーをハンドリングさせるためのカスタムなバリデーション
static func validate(statusCode: Int, data: Data?) -> Result<(), Error> {
}
}
// MARK: - 利用側
let request = 何らかのWebAPIリクエスト(パラメータ)
AF.request(request)
.validate { _, response, data in
何らかのWebAPIリクエスト.validate(statusCode: reesponse.statusCode, data: data)
}
.responseDecodable(of: 何らかのWebAPIリクエスト.Response.self) { response in
// response: DataResponse<Success, AFError> を利用した結果のハンドリング
// ...
}
- URLRequestConvertibleプロトコル
- AlamofireでMoyaっぽいことをやりたいために用意されたもの
- 今回はMoyaっぽいことをしたいわけじゃなく単一のリクエスト型を示すために使う
- AlamofireでMoyaっぽいことをやりたいために用意されたもの
- DataRequestクラス
-
validate(_ validation: @escaping Validation) -> Self
-
responseDecodable<T: Decodable>(of type: T.Type = T.self ...)
-
正常時に特定の型
T
にデコードしてResultに入れてくれる- 異常時にはエラーをResultに入れてくれる
-
正常時に特定の型
-
以降細かくどうやるかを書いておきます。
やりたいことの整理
- リクエスト仕様の表現
- リクエストに対するレスポンス型の明示
- 成功時のJSONを型として変換した際の表現
- 呼び出し側で自由にパラメータを変更できるのではなく、最低限のパラメータとしたい
- なぜ?
- パラメータを変更できるようにしすぎるとトラブル時の環境によって何が起こっているかわからない
- なぜ?
- どうやる?
- 型の持つ定数として利用時にインスタンス化したい
- リクエストに対するレスポンス型の明示
- レスポンスについて
- 正常時
- レスポンスのデータを型としたい
- 異常時
- エラーの可能性も列挙したい
- デコードする
- エラー時のハンドリング
- ステータスコードを列挙してそれごとの処理を網羅
- Errorとしてユーザ表示するものがあればデコードする
- エラーの可能性も列挙したい
- 正常時
やり方の整理
- リクエストの表現
- AlamofireのURLRequestConvertibleプロトコルに準拠した型を作る
- レスポンス型の明示
- 正常系
- structでつくってリクエストパラメータの型にtypealiasでアクセスしやすくしとく
- 異常系
- Alamofireのvalidateクロージャを使う
- 正常系
Alamofireのつかいたい部分のために整理する図は以下
AlamofireのURLRequestConvertibleプロトコルを使う
AlamofireにはいつからかURLRequestConvertibleプロトコルがあり、これがURLRequest自体を作成するメソッドを持ちます。
/// Types adopting the `URLRequestConvertible` protocol can be used to safely construct `URLRequest`s.
public protocol URLRequestConvertible {
/// Returns a `URLRequest` or throws if an `Error` was encountered.
///
/// - Returns: A `URLRequest`.
/// - Throws: Any error thrown while constructing the `URLRequest`.
func asURLRequest() throws -> URLRequest
}
これはAF(つまりSessionのdefult)がリクエスト送るときの引数として利用することで通信の内容を決めます。使い方は次のような感じ。
AF.request(request)
.validate()
.responseData { ... }
そんで、このURLRequestConvertible
に準拠させたプロトコルを用意してリクエスト仕様を表現できるようにしようというわけです。
URLRequestConvertible
に準拠させて次のようにアプリで利用するプロトコルを決めます。
protocol WebAPIRequest: URLRequestConvertible {
// APIリクエストごとに変わったり変わらなかったりする
var baseURL: URL { get }
var path: String { get }
var method: Alamofire.HTTPMethod { get }
var parameters: Alamofire.Parameters { get }
// 認証が不要な場合はnil
var accessToken: String? { get }
}
複数の各リクエストが上記プロトコルに準拠させて利用させますが、今回はaccessTokenによる認可の方式を想定としていて利用側が必要だったらセットするし、必要なかったらセットしないようにしています。大抵の場合、基本的には必要で、ログインAPIなどは必要じゃなかったりするでしょう。
URLRequestConvertible
のメソッドasURLRequest()
を実装しておき、各リクエストがデフォルトで同じ処理を行うようにしておきます。上記プロトコルで決めたプロパティから単にURLRequestを組み立てるだけです。
extension WebAPIRequest {
// デフォルト実装としてprotoocl extensionsで実装を書いておく
public func asURLRequest() throws -> URLRequest {
let request = URLRequest(url: baseURL.appendingPathComponent(path))
// ここではデフォルトとしてpost, put, patchの場合にJSONでパラメータを送るようにしています。
// もちろんそれはAPI仕様によって変わりますのでそれぞれの仕様に合わせるのが良いでしょう。
switch method {
case .post, .put, .patch:
return try Alamofire.JSONEncoding.default.encode(defaultIntercept(request), with: parameters)
default:
return try Alamofire.URLEncoding.queryString.encode(defaultIntercept(request), with: parameters)
}
}
}
private extension WebAPIRequest {
// デフォルトでアクセストークンがある前提
func addAccessToken(
_ accessToken: String,
to urlRequest: Foundation.URLRequest
) -> Foundation.URLRequest {
var urlRequest = urlRequest
// OAuthだいたいこんな感じだったような...
let headers = [
"Authorization": "Bearer \(accessToken)",
"Connection": "Close",
]
headers.forEach { key, value in
urlRequest.addValue(value, forHTTPHeaderField: key)
}
return urlRequest
}
}
これでWebAPIRequestプロトコルに準拠したリクエストを渡すことで、アクセストークンが必要な場合はデフォルトでそれをヘッダーに追記したりしてくれるわけです。
利用例は変わってません。
// ここでWebAPIRequestプロトコルに準拠したリクエスト渡しつつURLRequestを組み立て
AF.request(request)
.validate()
.responseData { ... }
サンプルとしてWebAPIRequestに準拠したstructを考えてみます。
架空のユーザ一覧を取得するAPIの場合、つぎのような感じでしょう。
struct UsersGetRequest: WebAPIRequest {
public typealias Response = [User]
let baseURL: URL
let accessToken: String?
var path: String { "/users" }
let method = HTTPMethod.get
let page: Int
var parameters: Parameters {
["per_page": page]
}
init(baseURL: URL, accessToken: String, page: Int) {
self.baseURL = baseURL
self.accessToken = accessToken
self.page = page
}
}
レスポンスも使ってほしいので
// ここでWebAPIRequestプロトコルに準拠したリクエスト渡しつつURLRequestを組み立て
AF.request(request)
.validate()
.responseDecodable(of: UsersGetRequest.Response.self) { ... }
// ここでレスポンス明示するんじゃなくてジェネリクスで決まれば良さそう...
.responseDecodable(of:)
でDecodableな型を指定すればデコードしてくれるんですが、呼び出し側で変更することなんてないので自動で決まると良いですね、とは思うもののそれはそのままにしておきます。
エラーハンドリングのために
場合によってはステータスコードによってサーバサイドから作りたくなります。
たとえばユーザ情報を取得するWeb APIが仮にステータスコード400や500を返すとあらかじめ決められている場合を考えます。400と500が来た際、400だとエラーメッセージがレスポンスにあるから取り出したい。
public struct UsersGetRequest: WebAPIRequest {
public typealias Response = Content
let baseURL: URL
let accessToken: String?
var path: String { "/users" }
let method = HTTPMethod.get
let page: Int
var parameters: Parameters {
["per_page": page]
}
init(baseURL: URL, accessToken: String, page: Int) {
self.baseURL = baseURL
self.accessToken = accessToken
self.page = page
}
// 仕様上定義されている既知のエラー
// 例えば400, 500エラーが想定されていて、400のときにレスポンスされるJSONにエラー情報がある。
enum ResponseError: Error {
case paramter(String)
case server
static func error(from statusCode: Int, _ data: Data?) -> ResponseError? {
switch statusCode {
case 400:
let response = try! JSONDecoder().decode(ErrorMessage.self, from: data!)
return .parameter(response.message)
case 500:
return .server
default:
return nil
}
}
}
static func validate(statusCode: Int, data: Data?) -> Result<(), Error> {
if let error = ResponseError.error(from: statusCode, data) {
// 既知のエラーをResultで返すようにする
return .failure(error)
} else if (200..<300).contains(statusCode) {
return .success(())
} else {
// 原因がわからないvalidationエラーはとりあえずAFErrorに用意されてるインタフェースを利用
let reason = AFError.ResponseValidationFailureReason.unacceptableStatusCode(code: statusCode)
return .failure(AFError.responseValidationFailed(reason: reason))
}
}
}
最後に、利用例はつぎのような感じ。
// ここでWebAPIRequestプロトコルに準拠したリクエスト渡しつつURLRequestを組み立て
AF.request(request)
.validate { _, response, data in
UsersGetRequest.validate(statusCode: reesponse.statusCode, data: data)
}
.responseDecodable(of: UsersGetRequest.Response.self) { ... }
// response: DataResponse<Success, AFError> を利用した結果のハンドリング
// ...
}
DataRequestにはvalidate(_ validation: @escaping Validation) -> Self
メソッドが有り、クロージャでカスタムな処理を渡せるわけです。
その他
AlamofireにはRequestInterceptorがある
AlamofireにはRequestInterceptorがあり、request利用時にURLリクエストに対する置き換えやリトライ条件の設定を行うことができます。
注意するところとしては、これを利用するのはリクエスト呼び出し時です。
AF.request(request, interceptor: interceptor)
.validate()
.responseDecodable(of: UsersGetRequest.Response.self) { ... }
RequestInterceptorでは例えばアクセストークンをセットするなどのインターセプトを行えますが、これはリクエストを型としたWebAPIRequest
のprotocol extensionsを使うほうがAPIの仕様をまとめるために私は良い気がします(つまり公式の機能としてRequestInterceptorがあるが使わない)。
リトライについては呼び出し側が制御したいので、公式のRequestInterceptorを使えば良いとは思います。
すでに自分で似たようなことを書いていた