Swift 用 graphql-codegen plugin の graphql-codegen-swift-operations を作った

この記事は [GraphQL Advent Calendar 2020](https://qiita.com/advent-calendar/2020/graphql) 9 日目の記事です。
前回の記事は @shinnoki さんの [2021年にGraphQL Code Generatorを使うなら、TypedDocumentNodeを使おう](https://qiita.com/shinnoki/items/576277184de0e89cab97) でした。

## GraphQL クライアントライブラリは必須ではない

GraphQL は一般的には HTTP 上で JSON をやりとりする形で実装されます。
つまり、HTTP をしゃべることと JSON の parse さえできればそれで十分です。
言語によってはこれらはライブラリを使わずとも実現できるでしょう。

GraphQL クライアントライブラリは主に cache マネジメントのために用いられます。
つまり、cache をほとんど活用しないアプリケーションであれば GraphQL クライアントの必要性はほとんどありません。

## TypeScript の GraphQL operation 用 codegen 事情

クライアントが必要ないとは言ったものの、レスポンスの型に対応する codegen が欲しくなりはします。
ここではもっともツールが揃っている TypeScript を例に示します。

GraphQL は型機能を提供しており、GraphQL schema と operation (query, mutation, subscription, fragment) から型が決定できます。

```graphql
type Query {
  me: User!
}

type User {
  id: ID!
  name: String!
}
```

という schema があり、

```graphql
query {
  me { name }
}
```

という operation があるとすると、そのレスポンスの型は TypeScript で書くと次のようになるでしょう。

```typescript
interface Data {
  me: { name: string }
}
```

そして次のように利用することができます。

```typescript
const res = await fetch("/graphql", {method: "POST", body: JSON.stringify({query: "query { me { name } }"})});
const data: Data = (await res.json()).data;
```

クライアントが必要ないとは言うものの、operation (`query { me { name } }`) から、この `Data` という型を生成できると、operation に対して返ってくるべきレスポンスの型を間違えてないで済むため便利そうです。
TypeScript であれば、graphql-codegen の typescript-operations がこれに該当します。

https://graphql-code-generator.com/docs/plugins/typescript-operations

## Swift の GraphQL operation 用 codegen 事情

Swift 向けの codegen は、以下の2つがあるようです。

- https://github.com/apollographql/apollo-ios
- https://github.com/Shopify/graphql_swift_gen

apollo-ios が生成するコードはあくまで apollo 前提で、apollo の機能を使わないのであればあまりうれしくありません。
graphql_swift_gen は schema に対して生成するようで、operation (query, mutation, subscription, fragment) に対して生成してくれるわけではないようです。
GraphQL はクライアントの operation の中身に対応してレスポンスが変化するため、これでは型の恩恵がイマイチ得られません。

先程の例でいくと、

```graphql
type Query {
  me: User!
}

type User {
  id: ID!
  name: String!
}
```

という定義に直接対応する型を生成しても、

```typescript
interface Query {
  me: User
}

interface User {
  id: string
  name: string
}
```

この `User` のもつ field すべてが返ってくるとは限りません。

```graphql
query {
  me { name }
}
```

のような operation に対して生成しなければならないのです。

そこで、GraphQL Codegen の swift かつ operation 用 plugin として graphql-codegen-swift-operations を作ってみました。

https://github.com/mtsmfm/graphql-codegen-swift-operations

## graphql-codegen-swift-operations

具体的な使い方の例は https://github.com/mtsmfm/graphql-codegen-swift-operations/tree/master/swift-test-project にあります。

まず、GraphQL Codegen のチュートリアル通りにセットアップします。

https://graphql-code-generator.com/docs/getting-started/installation

次に、`npm install --save-dev @mtsmfm/graphql-codegen-swift-operations` して、
codegen.yml で generated.swift を `@mtsmfm/graphql-codegen-swift-operations` を使って生成するようにします。

```yaml
schema: ./schema.graphql
documents: 'graphql/*.graphql'
generates:
  Sources/app/generated.swift:
    - '@mtsmfm/graphql-codegen-swift-operations'
```

https://github.com/mtsmfm/graphql-codegen-swift-operations/blob/master/swift-test-project/codegen.yml

あとは operation を書きます。

```graphql
query AppQuery {
  ...
}
```

https://github.com/mtsmfm/graphql-codegen-swift-operations/blob/master/swift-test-project/graphql/query.graphql

そして `$(npm bin)/graphql-codegen` して graphql-codegen を実行すると、generated.swift が生成されます。

```swift
class AppQuery: Decodable {
  let data: Data?;
  let errors: Errors?;
  public static let operationDefinition: String =
    """
    query AppQuery {
      ...
    }
    """

  private enum CodingKeys: String, CodingKey {
    case data
    case errors
  }

  required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.data = try container.decode(Data.self, forKey: .data)
    self.errors = try container.decodeIfPresent(Errors.self, forKey: .errors)
  }


  struct Data: Decodable {
    let organizations: [Internal_1_Organizations]
    ...
  }
}
```

https://github.com/mtsmfm/graphql-codegen-swift-operations/blob/master/swift-test-project/Sources/app/generated.swift

あとは `AppQuery.operationDefinition` を query としてつかって POST しつつ、`JSONDecoder` と `AppQuery.self` を使って結果を取り出せばよいです。

```swift
let client = SynchronousHTTPClient()
do {
  let decoder = JSONDecoder()
  let data = try client.post(url: "http://localhost:4000/graphql", json: ["query": AppQuery.operationDefinition])
  let result = try decoder.decode(AppQuery.self, from: data)
  if let data = result.data {
    for org in data.organizations {
      print(org as Any)
    }
  }
} catch let error {
  ...
}
```

## まとめ

GraphQL を叩くときにクライアントライブラリ選定から入ることが多いかもしれませんが、そもそも叩くだけなら必須ではないことを念頭に置くと違った視点が見えるかもしれません。
また、GraphQL Codegen の plugin を作るときは、

1. schema に対してではなく schema + operation に対して吐く
2. 特定のクライアントライブラリに依存しない

という点に留意して作るとよさそうです。