Moya を使ってレスポンス body が空の時、ステータスコードによって成功・失敗を判断する方法

以前初めて Moya を使った時に、

- API からステータスコードのみが送られる
- レスポンスの body は空である

というケースに出会いました。
僕は、「はいはい、どうせ空のレスポンスを作って上げれば `.success` 返ってくるんでしょ」を思って実装したのですが、、、
テストしてみると全然成功してくれない。。。

こんなときの対処法を紹介します。
**「こうすればもっとシンプルになるよ！」という意見あったら下さい！！！！！**

## 最初にざっくり結論
今から、実装クラスを紹介してから対処法を紹介しますが、すぐ知りたいせっかちな方のために結論を。

- パースを失敗させる
- すると、`MoyaError.objectMapping(Swift.Error, Response)` という `Error` になる
- この `Response` には `response` プロパティがある
- そして、 `response` プロパティには `statusCode: Int` があるからそれを見ればいい！！

## API の仕様

- 成功時: `status code: 200 – 204` が返ってくる
- `body: none` である
- `post` メソッド

という API です。

## 実装するクラス

まずは API Client から。
`ApiClient` という型を定義し、 `ApiClientImpl` がそれに準拠する実装クラスになっています。
Moya を使ったこと無い方はわかりにくいところもあると思いますが、

- `request` メソッドで API にリクエストする
- `Request` は `Codable` に準拠したパースしたい実体 (構造体) が入る
- 非同期で動作
- 結果は `Result<Codable, Error>` で返ってくる
- `.get`, `.post` などどんなメソッドでもこの `ApiClient` が担う

という至って普通の API Client だと思います (多分)。
ちなみに、 `Impl` は「実装」を意味する "Implementation" から来ています。

```swift:ApiClient.swift

import Moya

protocol ApiClient {
    var provider: MoyaProvider<MultiTarget> { get set }
}

class ApiClientImpl: ApiClient {
    func request<Request: ApiTargetType>(_ request: Request, completion: @escaping (Result<Request.Response, Error>) -> Void) {
        let target = MultiTarget(request)
        self.provider.request(target) { result in
            switch result {
            case .success(let response):
                let jsonDecoder = JSONDecoder()
                jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
                jsonDecoder.dateDecodingStrategy = .iso8601
                do {
                    let decodedResponse = try response.map(Request.Response.self, using: jsonDecoder)
                    completion(.success(decodedResponse))
                } catch {
                    completion(.failure(error))
                }
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}
```

次にこの `ApiClientImpl` を呼び出す repository です。

```swift:Repository.swift

import Moya

class Repository {
    var apiClient: ApiClient = ApiClientImpl()

    func post(completion: @escaping (Result<PostTarget.Response, Error>) -> Void) {
        let request = PostTarget()
        apiClient.request(request) { result in
            switch result {
            case .success:
                completion(.success(.init()))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}
```
(ネスト深いのは許してくれ。。。。)

最後に `PostTarget` です。
URL や response, Moya のメソッドなどを定義しています。
もし `GET` メソッドであれば、この中の `Response` にパースしたい実体を紐付けます。

```swift:PostTarget.swift

import Moya

struct  PostTarget: TargetType {
    typealias Response = PostEntity

    var baseURL: String {
        return "base.url"
    }

    var path: String {
        return "path/to/api"
    }

    var method: Moya.method {
        return .post
    }

    var parameters: [String: Any] {
        return ["testParam1": "testContent"]
    }
}
```

## やってはいけない対処法 (失敗例)
まず、`body: none` なのでマッピングはできません。
僕はよわよわ iOS エンジニアなので、最初以下のような空のレスポンスを定義してました。

```swift:PostEntity.swift

// この実体を空にすれば成功すると思ってた。。。。😭
struct PostEntity { }
```

response を定義した構造体を空にすれば Moya が
Moya **「あ、この request はレスポンスないんだね！ (従順)」**
なんて察してくれると思っていました。。。

が失敗。
Moya 「レスポンスがマッピングできんかったわ。失敗な。一昨日来やがれ。」
って言われました。(言われてない)

## 成功した対処法
コードからいきなり書いていきますが、後で軽く説明します。
まず、repository に以下のようなメソッドを作ります。

```swift:Repository.swift

extension Repository {
    private func isSuccess(error: Error) -> Bool {
        if case MoyaError.objectMapping(_, let response) = error {
            return 200...204 ~= response.statusCode
        } else {
            return false
        }
    }
}
```
(なにやら `response.statusCode` とやらを比較していますねぇ)

そしてこれを、repository エラーハンドリング部の内、`case .failure:` に組み込みます。

```swift:Repository.swift

// さっき書いたので細かい部分は略
apiClient.request(request) { result in
    switch result {
        case .success:
            completion(.success(.init()))
        case .failure(let error):
            if self.isSuccess(error: error) {
                completion(.success(.init()))
            } else {
                completion(.failure(error))
            }
        }
    }
}
```
これで、body が空でも、 repository がステータスコードを判定して `.success` を投げてくれるようになりました！！🎉

### 何をしているか
`isSuccess(error: Error) -> Bool` メソッドは、「**`ApiClient` がエラーを投げてきても、ステータスコードから判断して成功 or 失敗を `Bool` で返してくれるメソッド**」です。
軽く順序立てて説明してみます。(説明めっちゃ下手でごめん)

1. まず、レスポンス body が空のものをパースしようとすると、パースできない (返ってくるパース対象がない) のでエラーとなります。
2. これは、 Moya の `map` メソッドで失敗しています。
2. この場合のエラーは、 `MoyaError.objectMapping` というエラーになります。
3. なので、 `if case MoyaError.objectMapping(_, let response) = error` によって、`ApiClient` から返ってきたエラーが `MoyaError.objectMapping` かを判定します。
4. 次に、この `MoyaError.objectMapping` は `Response` 型引数を持ち、`Response` 型には `statusCode: Int` というプロパティが定義されています。
5. そして、この `statusCode` で判定すれば良いということになります。
6. 今回は、 200 から 204 に入れば成功となるので、`return 200...204 ~= response.statusCode` を返しています。

## おわり
これで**Moya を使ってレスポンスが空かつ、ステータスコードによって成功・失敗を判断する**ことができました。

でもなんか、そもそも `ApiClient` が `.failure` を返すのが気に入ってないんですよね。。。
(その場しのぎ感がすごい)
なんかフラグを変えて、ステータスコードによって判定するようにしてくれたりしないのでしょうか。。。

初めて Moya 使ったので、もっといい方法あれば教えて下さい。