APIKitとCodableでAPIクライアントを作る



Swift4.0から標準で`Codable(Encodable,Decodable)` というオブジェクトのシリアライズ、デシリアライズが可能になるプロトコルが登場しました。

- [SE-0166: Swift Archival & Serialization][se0166]

まだ正式リリースされていないですが早速自分の開発中のアプリで、[APIKit][apikit]と[Himotoki][himotoki]を使っていた部分を[APIKit][apikit]とSwiftの`Decodable`に書き換えてみました。ということで、[APIKit][apikit]とSwiftの`Decodable`を使ってAPIクライアントを構築していく手順を書かせていただきたいなと思います。
※[Decodable][decodable]というライブラリもあるのでここまでは`"Swiftの"`を付けていましたが、以降は全てSwift4.0での`Decodable`プロトコルと思って頂ければと..! :pray:

また、今回は[Github API v3][githubapi]の[Search repositories api](https://developer.github.com/v3/search/#search-repositories)を使って指定のキーワードで検索したリポジトリの一覧を取得してみます。

### 注意書き
※まだSwift4.0が正式リリースされていませんので一部変更の可能性があります
※APIKitの今後のバージョンアップによって対応の仕方が変わる可能性があります。現状のv3.1.2ベースでのものになります。
※Decodable(Codable)、JSONDecoderの機能や使い方といったものは割愛します。以下が参考になると思います!

- [Swift4.0 で追加される Codable](http://qiita.com/hanawat/items/f3fffac2a4ea2e8a1767)
- [Swift 4からFoundationに採用されるCodableプロトコルに感動した #wwdc2017](http://techblog.timers-inc.com/entry/swift_codable)

## 動作環境
- Xcode 9 beta
- Swift 4.0
- [APIKit][apikit] 3.1.2
- (Result)

## tl;dr
今回の内容を先に簡単にまとめておきます。詳細は次項からです。
詳細を読みたい方は[こちら](http://qiita.com/su_k/private/e822a379d41462e05e0d#request%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B1)から。

### DataParserを作る
APIKitはデフォルトでDataParserがData→JSONにパースするDataParserなのでこれを変更すべくDecodableDataParserを定義します。

```swift:DecodableDataParser
import Foundation
import APIKit

final class DecodableDataParser: DataParser {
    var contentType: String? {
        return "application/json"
    }
    
    func parse(data: Data) throws -> Any {
        return data
    }
}
```

実際にはparse関数でdataをそのまま返却するだけです。(参考: [ProtobufDataParser][rotobufdataparser])

### GitHubRequestを作る
Requestに準拠したGitHubRequestを定義します。Request.ResponseがDecodableに準拠することと、先ほど定義したDecodableDataParserが肝になります

```swift:GitHubRequest
import Foundation
import APIKit

protocol GitHubRequest: Request {
    
}

extension GitHubRequest {
    var baseURL: URL {
        return URL(string: "https://api.github.com")!
    }
}

extension GitHubRequest where Response: Decodable {
    var dataParser: DataParser {
        return DecodableDataParser()
    }
    
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        guard let data = object as? Data else {
            throw ResponseError.unexpectedObject(object)
        }
        return try JSONDecoder().decode(Response.self, from: data)
    }
}
```

### Responseを定義する
[Github API v3][githubapi]の[Search repositories api](https://developer.github.com/v3/search/#search-repositories)のResponseを以下のように定義します。Decodableに準拠させます。

```swift
struct SearchRepositoriesResponse: Decodable {
    let items: [Repository]
}

struct Repository: Decodable {
    let id: Int
    let fullName: String
    
    private enum CodingKeys: String, CodingKey {
        case id
        case fullName = "full_name"
    }
}
```

これで、GitHubRequestのResponseがDecodableに準拠する場合はJSONDecoderを用いてDataからResponseの型にデコードされるようになります。

### SearchRepositories Requestを定義する
```swift
final class GitHubAPI {
    private init() {}
    
    struct SearchRepositories: GitHubRequest {
        typealias Response = SearchRepositoriesResponse
        
        let method: HTTPMethod = .get
        let path: String = "/search/repositories"
        var parameters: Any? {
            return ["q": query, "page": 1]
        }
        
        let query: String
    }
}
```

GitHubRequestに準拠したSearchRepositoriesリクエストを定義します。
※Paginationに関しては今回は考慮しないです。

### 実行する

```swift
import Foundation
import APIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        Session.send(GitHubAPI.SearchRepositories(query: "rxswift")) { result in
            switch result {
            case .success(let response):
                print(response)
            case .failure(let error):
                print(error)
            }
        }
    }
}
```

:clap_tone1: 





<br />

---

## Requestを定義する(1)
まずは、次のようにベースとなる **GitHubRequest** を定義してみます。

```swift:GitHubRequest
import Foundation
import APIKit

protocol GitHubRequest: Request {

}

extension GitHubRequest {
    var baseURL: URL {
        return URL(string: "https://api.github.com")!
    }
}

extension GitHubRequest where Response: Decodable {
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        // ここをあとで書き換える
    }
}
```

一旦、 `Response`が`Decodable`に準拠している場合のextensionを追加して、現段階では仮の状態ですが`response(from:, urlResponse:)`を定義しておきます。


## DataParserを作る
今回の重要なポイントになります。
[APIKit][apikit]は通信のレスポンス(のData)を受け取った時にデフォルトで[JSONDataParser][jsondataparser]を使うようになっています。
なので、そのままだと先で定義した`response(from:, urlResponse:)`の第一引数のAnyにはデシリアライズされたJSONが渡されてきます。なので...

```swift
extension GitHubRequest where Response: Decodable {
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        let data = try JSONSerialization.data(withJSONObject: object, options: [])
        return try JSONDecoder().decode(Response.self, from: data)
    }
}
```

**と実装してしまいがち**なのですが、これだと一度Dataを`JSONSerialization`でデシリアライズされたものをまたシリアライズして`JSONDecoder`でデコードしてとかなり非効率になってしまうので、 **DecodableDataParser**なるものを作ります。

```swift:DecodableDataParser
import Foundation
import APIKit

final class DecodableDataParser: DataParser {
    var contentType: String? {
        return "application/json"
    }
    
    func parse(data: Data) throws -> Any {
        return data
    }
}
```

ここでは単純にDataParserプロトコルに準拠するように定義をし、`parse(data:)`関数ではそのままDataを返却するようにします。
この定義の仕方は、[ProtobufDataParser][rotobufdataparser]を参考にしました。

この定義した`DecodableDataParser`を使って、先程までのGitHubRequestを書き換えます

```swift
extension GitHubRequest where Response: Decodable {
    var dataParser: DataParser {
        return DecodableDataParser()
    }
    
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        guard let data = object as? Data else {
            throw ResponseError.unexpectedObject(object)
        }
        return try JSONDecoder().decode(Response.self, from: data)
    }
}
```

これで、`response(from:, urlResponse:)`に渡ってくる`object`がDataになるので、それをそのままJSONDecoderにかけてデコードします。(objectの型がAnyになっているものの、Parserで受け取ったDataをそのまま流しているので強気に`try JSONDecoder().decode(Response.self, from: object as! Data)` でも良いのですが、念のためguard節を入れています)


ここまでで一旦、ベースとなるGithubRequestが定義できました。あとは`Decodable`に準拠したResponseを作成し、GithubRequestに準拠したSearchRepositoriesRequestを定義します。

### 余談(将来できそうなこと)
現在開発が進められているAPIKitの`develop/4.0`ブランチだと、[DataParser](https://github.com/ishkawa/APIKit/blob/develop/4.0/Sources/APIKit/DataParser/DataParser.swift)の定義が変わっており、以下のように今後定義することができるようになった場合は、`response(from:, urlResponse:)`の第一引数がAnyから`DataParser.Parsed`、つまりこの場合では **Data** になるので余計なチェックが不要になります。期待ですね。

```swift:(引用)APIKit`develop/4.0`でのDataParser,Request

/// `DataParser` protocol provides inteface to parse HTTP response body and to state Content-Type to accept.
public protocol DataParser {
    associatedtype Parsed

    /// Value for `Accept` header field of HTTP request.
    var contentType: String? { get }

    /// Return `Any` that expresses structure of response such as JSON and XML.
    /// - Throws: `Error` when parser encountered invalid format data.
    func parse(data: Data) throws -> Parsed
}

public protocol Request {
    /// Build `Response` instance from raw response object. This method is called after
    /// `intercept(object:urlResponse:)` if it does not throw any error.
    /// - Throws: `Error`
    func response(from object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> Response
}
```

```swift
final class DecodableDataParser: DataParser {
    typealias Parsed = Data
    var contentType: String? {
        return "application/json"
    }
    
    func parse(data: Data) throws -> Parsed {
        return data
    }
}

extension GitHubRequest where Response: Decodable {
    var dataParser: DataParser {
        return DecodableDataParser()
    }

    func response(from object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> Response {
        // 今回のケースでは、DataParser.Parsed == Dataになるので先述のDataへのキャストが不要になる
        return try JSONDecoder().decode(Response.self, from: object) 
    }
}
```

まだまだ変わる可能性はありますが、より便利になりますね! :blush:

## Responseを定義する
さて、一旦Requestから離れてResponseの定義に移ります。
今回、[Search repositories api](https://developer.github.com/v3/search/#search-repositories)を使用するので、以下のようにResponseを定義してみます。
今回はシンプルにするため、パースする要素を最小限にしています。

```json:(引用)jsonのexample
{
  "total_count": 40,
  "incomplete_results": false,
  "items": [
    {
      "id": 3081286,
      "name": "Tetris",
      "full_name": "dtrupenn/Tetris",
      "owner": {
        "login": "dtrupenn",
        "id": 872147,
        "avatar_url": "https://secure.gravatar.com/avatar/e7956084e75f239de85d3a31bc172ace?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png",
        "gravatar_id": "",
        "url": "https://api.github.com/users/dtrupenn",
        "received_events_url": "https://api.github.com/users/dtrupenn/received_events",
        "type": "User"
      },
      "private": false,
      "html_url": "https://github.com/dtrupenn/Tetris",
      "description": "A C implementation of Tetris using Pennsim through LC4",
      "fork": false,
      "url": "https://api.github.com/repos/dtrupenn/Tetris",
      "created_at": "2012-01-01T00:31:50Z",
      "updated_at": "2013-01-05T17:58:47Z",
      "pushed_at": "2012-01-01T00:37:02Z",
      "homepage": "",
      "size": 524,
      "stargazers_count": 1,
      "watchers_count": 1,
      "language": "Assembly",
      "forks_count": 0,
      "open_issues_count": 0,
      "master_branch": "master",
      "default_branch": "master",
      "score": 10.309712
    }
  ]
}
```

```swift:上記jsonを基に作成したResponse
struct SearchRepositoriesResponse: Decodable {
    let items: [Repository]
}

struct Repository: Decodable {
    let id: Int
    let fullName: String
    
    private enum CodingKeys: String, CodingKey {
        case id
        case fullName = "full_name"
    }
}
```

とりあえず最小限の構成でResponseを定義できました。

## Requestを定義する(2)
Requestに対応するResponseが定義できたので、`SearchRepositories` Requestを定義します。
特筆すべき点はないのでスッと定義できると思います。

```swift
final class GitHubAPI {
    private init() {}
    
    struct SearchRepositories: GitHubRequest {
        typealias Response = SearchRepositoriesResponse
        
        let method: HTTPMethod = .get
        let path: String = "/search/repositories"
        var parameters: Any? {
            return ["q": query, "page": 1]
        }
        
        let query: String
    }
}
```

個人的にはいつも`GithubAPI`みたいなnamespaceで括るのが好きなので括っていますがお好みで!
**※尚、今回はPaginationについては一旦考えないものとします。** なので、常に最初のx件だけ取得という形になります。
Paginationを考えるなら、`let page: Int`も加えてあげて可変になるようにしてあげると良いです!


## 通信してみる
無事にRequest/Responseを定義できたので、通信してみます。実際には結果を取得してUIに反映させるのが実際の流れになりますが、
ひとまず「rxswift」でリポジトリ検索した結果を`print`で出力してみます

```swift
import Foundation
import APIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        Session.send(GitHubAPI.SearchRepositories(query: "rxswift")) { result in
            switch result {
            case .success(let response):
                print(response)
            case .failure(let error):
                print(error)
            }
        }
    }
}

```


ここまででシンプルな形で[APIKit][apikit]と`Decodable`プロトコルを使ったAPIクライアントが設計できました。 :clap_tone1: 

## 応用: Responseに応じてJSONDecoderの各種strategyを変えられるようにする
ただ、このままではJSONDecoderの各種strategyが変更できず固定のままになってしまうので、Responseに応じて変えられるようにします。
(同じサービスのAPIでDateのフォーマットがコロコロ変わったりすることは稀だと思いますが...)

試しに、`Repository`に`createdAt`を追加します。

```swift
struct Repository: Decodable {
    let id: Int
    let fullName: String
    let createdAt: Date
    
    private enum CodingKeys: String, CodingKey {
        case id
        case fullName = "full_name"
        case createdAt = "created_at"
    }
}
```

再度実行してみると、次のようなエラーがでます


> responseError(Swift.DecodingError.typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [Optional(xxxx.SearchRepositoriesResponse.(CodingKeys in xxxx).items), nil, Optional(xxxx.Repository.(CodingKeys in xxxx).createdAt)], debugDescription: "Expected to decode Double but found a string/data instead.")))

*"Expected to decode Double but found a string/data instead."*

GitHub APIではDateの部分は `"2012-01-01T00:31:50Z"`　のように ISO8601規格に則ったフォーマットの文字列で渡ってくるため、JSONDecoderの`dateDecodingStrategy`を`.iso8601`に変えないとパースできず、DecodingError.typeMismatchエラーが返却されてしまいます。

- 参考: [Swift4のCodableでISO8601の日付をデコードする](http://qiita.com/rizumita/items/88c38f5671e0627c5df6)

なので、GitHubRequestのデコード部分を変更します。

```swift
extension GitHubRequest where Response: Decodable {
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        guard let data = object as? Data else {
            throw ResponseError.unexpectedObject(object)
        }
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601 // dateDecodingStrategyをISO8601に変更する
        return try decoder.decode(Response.self, from: data)
    }
}
```

再度実行すると無事に成功しますが、これだと全てのGitHubRequestのResponseに対してデコードする時にDateはISO8601フォーマットでデコードするよ！となってしまいます :thinking: 

なのでResponse毎に各種strategyの指定があれば、JSONDecoderの各種strategyを変更できるようにします。
まずは`CustomDecodingStrategy`なるプロトコルを定義します。

```swift
protocol CustomDecodingStrategy {
    typealias Strategies = (
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy,
        dataDecodingStrategy: JSONDecoder.DataDecodingStrategy,
        nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy
    )
    static var decodingStrategies: (Strategies) { get }
}
```

次に、GitHubRequestに定義を追加していきます。併せて少し書き方も変えていきます。


```swift
// protocolの宣言時点で、ResponseがDecodableに準拠するという制約をつける
protocol GitHubRequest: Request where Response: Decodable {
    var decoder: JSONDecoder { get } // 今回追加
}

extension GitHubRequest {
    var baseURL: URL {
        return URL(string: "https://api.github.com")!
    }
    
    var dataParser: DataParser {
        return DecodableDataParser()
    }
    
    // 今回追加
    var decoder: JSONDecoder {
        return JSONDecoder()
    }
    
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
        guard let data = object as? Data else {
            throw ResponseError.unexpectedObject(object)
        }
        return try decoder.decode(Response.self, from: data)
    }
}

// 今回追加
extension GitHubRequest where Response: CustomDecodingStrategy {
    var decoder: JSONDecoder {
        let decoder = JSONDecoder()
        let strategies = Response.decodingStrategies
        decoder.dateDecodingStrategy = strategies.dateDecodingStrategy
        decoder.dataDecodingStrategy = strategies.dataDecodingStrategy
        decoder.nonConformingFloatDecodingStrategy = strategies.nonConformingFloatDecodingStrategy
        return decoder
    }
}
```

`var decoder: JSONDecoder { get }`というのを加えた関係で、GitHubRequestプロトコルに準拠させるにはResponseがDecodableに準拠するのが必須になりますが、そこまでややこしくはならないと思います。Requestによって[Himotoki][himotoki]やObjectMapperを使う...と混ざらない限りは。

更に、Responseが先ほどの`CustomDecodingStrategy`に準拠していた場合は、`JSONDecoder`の生成する時にResponseに定義されたstrategyを反映させるようにしました。

あとは、RepositoryをCustomDecodingStrategyに準拠させるだけです。

```swift
struct Repository: Decodable, CustomDecodingStrategy {
    let id: Int
    let fullName: String
    let createdAt: Date
    
    private enum CodingKeys: String, CodingKey {
        case id
        case fullName = "full_name"
        case createdAt = "created_at"
    }
    
    static var decodingStrategies: Strategies {
        return (.iso8601, .base64Decode, .throw)
    }
}
```

また、今回はそのRepositoryを配列で受ける`SearchRepositoriesResponse`をResponseとしているので、こちらも同様に`CustomDecodingStrategy`に準拠させます。
ややスッキリしないですが、Repositoryの`decodingStrategies`をそのまま返却します。

```swift
struct SearchRepositoriesResponse: Decodable, CustomDecodingStrategy {
    let items: [Repository]
    
    // RepositoryのdecodingStrategiesをそのまま返却する
    static var decodingStrategies: Strategies {
        return Repository.decodingStrategies
    }
}
```

これでResponse毎にJSONDecoderの各種strategyを変更できるようになりました!

もし1つのResponseモデルの中で複数の要素のDateのフォーマットが異なる場合は、Decodableの`init(from decoder: Decoder)`を実装しないといけないですが。。

基本的に、例えば今回のようにGitHub apiを使うとなった時に一貫してJSONDecoderのstrategyを設定できる状況だったら、この項目の最初に示したように直指定でもよいかもしれません。

1つの解としてみてもらえればと思います :pray_tone1: 

## 更に先へ
実際には[RxPagination][rxpagination]のようにPaginationに対応したRequest/Responseを使ったりするケースもあると思いますので、今回のサンプルよりは考えることが複雑になってくると思いますが、同じようにDecodableを使って構築していくことは可能だと思います。
後日サンプル交えて記事書けたら書きたいなと思います :pray_tone1: 

## さいごに
サンプルを[こちら](https://github.com/sgr-ksmt/APIKitWithDecodableSample)にあげています。


<!-- -->
[apikit]:https://github.com/ishkawa/APIKit
[himotoki]:https://github.com/ikesyo/Himotoki
[rxpagination]:https://github.com/tryswift/RxPagination
[githubapi]:https://developer.github.com/v3/
[se0166]:https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md
[decodable]:https://github.com/Anviking/Decodable
[jsondataparser]:https://github.com/ishkawa/APIKit/blob/master/Sources/APIKit/DataParser/JSONDataParser.swift
[rotobufdataparser]:https://github.com/ishkawa/APIKit/blob/master/Sources/APIKit/DataParser/ProtobufDataParser.swift
<!-- -->