Web APIが何もわからない状態でsmash.gg APIを用いて配信台の試合の選手名を取得するまで【GraphQL】

# やりたいこと
smash.ggで配信台に設定した試合の選手情報をjsonファイルで取得したい。
スコアボードに名前を入力する手間を省きたい。

# 私のプログラミング能力と想定読者
私は非エンジニアで、WebAPIが何かよくわからないところからスタートします。
HTMLとCSSが少し書けて、JS・Java・Pythonのコードや参考書をちょっと読んだことがある程度
です。

この記事はあくまで私の備忘録です。
各技術の正しい認識に基づいた解説書ではありません。

これを記事化した主な目的は、私と同じくらいの初学者で私と同じことをしたい人が参考文献を拾ったり、普段プログラミングに慣れ親しんだ人が私を哀れんで参考になる情報を与えてくれたりすることを期待するためです。

# 前提知識
### Web APIとは
こちらの記事を参考にしました。流れを掴む助けになりました。

[WebAPIについての説明](https://qiita.com/busyoumono99/items/9b5ffd35dd521bafce47)

ざっくり要約します。
普段私たちがブラウザを使ってWebサイトを閲覧しています。
それと同様に、どうやらWeb APIを用いれば、適切な方法でWebサーバーにリクエストを飛ばすと、それに応じて必要な情報を返してくれるそうです。
これを用いれば、人間がブラウザを開いて情報をコピペする手間を省くことができそうです。

### GraphQLとは
先ほどのWeb APIの説明であった「適切な方法でWebサーバーにリクエストを飛ばすと」の部分に関わるものです。
ここで言う適切な方法でのリクエストのことを、 "クエリ" と呼ぶそうです。
クエリの書き方・様式のひとつをGraphQLと呼ぶそうです。
RESTという別の様式が主流だったようですが、GraphQLは欲しい情報をピンポイントで取得することに長けた様式とのことで、GraphQLを用いるサービスが増えているそうです。
smash.ggのAPIもGraphQLを用いています。

### JSONとは
一時期Twitterで話題になっていたので名前だけ知っていました。

[「JSONという気味の悪い拡張子」“誤訳”で物議　東洋経済オンラインの記事修正「原文とかい離していた」](https://www.itmedia.co.jp/news/articles/1806/08/news065.html)


こちらの記事を参考にしました。

[第1回 進化するフォーマット｜CSV、XML、JSON…データフォーマットの変遷について考える](https://www.gixo.jp/blog/3965/)

JSONとは、データを扱うファイルの形式です。
必要なデータをPC間で送受信したり、PC内のプログラム間で読み書きしたりするためにデータを扱いやすくしたファイル形式です。
APIでリクエストしたデータが送られてくる際に、ただのテキストで送られてくるよりも、JSONファイルの方がプログラム側が扱いやすくて都合がいいそうです。
今回はこのデータフォーマットを用い、smash.gg上の選手情報を受け取ります。

### smash.ggとは
[smash.gg](https://smash.gg)

Espotsコミュニティを強化するトーナメント管理Webサービスです。
様々な形式のゲームのトーナメントを管理することができます。
また、募集・管理・運営を一本化するすることが可能です。


# やったこと
なんとsmash.ggが公式にイントロダクションを用意してくれています。
ここの手順に沿って手を動かせば全てが解決します。
[smash.gg Developer Portal](https://developer.smash.gg)

![スクリーンショット 2020-07-24 9.34.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636895/43e4266b-fc78-5a41-eb97-989ccbf93358.png)

GET STARTEDをクリックすると案内が始まります。
適当にページを送ると[smash.ggのDiscord](https://discord.gg/smashgg)に参加することを推奨されたり、GraphQLの紹介をされたりします。

### トークンによる認証

ページを送ると、APIを用いたリクエストをするには認証が必要であることを知らされます。
[Authentication smash.gg Developer Portal](https://developer.smash.gg/docs/authentication)

こちらも画面の指示に従ってトークンを取得すればOKです。

[トークン取得ページ](https://smash.gg/admin/user/782f51a2/developer)

トークンがないとAPIによるリクエストができません。
またトークンはコピペして保管しておく必要があります。

### リクエストのテスト

[次のページ](https://developer.smash.gg/docs/testing-your-requests)に送ると、リクエストをテストすることを推奨されます。
[API Explorer](https://developer.smash.gg/explorer/)を用いることで、リクエストのテストが可能です。
やっと本題に近づいてきました。

こちらがAPI Explorerの画面です。
![スクリーンショット 2020-07-24 9.41.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636895/f8bf3fa5-d58f-0bb7-31e1-8dee75741625.png)
左側にリクエストを書き、左上の再生ボタンをクリックすると、右側にリクエストした情報が表示されます。
左側の情報はGraphQLの様式で記入します。
右側の情報はJSONの形式で表示されます。

利用するには、左上の "Auth token" の部分にトークンを入力しておく必要があります。

公式イントロダクションを2ページほど送ると、使用例が載っています。

[Benefit of GraphQL (使用例)](https://developer.smash.gg/docs/benefits-of-gql)

「EVO2018の各タイトルTop3を出力するリクエストを書いてみましょう」というものです。

API Explorerの左側にこちらをコピペします。

```GraphQL:ExampleRequest
query TournamentQuery($slug: String, $page: Int!, $perPage: Int!) {
  tournament(slug: $slug) {
    events {
      name
      standings(query: { page: $page, perPage: $perPage }) {
        nodes {
          standing
          entrant {
            name
          }
        }
      }
    }
  }
}
```

また、API Explorerの左下に "QUERY VARIABLES" と書いてある部分をドラッグして持ち上げると、新たなテキストボックスが現れます。
![スクリーンショット 2020-07-24 10.04.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636895/2ae06253-2c1a-bf28-9f93-2d9a8d473056.png)

ここには、左上にコピペしたリクエスト文の中の変数部分に入る情報を入力します。
以下をコピペします。

```GraphQL:ExampleRequestVariables
{
  "slug": "evo2018",
  "page": 1,
  "perPage": 3
}
```

この状態で左上の再生ボタンを押すと、API Explorerの右側に以下のJSONデータが現れます。

```JSON:出力
{
  "data": {
    "tournament": {
      "events": [
        {
          "name": "DRAGON BALL FighterZ",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "Echo Fox | SonicFox"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "CAG | GO1"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "CAG | Fenritti"
                }
              }
            ]
          }
        },
        {
          "name": "Street Fighter V: Arcade Edition",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "Mousesports | Problem-X"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "Echo Fox | Tokido"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "CYG BST | | Fuudo"
                }
              }
            ]
          }
        },
        {
          "name": "TEKKEN 7",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "Fursan | LowHigh"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "UYU | qudans"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "ITS | | Lil Majin"
                }
              }
            ]
          }
        },
        {
          "name": "Super Smash Bros. for Wii U",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "Lima"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "CaptainZack"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "DNG | Nietono"
                }
              }
            ]
          }
        },
        {
          "name": "Super Smash Bros. Melee",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "TSM | Leffen"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "[A] | Armada"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "PG | Plup"
                }
              }
            ]
          }
        },
        {
          "name": "BlazBlue: Cross Tag Battle",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "heiho"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "PAG|HELLA | Fame96"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "GRPT|BE | DORA_BANG"
                }
              }
            ]
          }
        },
        {
          "name": "Guilty Gear Xrd REV2",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "OMITO"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "SURUGAYA | Machabo"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "WongNation | LostSoul"
                }
              }
            ]
          }
        },
        {
          "name": "Injustice 2",
          "standings": {
            "nodes": [
              {
                "standing": 1,
                "entrant": {
                  "name": "Noble | Rewind"
                }
              },
              {
                "standing": 2,
                "entrant": {
                  "name": "Noble | Tweedy"
                }
              },
              {
                "standing": 3,
                "entrant": {
                  "name": "Echo Fox | SonicFox"
                }
              }
            ]
          }
        }
      ]
    }
  },
  "extensions": {
    "cacheControl": {
      "version": 1,
      "hints": [
        {
          "path": [
            "tournament"
          ],
          "maxAge": 300,
          "scope": "PRIVATE"
        }
      ]
    },
    "queryComplexity": 33
  },
  "actionRecords": []
}
```

ここまでくると、なんとなくできることが見えてきたような気がしてきます。
Request Variablesの大会名(slug)や表示数(perPage)を変えれば出力も変わります。

### 本題: 配信台の試合のプレイヤー名を取得したい
さて、本題の「配信台に設定した試合の選手情報を取得」に入ります。
このページに、配信台に関する情報を取得するためのGraphQLの書き方が載っています。

[Stream Queue](https://developer.smash.gg/docs/examples/queries/stream-queue#!)

本題に入る前に、smash.ggのAPIを用いる上でGraphQLで書いていいこと／書けることのルールを把握する必要があります。
当然ですが、なんでも際限なく情報を引っ張ってくることはできないようです。

先ほどのExampleRequestを読むとなんとなくわかる気がしますが、GraphQLの文は入れ子構造になっています。

```GraphQL:ExampleRequest
query TournamentQuery($slug: String, $page: Int!, $perPage: Int!) {
  tournament(slug: $slug) {
    events {
      name
      standings(query: { page: $page, perPage: $perPage }) {
        nodes {
          standing
          entrant {
            name
          }
        }
      }
    }
  }
}
```
[[[入れ子]]]

tournament
┗ events
....┣ name
....┗ standings
........┗ nodes
............┣ standing
............┗ entrant
................┗ name

tournamentの中にeventsがあり、eventsの中にはname(イベント名)とstanding(順位)があり……、といった構造です。
この入れ子のルール、つまり「この項目は、これとこれの項目を内部に持てる」といった情報は、API Explorer画面右上メニューの "Schema" をクリックすると見ることができます。

[Schema](https://developer.smash.gg/reference/query.doc.html)


Schema(スキーマ)とは、smash.ggでいう各項目 (tournament, eventsなど) のことを指しているようです（ここちゃんと理解していないです。違うかもしれません）。ここで解説されています。

[GraphQLのスキーマと型定義](https://qiita.com/NagaokaKenichi/items/d341dc092012e05d6606)

smash.ggのSchemaのページには左側にメニューがあります。
クリックするとその項目の詳細が見られます。

[StreamQueueInfo](https://developer.smash.gg/reference/query.doc.html)

このページは "StreamQueueInfo" という項目のページです。
 "StreamQueueInfo" を選んだ理由は、私が求める情報を最も与えてくれそうな項目だからです。
![スクリーンショット 2020-07-24 10.51.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636895/5479b61e-5144-b3e8-5035-a2e78543d497.png)

このページのGraphQL Schema definitionの部分には、"StreamQueueInfo" というスキーマの定義が書かれています。

```GraphQL:StreamQueueInfo
type StreamQueueInfo {
stream: Streams
sets: [Set]
}
```
これによると、 "StreamQueueInfo" というスキーマは "stream(配信先情報)" と "sets(試合情報)" を内包することが定義づけられています。

またこのページのRequired Byの部分には、"Query" と "Tournament" と書かれています。
これは、 "StreamQueueInfo" が "Query" と "Tournament" に内包されることを表しています。


さて、Schemaの入れ子構造を辿って配信台の試合のプレイヤーの名前を取得するためには、 "stream(配信先情報)" と "sets(試合情報)" のうち、求める情報に近そうな方を入れ子の親として進んでいく必要があります。

"StreamQueueInfo"のページの "Set" の部分はクリックできるようになっています。
ここをクリックすると、 ["Set"](https://developer.smash.gg/reference/set.doc.html) という別のスキーマのページに飛びます。
"Set" は、"startedAt(試合開始時刻)" や "slots(参加者情報)" などを内包します。
"slots(参加者情報)" が求める情報に最も近そうなので、slots(クリックできるスキーマ名はSetSlot)をクリックします。
このようにして、入れ子構造を奥へ奥へと進んでいくことができます。

最終的に配信台の選手情報を得るまでに、以下の入れ子構造を辿る必要がありました。
Tournament → StreamQueue → Sets → Slots → Entrants → Name


### 実際にプレイヤー名の情報を取得する

smash.gg上で架空の大会ページを用意し、2名からなるトーナメントを作成しました。
[大会ページ](https://smash.gg/tournament/api-test-1/event/special-1on1-ultimate-singles/brackets/843223/1348740)
![スクリーンショット 2020-07-24 11.24.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/636895/71e62fd0-9e7d-06b3-be0d-bd89518e2c3e.png)

先ほどの入れ子構造 (Tournament → StreamQueue → Sets → Slots → Entrants → Name) をGraphQLの文に見様見真似で反映させたものが以下。
[StreamQueueInfo](https://developer.smash.gg/reference/query.doc.html)のページを参考にしています。

```GraphQL:配信台プレイヤー名をリクエストするGraphQL
query StreamQueueOnTournament($tourneySlug: String!) {
  tournament(slug: $tourneySlug) {
    id
    streamQueue {
      sets {
        id
        slots {
          entrant {
             name
          }
        }
      }
    }
  }
}
```

```GraphQL:QueryVariables
{
  "tourneySlug": "tournament/api-test-1"
}
```

これらとトークンをAPI Explorerに入力し、再生ボタンを押して出力したJSONが以下。

```JSON:出力
{
  "data": {
    "tournament": {
      "id": 234359,
      "streamQueue": [
        {
          "sets": [
            {
              "id": 30841665,
              "slots": [
                {
                  "entrant": {
                    "name": "EGS | Tempra"
                  }
                },
                {
                  "entrant": {
                    "name": "EGS | Staff account A"
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  },
  "extensions": {
    "cacheControl": {
      "version": 1,
      "hints": [
        {
          "path": [
            "tournament"
          ],
          "maxAge": 300,
          "scope": "PRIVATE"
        }
      ]
    },
    "queryComplexity": 6
  },
  "actionRecords": []
}
```

一応これで欲しい情報を引っ張り出すことには成功しました。
お疲れ様です。

# おわりに

これをツールとして実装するためには、どうにかして

- API Explorerではなくプログラムからリクエストを飛ばし、帰ってきたJSONを保存したり
- JSONを読み込んでプレイヤー名をプログラムで読み出したり


といったことをする必要があると思いますが、まだよく分かっていないのでこの辺で。


ここまで読んでくださりありがとうございました。
詳しい方は何でもいいので参考リンクなど教えてくだされば幸いです。不躾でごめんなさい・・・