AlamofireのRequestAdapter, RequestRetrierについて

# 概要

Alamofire 4.0以降に導入された RequestAdapter, RequestRetrierについての説明とユースケースです。APIClientの開発をしており、とても便利だったのですが、一方で日本語記事がほとんど見当たらなかったので情報提供させていただきます。

# RequestAdapter, RequestRetrier

## 公式ドキュメント

まずはこちらをご一読ください。
v4.7.2現在の公式ドキュメントです。
[Adapting and Retrying Requests](https://github.com/Alamofire/Alamofire/blob/2fb881a1702cb1976c36192aceb54dcedab6fdc2/Documentation/AdvancedUsage.md#adapting-and-retrying-requests)

## つまり何？？（ユースケース）

ほとんどドキュメントに書かれている通りのことではありますが、以下のような用途でこれを導入するととても便利です。

* 認証機構をHTTPに組み込む際に、HTTPヘッダーにトークンを埋め込む。
* トークンが有効期限切れだった場合に再認証を行なった上で、通信をリトライする。
* カスタムのUserAgentを事前にHTTPヘッダーに埋め込む

などです。

## 実装方法

以下のProtocolに準拠したオブジェクトを実装するとRequest実行前にURLRequestがDelegateされてくるので、その時点でURLRequestオブジェクトに必要な前処理を施すことができます。

### RequestAdapter

引用元: [Request.swift](https://github.com/Alamofire/Alamofire/blob/2fb881a1702cb1976c36192aceb54dcedab6fdc2/Source/Request.swift#L27-L37)

```swift:RequestAdapter
/// A type that can inspect and optionally adapt a `URLRequest` in some manner if necessary.
public protocol RequestAdapter {
    /// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result.
    ///
    /// - parameter urlRequest: The URL request to adapt.
    ///
    /// - throws: An `Error` if the adaptation encounters an error.
    ///
    /// - returns: The adapted `URLRequest`.
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest
}
```

### RequestRetrier

以下のProtocolに準拠したオブジェクトを実装し、リクエスト時に `validate()` というメソッドを実行した上でリクエストを行うと完了時にRequestRetrierのオブジェクトがDelegateされるようになります。

```swift:リクエスト実行の例
sessionManager
  .request(urlRequest)
  .validate()
  .responseJSON { responseData in
    print(responseData)
}
```

引用元: [Request.swift](https://github.com/Alamofire/Alamofire/blob/2fb881a1702cb1976c36192aceb54dcedab6fdc2/Source/Request.swift#L41-L58)

```swift:RequestRetrier
/// A closure executed when the `RequestRetrier` determines whether a `Request` should be retried or not.
public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void

/// A type that determines whether a request should be retried after being executed by the specified session manager
/// and encountering an error.
public protocol RequestRetrier {
    /// Determines whether the `Request` should be retried by calling the `completion` closure.
    ///
    /// This operation is fully asynchronous. Any amount of time can be taken to determine whether the request needs
    /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly
    /// cleaned up after.
    ///
    /// - parameter manager:    The session manager the request was executed on.
    /// - parameter request:    The request that failed due to the encountered error.
    /// - parameter error:      The error encountered when executing the request.
    /// - parameter completion: The completion closure to be executed when retry decision has been determined.
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion)
}
```

## サンプルコード

上記までの情報を元に、簡単なサンプルコードを示します。
要件としては、最初に説明したものを実装します。

* 要件
    * 認証機構をHTTPに組み込む際に、HTTPヘッダーにトークンを埋め込む。
    * トークンが有効期限切れだった場合に再認証を行なった上で、通信をリトライする。
    * カスタムのUserAgentを事前にHTTPヘッダーに埋め込む

```swift
import Foundation
import Alamofire

class RequestAdapterAndRetrier: RequestRetrier, RequestAdapter {

    // SessionTokenを取得するRepository（例）
    let sessionRepository: SessionRepository
    // Requestに付与するUserAgent文字列
    let userAgent: String

    init(sessionRepository: SessionRepository, userAgent: String) {
        self.sessionRepository = sessionRepository
        self.userAgent = userAgent
    }

    // RequestRetrierに準拠
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        // Lockを取得してThread safeに通過させる
        lock.lock() ; defer { lock.unlock() }

        if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
            // 401でレスポンスされた場合にSessionTokenを取得し直す
            self.sessionRepository.get { (_, error) in
                if let error = error {
                    print(error)
                    return
                }
                // 0.5秒後にリトライ実行する
                completion(true, 0.5)
            }
        } else {
            // 401以外はリトライしない
            completion(false, 0.0)
        }
    }

    // RequestAdapterに準拠
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        var urlRequest = urlRequest

        // ExpireしていないSessionTokenを持っている場合はURLReuestに付与する
        if let sessionToken = self.sessionRepository.get() {
            urlRequest.setValue(sessionToken, forHTTPHeaderField: "Session-Token")
        }
        
        // UserAgentを付与する
        urlRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent")
        return urlRequest
    }
}
```

こんな感じです。
認証リトライは結構複雑な記述をしがちですが、この機構を使えば見通し良いコードで安全に書けるので便利！！！！