PaizaIO API でコードジャッジシステムを作ってみた

この記事は、[ニフティグループ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nifty) 21日目の記事です。
昨日は[@D_W](https://qiita.com/D_W)さんで、「[GitHub Actionsでプルリク時にStoryBookをS3にデプロイする](https://qiita.com/D_W/items/50c144f1c37042d485a0)」でした。
StoryBookはまだ利用したことがなかったので、機会があれば参考にさせていただきます！

# はじめに
社内のReact勉強会で「Reactを用いて何か作ってみよう」というアウトプット回があったので、その時間を使ってコードジャッジシステムを作成してみました。
ソースコードに対して、予め用意したテストケースを入力として与え、期待する結果と等しいか判定するシステムです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911202/7d51ccda-b17d-e3a7-a712-ef570994133f.png)

コードジャッジシステムを自作しようとすると、多言語対応したソースコードの実行処理が大変ですが、[Paiza IO API](http://api.paiza.io/docs/swagger/#!/runners)を使うととても簡単にコード実行処理を実装できます。
この記事では、多言語対応のコード実行処理を簡単に実装できる [Paiza IO API](http://api.paiza.io/docs/swagger/#!/runners) について解説します。



# Paiza IO API について
Paiza IO APIでは以下3つのAPIが利用可能です。

- `create`：任意のソースコードをAPIに投げて実行
- `get_status`：投げたコードの実行状況を取得
- `get_details`：投げたソースコードの実行結果を取得

それぞれの使い方を詳しくみていきます。
## `create`：任意のソースコードをAPIに投げて実行
`create` はPOSTリクエストでソースコードや実行言語、標準入力の値など、実行時に必要な情報をAPIに投げて実行してもらうリクエストです。
レスポンスは実行結果ではなく、API側で管理しているセッションIDと、実行時のステータスが返ってきます。
ここで返されたセッションIDを利用して、再度実行結果をGETリクエストで取得する必要があります。

### パラメータ
| パラメータ | 値の例 | 説明 | 型| 必須 |
| --- | --- | --- | --- | --- |
| source_code | print("hoge") | 実行したいソースコード | string | 〇 |
| language | python3 | 実行言語 | string | 〇 |
| input | 10 | 標準入力 | string | ✕ |
| longpoll | true | ロングポーリングを有効にするかどうか。初期値は`false` | boolean | ✕ |
| longpoll_timeout | 5.0 | ロングポーリングを有効にする場合のタイムアウト時間。初期値は10.0 | double | ✕ |
| api_key| guest | APIキー | string | 〇 |

#### 実行可能言語一覧
実行可能な言語は以下の通りです。

| 言語 | 値 |
| --- | --- |
| C言語 | c |
| C++ | cpp |
| Objective-C | objective-c |
| Java | java |
| Kotlin | kotlin |
| Scala | scala |
| Swift | swift |
| C# | csharp |
| Go | go |
| Haskell | haskell |
| Erlang | erlang |
| Perl | perl |
| Python | python |
| Python3 | python3 |
| Ruby | ruby |
| PHP | php |
| Bash | bash |
| R | r |
| JavaScript | javascript |
| CoffeeScript | coffeescript |
| Visual Basic | vb |
| Cobol | cobol |
| F# | fsharp |
| D | d |
| Clojure | clojure |
| Elixir | elixir |
| MySQL | mysql |
| Rust | rust |
| Scheme | scheme |
| CommonLisp | commonlisp |
| Plain | plain |

#### ロングポーリングについて
パラメータで指定可能なロングポーリングのありとなしの違いについてです。
一言で説明すると、リクエストしたソースコードの実行完了まで待機するかしないかの違いです。

具体例を用いて説明します。
例えば、以下コードの実行結果を得ようとPaiza IO APIにリクエストを送ります。

```python:sample.py
cnt = 0
while(cnt < 10**9):
    cnt += 1
print(cnt)
```
10の9乗回ループしているので、およそ1秒かかる処理です。
上記のコードをロングポーリング無しでリクエストを投げると、**APIのリクエストに必要な時間**が経過した後に以下のレスポンスが返ってきます。

```js:レスポンス
{
  id: "XXXXXXXXXXXXXXX" // セッションID
  status: "running"     // 実行状態
}
```
一方で、ロングポーリングを有効にすると、**APIのリクエストにかかる時間+コードの実行時間**が経過した後に以下のレスポンスが返ってきます。

```js:レスポンス
{
  id: "XXXXXXXXXXXXXXX" // セッションID
  status: "completed"     // 実行状態
}
```
つまり、完了を待つか待たないかの違いです。
APIにリクエストを投げた後に、コード実行中も別の処理を行いたい場合はロングポーリングは無効にし、コード実行リクエストから実行結果を得るまで別の処理を行わない場合は、ロングポーリングを有効にすると良いと思います。

### リクエスト

JavaScriptで`create`リクエストを送るサンプルコードです。

```js:JavaScriptでcreateリクエストを送るサンプルコード
async function postData(url = "", data = {}) {
  const response = await fetch(url, {
    method: "POST",
    mode: "cors",
    cache: "no-cache",
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/json",
    },
    redirect: "follow",
    referrerPolicy: "no-referrer",
    body: JSON.stringify(data),
  });
  return response.json();
}

async function main(){
  const url = "http://api.paiza.io:80/runners/create";
  const data = {
    source_code: "print(input())",
    language: "python3",
    input: "hellow world",
    api_key: "guest",
  };
  const res = await postData(url, data);
}

main();
```

### レスポンス
レスポンスはJSON形式で以下の通りです。
ここで受け取ったセッションIDを用いて、実行状態や実行結果を問い合わせることができます。

```js:レスポンス
{
  id: "XXXXXXXXXXXXXXX" // セッションID
  status: "running"     // 実行状態（実行中：running、実行完了：completed）
}
```

## `get_status`：投げたコードの実行状況を取得
`get_status` は`create`リクエストで投げたソースコードの実行状況を、GETリクエストで問い合わせます。
ロングポーリングが無効の場合は実行結果を問い合わせる前に、`get_status`で実行状況を確認すると良いと思います。

### パラメータ
| パラメータ | 値の例 | 説明 | 型| 必須 |
| --- | --- | --- | --- | --- |
| id| ab-CdE6FG7hI8JKl | セッションID | string | 〇 |
| api_key| guest | APIキー | string | 〇 |

### リクエスト

JavaScriptで`get_status`リクエストを送るサンプルコードです。

```js:JavaScriptでget_statusリクエストを送るサンプルコード
async function getData(url) {
  const response = await fetch(url);
  return response.json();
}

async function main() {
  const sessionId = "XXXXXXXXXXXXXXXX";
  const url = `http://api.paiza.io/runners/get_status?id=${sessionId}&api_key=guest`;
  const response = await getData(url);
}

main();
```

### レスポンス
レスポンスはJSON形式で以下の通りです。

```js:レスポンス
{
  id: "XXXXXXXXXXXXXXX" // セッションID
  status: "completed"   // 実行状態（実行中：running、実行完了：completed）
}
```

## `get_details`：投げたソースコードの実行結果を取得
`get_details`はGETリクエストで`create`リクエストで投げたソースコードの実行結果を取得します。
`get_details`で実行結果を得るには、リクエストしたコードの実行が完了している必要があります。

### パラメータ
| パラメータ | 値の例 | 説明 | 型| 必須 |
| --- | --- | --- | --- | --- |
| id| ab-CdE6FG7hI8JKl | セッションID | string | 〇 |
| api_key| guest | APIキー | string | 〇 |

### リクエスト

JavaScriptで`get_details`リクエストを送るサンプルコードです。

```js:JavaScriptでget_detailsリクエストを送るサンプルコード
async function getData(url) {
  const response = await fetch(url);
  return response.json();
}

async function main() {
  const sessionId = "XXXXXXXXXXXXXXXX";
  const url = `http://api.paiza.io/runners/get_details?id=${sessionId}&api_key=guest`;
  const response = await getData(url);
}

main();
```

### レスポンス
レスポンスはJSON形式で以下の通りです。

```js:レスポンス
{
  "id": "eFES9ngIykoZovYasRlA0w",
  "language": "python3",
  "status": "completed",
  "build_stdout": null,
  "build_stderr": null,
  "build_exit_code": 0,
  "build_time": null,
  "build_memory": 9632000,
  "build_result": null,
  "stdout": "hello world\n",
  "stderr": "",
  "exit_code": 0,
  "time": "0.02",
  "memory": 9632000,
  "result": "success"
}
```
各プロパティの意味は以下の通りです。

| パラメータ | 説明 |
| --- | --- |
| id | セッションID |
| language | 実行言語 |
| status | 実行状況。実行中の場合は`running`、実行完了の場合は`completed` |
| build_stdout | ビルド実行後、正常終了時の標準出力 |
| build_stderr | ビルド実行後、異常終了時の標準出力 |
| build_exit_code | ビルド実行後の終了コード |
| build_time | ビルド実行にかかった時間（秒） |
| build_memory | ビルド実行に使ったメモリ（byte） |
| build_result | ビルド結果。正常終了：`success`, 以上終了：`failure`, タイムアウト（最大1秒）：`timeout`  |
| stdout | コード実行後、正常終了時の標準出力 |
| stderr | コード実行後、異常終了時の標準出力 |
| exit_code | コード実行後の終了コード |
| time | コードの実行にかかった時間（秒） |
| memory | コードの実行に使ったメモリ（byte） |
| result | コードの実行結果。正常終了：`success`, 以上終了：`failure`, タイムアウト（最大1秒）：`timeout` |


# おわりに
以上が、PaizaIO APIについての説明でした。
コードジャッジシステムを自作しようとすると、多言語実行処理の実装という大きな壁が存在しますが、このようなAPIが用意されているととても助かります。
明日は [@mito1296](https://qiita.com/mito1296) さんです。お楽しみに！