Alamofire+Codableで204 No Contentもまとめて扱えるようにする

既存プロジェクトのAPIリクエスト周りの処理を見直した際に出くわした問題と、その対応のために行った小細工のメモ。

## もともとやりたかったこと
- APIリクエストのデコード処理にCodable(Decodable)を利用する
- リクエストにDecodableに適合したオブジェクトを紐付け、リクエスト実行とデコードを共通的に行えるようにする
  - 簡易的な[APIKit](https://github.com/ishkawa/APIKit)のようなイメージ

#### 実装における(緩い)制約
APIリクエスト処理にはもともと[Alamofire](https://github.com/Alamofire/Alamofire)を使用していたので、従来のコードを生かすため、Alamofireベースで処理を行う(=APIKitへの乗り換えはしない)


## やったこと(基本)
ここでは特に変わったことはしていない。`204 No Content`時の対応については[こちら](#やったこと204-no-content対応)

#### 1. 抽象リクエストの作成
リクエストの情報(=`Alamofire.request()` の引数とするもの)とレスポンスの型を紐付けるための型を定義。
レスポンスの型はデコード処理を簡潔に記述できるよう、`Decodable`に適合するよう制約を付加。

```Swift:AbstractRequest.swift
import Alamofire

struct AbstractRequest<R: Decodable> {
    typealias Response = R
    
    var urlString: URLConvertible
    var method: Alamofire.HTTPMethod
    var parameters: [String: Any]?
    var encoding: ParameterEncoding
    var headers: [String: String]
}
```
※ ↑実際のコードとは一部異なるがだいたいのイメージとして(以降のコードも同様)

#### 2. 抽象リクエストを引数としたリクエスト処理の実装

上述の`AbstractRequest`を引数としてリクエストの作成/レスポンスのデコードを行えるようにApiClientの処理を定義。
処理内容はAlamofireでリクエストを作成・実行し、`JSONDecoder`でデコードする程度。

```Swift:ApiClient.swift
import Alamofire

class ApiClient {
    func send<R>(_ abstract: AbstractRequest<R>,
                 completion: @escaping (Result<R, Error>) -> Void) {
       let request = Alamofire.request(
            abstract.urlString,
            method: abstract.method,
            parameters: abstract.parameters,
            encoding: abstract.encoding,
            headers: abstract.headers)
            
        request.validate(statusCode: 200..<400).response { response in
            // エラーハンドリングなど
            
            do {
                let r = try JSONDecoder().decode(R.self, from: response.data!)
                completion(.success(r))
            } catch let error {
                completion(.failure(error))
            }
        }
    }
}
```

## ↑の実装で困ったこと
ここまでの実装で基本的にはAPIリクエストの実行、レスポンスのデコードが行える。
しかし、レスポンスのステータスが`204 No Content`であった場合、デコード対象のデータが存在せずエラーが発生する。

## やったこと(`204 No Content`対応)
#### 1. 専用オブジェクトの定義
`204 No Content`のレスポンスが返るケース専用のオブジェクトを定義。

```Swift:NoContent.Swift
/// No Contentが返るケース専用のためプロパティは持たない
/// AbstractRequestに紐付けられるよう、Decodableに適合させておく
struct NoContent: Decodable {}

// NoContentを利用した際のAbstractRequestのイメージ
// let req = AbstractRequest<NoContent>(
//               "https://example.com/api/item/123,
//               method: .delete,
//               …
//               )
```

#### 2. APIClientを修正
デコード処理の前にレスポンスステータスの判定を追加。
ステータスが204の場合、`NoContent`を返すようにする。

```diff
        request.validate(statusCode: 200..<400).response { response in
            // エラーハンドリングなど
+ 
+            if response.response!.statusCode == 204,
+                let noContent = NoContent() as? R {
+                completion(.success(noContent))
+                return
+            }
            
            do {
                let r = try JSONDecoder().decode(R.self, from: response.data!)
                completion(.success(r))
            }
```


## なぜこうしたか
値が存在しないケースも扱えるようにするのであれば、`APIClient.send()`のcompletion handlerの引数を`Result<R?, String>`にする選択肢もあった。
しかしこの場合、すべてのAPIリクエストにおいて(大半は不要なはずの)アンラップ処理が各所に蔓延する問題が生じる。
この問題を避け、APIClient内のみで解決できるよう、上記の方法とした。
(`NoContent.swift`なる虚無感あふれるファイルが誕生していたり、無理やり感は否めないので、もっとうまいやり方があればコメントで指摘いただけると嬉しいです)