Ruby で始める GraphQL Client

## これは何

* Ruby のコードから GraphQL API を叩く方法を紹介します
  * Ruby のコードで GraphQL API を作る方法は紹介しません

## GraphQL とは

GraphQL とは何か、を一言で表現するのは難しいのですが、個人的には「API を表現するプロトコルの一種」というような認識です。
REST API と比較されることが多いです。REST API で

```
curl http://example.com/users/123/posts/
```

のようにして叩くところを、GraphQL では

```
curl http://example.com/graphql -X POST --data '
  {
    user("123") {
      posts {
        title
      }
    }
  }
'
```

のように叩きます。REST API では `/users/` `/users/123/` `/users/123/posts/` のように様々な URL にリクエストを投げて目的の情報を手に入れますが、GraphQL では 1 つのエンドポイントに様々なクエリを投げ分けることで、必要な情報を全て（不必要な情報は除いて）1 度のリクエストで取得することができます。

## GraphQL API の利用方法を学ぶ

取り急ぎ GraphQL API を利用することができればよかったので、私は GraphQL 公式ガイド (https://graphql.org/learn/) の `Queries and Mutations` と `Schemas and Types` だけを読みました。やや長めのページ 2 つを読むだけで概要が理解できたのでおすすめです。

ただし注意点が 1 つあります。 **例として登場する API の意味を深く考えてはいけません。** 例えば、冒頭でこんなクエリが登場します。

```
{
  hero {
    name
  }
}
```

これに対して、サンプル API はこんなレスポンスを返しています。

```
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}
```

この例を見たとき、「hero_id を指定したわけでもないのになぜ R2-D2 を返すんだろう？」「hero_id を指定しないときは hero 全てを返す方がいいのでは？」というのが気になって、全体の理解の妨げになってしまいました。実際にはこの API はただのサンプルでしかないので、実用性などは無視して「そういうもの」と思い込む必要があります。単に「任意引数として Episode を 1 つ受け取り、Character を 1 つ返す、hero という Query」があるだけです。

## 学習用 GraphQL サーバーを立てる

上述した GraphQL 公式ガイドに出てくる謎のスターウォーズ API を再現したものが [apollographql/starwars-server](https://github.com/apollographql/starwars-server) です。このあと Ruby から叩くのに使うので、さくっと起動しておきましょう。ブラウザから http://localhost:8080/graphql にアクセスできれば起動成功です。

なお、似た名前のものに [graphql/swapi-graphql](https://github.com/graphql/swapi-graphql) がありますが、これはより本格的なスターウォーズ API の GraphQL ラッパーなので別物です。気をつけましょう。

## Ruby で GraphQL クライアントを使ってみる

Ruby 製の GraphQL Client として [github/graphql-client](https://github.com/github/graphql-client) があります。github がオーナーという安心感が凄かったので、ひとまずこれを使おうと思います。

README にも書かれていますが、以下のようにして使います。

```ruby
require "graphql/client"
require "graphql/client/http"

module SWAPI
  # http アダプターを設定
  HTTP = GraphQL::Client::HTTP.new("http://localhost:8080/graphql")

  # 上記を使って API サーバーから GraphQL Schema 情報を取得
  Schema = GraphQL::Client.load_schema(HTTP)

  # 上記を使ってクライアントを作成
  Client = GraphQL::Client.new(schema: Schema, execute: HTTP)
end

HeroQuery = SWAPI::Client.parse <<~'GRAPHQL'
 {
  hero {
    name
  }
}
GRAPHQL

result = SWAPI::Client.query(HeroQuery)
pp result.to_h
```

上記では Schema 情報をサーバーから取得していますが、以下のようにしてあらかじめローカルに保存しておき、それをロードする方法もあるそうです。

```ruby

module SWAPI
  # http アダプターを設定（上例と同じ）
  HTTP = GraphQL::Client::HTTP.new("http://localhost:8080/graphql")

  # ローカルファイルから GraphQL Schema 情報を取得
  Schema = GraphQL::Client.load_schema("path/to/schema.json")

  # 上記を使ってクライアントを作成（上例と同じ）
  Client = GraphQL::Client.new(schema: Schema, execute: HTTP)
end

# API サーバーから GraphQL Schema 情報を取得してローカルファイルに出力
GraphQL::Client.dump_schema(SWAPI::HTTP, "path/to/schema.json")

# ...
```

### 注意点

クエリに Operation Name を付ける場合、 `SWAPI::Client.query()` の引数にモジュール名のように付けて渡す必要があります。

```ruby
HeroQuery = SWAPI::Client.parse <<~'GRAPHQL'
 # NameOfHero という名前をつけた場合・・・
 query NameOfHero {
  hero {
    name
  }
}
GRAPHQL

# HeroQuery ではなく HeroQuery::NameOfHero を渡す必要がある！
result = SWAPI::Client.query(HeroQuery::NameOfHero)
```

これを忘れると、 `expected definition to be a GraphQL::Client::OperationDefinition but was GraphQL::Language::Nodes::Document (TypeError)` というエラーが起きます。


またこのため、必然的に小文字から始まる名前も付けられません（`client.rb:241:in 'const_set': wrong constant name ... (NameError)` というエラーが起きます）。

この ↓ あたりの issue は全て単にこの仕様を見落としている気がします。

* https://github.com/github/graphql-client/issues/212
* https://github.com/github/graphql-client/issues/171
* https://github.com/github/graphql-client/issues/148

README に明記されていないので私もハマってしまいました・・・（コントリビュートチャンス？）