OpenAPIでお手軽にモックAPIサーバーを動かす

OpenAPIというものを教えてもらったので、ちょっと~~遊んで~~試してみました。

# 目標
APIをOpenAPIで定義し、コードをジェネレートしてAPIのレスポンスが得られる。
なので、使うのは[OpenAPI Specification](https://swagger.io/specification/)と[OpenAPI generator](https://openapi-generator.tech/)になります。

# 環境など

|ツールなど|バージョンなど|
|:--|:--|
| MacbookPro |macOS Mojave 10.14.5|
|Android Studio |3.6.1|
|Java |1.8.0_131|
|Postman|7.19.1|

# OpenAPIでAPIを定義する

## 1.APIの設計

APIの設計をzeroからやったことがないので流儀がよくわからないですが、以下のようなRESTfulAPIを考えます。

とあるログデータ型`LogData`があるとして、データは何かしらのDBに入っていると仮定します。プライマリキーは日付となります(`yyyy/MM/dd`の文字列と仮定しています)

- ユーザーのログデータをCREATE(post)
- ユーザーのログデータをREAD(get{date}/getAll}
- ユーザーのログデータをUPDATE(put)
- ユーザーのログデータをDELETE(delete)

また、戻りは**Json**に固定しようと思います(xmlはパースが大変じゃない？)。

## 2.YAMLを設定する

**OpenAPI Specification**でAPIの定義を書いていきます。
Jsonでも書けるそうですが、YAMLのほうが見やすい気がするので、YAMLで行ってみます。

### (1)エンドポイント
まず、エンドポイントのurlを決めます。
ここでは、`http://my.data.example.com/v1`とします。

```example_api.yaml
openapi: "3.0.3"

info:
  version: 0.0.1
  title: My SampleData Api
  license:
    name: MIT
servers:
  - url: http://my.data.example.com/v1
```

### (2)パスの定義

RESTfulはパスとメソッドタイプで処理内容を表現するものですが、処理内容がわかっていれば、ほぼ決められますね。

以下のようにパスを定義しました。

- getAll *(GET)*
  - `/logs`
- get{id} *(GET)*
  - `/logs/{date}`
- create *(POST)*
  - `/logs`
- update *(PUT)*
  - `/logs`
- delete *(DELETE)*
  - `/logs/{date}`

YAMLの記述は次のようになります。
※既出の箇所は随時省略していきますので、どこに追記するかは一つ前のコードと見比べて読み取ってください。

```example_api.yaml
servers:
  - url: http://my.data.example.com/v1
paths:
  /logs:
    get:
    post:
    put:
  /logs/{date}:
    get:
    delete:
```

パスとメソッドの組み合わせで定義していきます。

### (3)各メソッドの詳細を定義する

サマリーやoperationId, パラメータなどを定義します。パラメータ以外は主にドキュメントに関わる部分になります。戻り値については次項でやりますので今はおいておきます。

```example_api.yaml
paths:
  /logs:
    get:
      summary: List all logs
      operationId: listLogs
      tags:
        - logs
    post:
      summary: Create a log
      operationId: postLogs
      tags:
        - logs
    put:
      summary: Update a log
      operationId: putLogs
      tags:
        - logs
  /logs/{date}:
    get:
      summary: Log for a specific date
      operationId: showLogByDate
      tags:
        - logs
      parameters:
        - name: date
          in: path
          required: true
          description: The date string formatted yyyy/MM/dd
          schema:
            type: string
    delete:
      summary: Delete log by date key
      operationId: deleteLogByDate
      tags:
        - logs
      parameters:
        - name: date
          in: path
          required: true
          description: The date string formatted yyyy/MM/dd
          schema:
            type: string
```

何となく見てわかりますよね。

`/logs`というパスの中には、`get:`と`post:`、`put:`のメソッドがあり、`logs/{date}`というパスでは、`get:`と`delete:`が出来る。

`get:`と`delete:`は、`date`という名前のパラメータが必須(`required`)で、`string`型ですよ、と。

そんな定義が読み取れると思います。

### (4)レスポンスの定義
レスポンスの定義は、正常時(HttpStatus=200)の場合のヘッダーや`ContentType`を定義します。また、エラー時の定義も行います。

戻すのは、ほぼJsonになりますね。
※スキーマの定義は次項でやります。

```example_api.yaml
paths:
  /logs:
    get:
      summary: List all logs
      operationId: listLogs
      tags:
        - logs
      responses:
        '200':
          description: Array of logs
          content:
            application/json:
        default:
          description: unexpected error
          content:
            application/json:
```

長くなってきたのでメソッドごとに区切ります。

```yaml
    post:
      summary: Create a log
      operationId: putLogs
      tags:
        - logs
      responses:
        '201':
          description: Null response
          headers:
            location:
              description: A link to the new log
              schema:
                type: string
        default:
          description: unexpected error
          content:
            application/json:
```

RESTfulAPIの設計では、CREATEでは作成したらヘッダーに`Location`で作成したデータへのGETリクエストパスを返すことが推奨されているので、それを入れています。
https://docs.microsoft.com/ja-jp/azure/architecture/best-practices/api-design#post-methods

```yaml
    put:
      summary: Update a log
      operationId: putLogs
      tags:
        - logs
      responses:
        '200':
          description: Update results
          content:
            application/json:
        default:
          description: unexpected error
          content:
            application/json:
```

PUTメソッドでは、作成したら`201`を返し、更新が成功したら`200`を返すことが推奨されていますが、今回はどのみちダミーでしか動かないので、`200`を返す更新成功のみを入れています。ちなみに、戻りのボディに更新後のデータそのものを入れることも推奨されているため、入れることにしました。

```yaml
  /logs/{date}:
    get:
      summary: Log for a specific date
      operationId: showLogByDate
      tags:
        - logs
      parameters:
        - name: date
          in: path
          required: true
          description: The date string formatted yyyy/MM/dd
          schema:
            type: string
      responses:
      	'200':
      	  description: Expected response to a valid request
          content:
            application/json:
        default:
          description: unexpected error
          content:
            application/json:
```

```yaml
    delete:
      summary: Delete log by date key
      operationId: deleteLogByDate
      tags:
        - logs
      parameters:
        - name: date
          in: path
          required: true
          description: The date string formatted yyyy/MM/dd
          schema:
            type: string
      responses:
      	'204':
      	  description: Expected response to a valid request
        default:
          description: unexpected error
          content:
            application/json:
```

### (5)スキーマの定義
スキーマの定義というのは、レスポンスの戻りの`Json`の項目を宣言することです。
たとえば、`/logs/{date}`の**GET**の場合で考えます。

`LogData`のクラスがこのような設定の場合、

```java
class LogData{
  public String date;
  public int foo;
  public bool bar;
}
```

`LogData`のオブジェクト1つを表すJson文字列は、

```json
{
  "date" : "2020/02/22",
  "foo" : 12345,
  "bar" : false,
}
```

とするのが普通ですから、`response`のスキーマはこのように宣言します。

```yaml
components:
  schemas:
    Log:
      type: object
      required:
        - date
        - foo
        - bar
      properties:
        date:
          type: string
        foo:
          type: integer
          format: int32
        bar:
          type: boolean
```

スキーマの定義は、`components`下に置き、それを各API定義のところで参照するという形を取ります。

続いて`Logs`スキーマを定義します。これは`getAll`で返ってくるのが配列なはずなので、それの定義となります。

```yaml
components:
  schemas:
    Log:
    ....
    Logs:
      type: array
      items:
        $ref: "#/components/schemas/Log"
```

スキーマの中でスキーマを参照しています。
スキーマを参照するときは、`$ref: "#/components/schemas/スキーマ名`というように書きます。
`Logs`は`Log`の配列ってことですね。

ついでにエラーのスキーマも定義します。
よく、エラー時のレスポンス(Json)はこんな感じにしますよね？

```json
{
  "status" : 100
  "message" : "なんかエラーだよ"
}
```

というわけで、エラーのときはこれを返すようにスキーマも定義します。

```yaml
components:
  schemas:
    Log:
      ...
    Logs:
      ...
    Error:
      type: object
      required:
        - status
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string
```

### (6)APIの戻りスキーマを指定する

作成したスキーマを参照して、各APIメソッドのresponseにきちんと設定してやります。
`applicatoin/json`とした属性の下に、`schema`を宣言し、そこで`$ref`で先ほど作成したスキーマを参照指定します。

```yaml
  /logs:
    get:
      summary: List all logs
      operationId: listLogs
      tags:
        - logs
      responses:
        '200':
          description: Array of logs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Logs"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
```

```yaml
    post:
      summary: Create a log
      operationId: createLog
      tags:
        - logs
      responses:
        '201':
          description: Null response
          headers:
            location:
              description: A link to the new log
              schema:
                type: string
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
```

```yaml
    put:
      summary: Update a log
      operationId: putLog
      tags:
        - logs
      responses:
        '200':
          description: Update results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Log"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
```

```yaml
  /logs/{date}:
    get:
      summary: Log for a specific date
      operationId: showLogByDate
      tags:
        - logs
      parameters:
        - name: date
          in: path
          required: true
          description: The date string formatted yyyy/MM/dd
          schema:
            type: string
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Log"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
```

```yaml
    delete:
      summary: Delete log by date key
      operationId: deleteLogByDate
      tags:
        - logs
      parameters:
        - name: date
          in: path
          required: true
          description: The date string formatted yyyy/MM/dd
          schema:
            type: string
      responses:
        '204':
          description: Expected response to a valid request
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
```

### (7)リクエストボディの設定
リクエストボディも、スキーマを使って定義します。

```yaml
    post:
      summary: Create a log
      operationId: createLog
      tags:
        - logs
      requestBody:
        description: log to create
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Log"
```

```yaml
    put:
      summary: Update a log
      operationId: putLog
      tags:
        - logs
      requestBody:
        description: update to create
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Log"
```

どうでしょうか？結構なコード量になりましたね。
合ってるかどうかわからなくて不安な人は、[Swagger Editor](http://editor.swagger.io/)という便利なサイトがありますので、確認しながら書いていくといいでしょう。ただし、スキーマ定義をまっさきにやったほうがいいです。

<img width="1143" alt="swagger_editor.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/d512e1ce-9b4a-0c6f-30ed-a25bbbfb6d5b.png">

なんとなく、右側のパネルが、すでにAPI仕様書っぽくなっていますね。

## 3. example値の設定
OpenAPIの定義に、サンプルの戻り値を設定できます。それが`example`です。
スキーマの下に設定できます。

たとえば、`Log`オブジェクト１件のサンプル値だと、次のように書けます。

```yaml
components:
  schemas: 
    Log:
      type: object
      required:
        - date
        - foo
        - bar
      properties:
        date:
          type: string
        foo:
          type: integer
          format: int32
        bar:
          type: boolean
      example: 
        date: "2020/02/22"
        foo: 11
        bar: true
```

配列の場合は、`-`で要素を区切ります。

```yaml
    Logs:
      type: array
      items:
        $ref: "#/components/schemas/Log"
      example: 
        - date: "2020/02/22"
          foo: 11
          bar: true
        - date: "2020/08/23"
          foo: 13
          bar: false
```

これで、モックサーバーを動かす準備が出来ました。
Swagger Editorのページから、`yaml`ファイルがダウンロードできます。

# モックサーバーを動かす

早速、この定義を使って、モックサーバーを動かしてみます。
ただ、OpenAPI Generator等を使うと、サーバープログラムのビルドが必要になってしまいます。それだとサクッと動かしたい・・・という希望に添わないので、探したところ、こちらを発見しました。

API Sprout
https://github.com/danielgtaylor/apisprout

なんとyamlファイルを指定するだけで、モックサーバーを動かしてくれるというではありませんか。
早速インストールしてみます。

## 1. API Sproutのインストール
Dockerでやるのが良さそうです。
Dockerをインストールしてない方は[こちらの記事](https://qiita.com/kasa_le/items/54787090930934389880)などを参考に、先に入れて下さい。

### (1)Dockerからpull

```console
$ docker pull danielgtaylor/apisprout
```

普通にpullするだけですね。

## 2. API Sproutでモックサーバーを起動

```console
$ docker run -p 8000:8000 -v /foo/bar/workspace/openapi/openapi.yaml:/openapi.yml danielgtaylor/apisprout /openapi.yml
```

`/foo/bar/workspace/openapi/openapi.yaml`の部分は、自分で作成したファイル名で、フルパスで指定する必要があります。

```console
🌱 Sprouting My SampleData Api on port 8000
```
と表示が出れば起動OKです。

ブラウザとかで`localhost:8000/logs`とかしてみましょう。
以下は`curl`の例です。

```console
$ curl localhost:8000/logs
[
  {
    "bar": true,
    "date": "2020/02/22",
    "foo": 11
  },
  {
    "bar": false,
    "date": "2020/08/23",
    "foo": 13
  }
]
```

example値がちゃんと出力されましたね！

# Postmanで実行

GET以外のメソッドの動作確認にはPostmanを使う方が楽なので、こっちでやってみます。
Postmanのダウンロードは[こちらから](https://www.postman.com/)。

## 1.POST

メソッドをPOSTにして、[Send]をクリック。
Bodyは、API側がどうせダミーなので指定しなくても大丈夫ですがサンプルでは入れています。

|![openapi_post.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/fcbfcd18-8694-ece1-4e57-31a03b31b068.png)|
|:--|

resultが`201 Created`になっていますね。

## 2.PUT

メソッドをPUTにして、[Send]をクリック。
Bodyは今はダミーなので空でもOK。

|![openapi_put.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/ad7a9c3d-4a76-19b4-c307-577d7890435f.png)|
|:--|

resultが`200 OK`になっていますね。
そしてresponseのBodyに更新結果のJsonが入っています。

## 3.DELETE

メソッドをDELETEにして、[Send]をクリック。

|![openapi_delete.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/ae5ad6af-4c0e-5adb-e16e-2c97055b4248.png)|
|:--|

成功のresultは`204`です。

# クライアントアプリの作成

Postmanだけじゃなんだか味気ないので、クライアントアプリがAPI定義ができているだけで実行できるというメリットを確認すべく、なにか作ってみようと思います。
何でもいいのですが、私の得意分野でということで、Android+Kotlin+coroutineでサクッと作ってみようと思います。(本当にサクッと出来たかは内緒w)

なお、APIサーバーがlocalhostで動いているので、エミュレーターを使うしかありません。ひところに比べ、エミュレーターは爆速にはなりましたが、やっぱりもっさり感は否めないですよね・・・まあでも今回は仕方ありません。

エミュレーターでローカルネットワークで稼働するAPIに繋ぐには、`localhost`や`127.0.0.1`ではなく、`10.0.2.2`を使う必要があるので、ご注意を。

## 1. Androidアプリの作成

Kotlinでサクッと(略)

こちらにサンプルコードがあります。
https://github.com/le-kamba/RestApiCoroutineSample

※後ほど、実装の詳細を別記事に上げる予定。

OpenAPI Generatorで出来るAndroid用コードもあります。

OpenAPI GeneratorのCLIツールでクライアントコードを作成する場合、Dockerを使って次のように出来ます。

```console
$ GENERATOR=android
$ docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i /local/openapi.yaml -g ${GENERATOR} -o /local/out/${GENERATOR}
```

なお、事前にプロジェクトを作りたいフォルダを作ってそこに移動しその中に作成したyamlファイルをコピーしておきます。

**が！！**

作成されるプロウジェクトが古すぎて、新しめのAndroidStudioでは、もうビルドできない代物になっています(泣)　2系が残っている人はビルドできるかも？^^;

もしこれを新しめ(3系以降?)のAndroidStudioで動かそうとするなら、プロジェクトを新規作成した上で、クラスや依存関係をコピーしてくる方が良さそうです。やりませんけど。

## 2.動作テスト

文字が潰れて見づらいかもですが、レスポンスヘッダーとボディのJsonをそのまま出力しています。

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/894ee210-2c6f-9a20-6007-736da73f3efb.gif)

# 感想

とっても簡単にAPIのモックサーバーが出来ました。
これで、アプリ側の開発初期でも通信を入れたテストが出来ますね。
I/Fの設計が後で頻繁に変わるようだと辛いのは変わらなさそうですが(汗)

ちなみに、Androidのアプリは、以下のアーキテクチャ・ライブラリなどを使って実装しました。

- Kotlin
  - Javaに代わる推し言語  
- coroutine
  - Kotlinでの非同期処理
- Retrofit2
  - 通信ライブラリ
- Koin
  - Kotlin用お手軽DI

上にも書きましたがご興味ある方は以下よりご覧頂けます。
https://github.com/le-kamba/RestApiCoroutineSample

YAMLファイルのサンプルも、上記リポジトリの`OpenAPIConfig`フォルダにあります。


# 参考サイトなど

apisproutを教えて頂きました。
https://qiita.com/ShoichiKuraoka/items/f1f7a3c2376f7cd9c56

dockerでOpenAPI Generatorを走らせる方法の参考になりました。
https://qiita.com/amuyikam/items/e8a45daae59c68be0fc8

Androidエミュレーターでlocalhostに繋ぐ場合のIPアドレスについて参考になりました。
https://araramistudio.jimdo.com/2018/01/11/android%E3%81%AE%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E3%81%8B%E3%82%89%E8%87%AA%E8%BA%AB%E3%81%AEpc-localhost-%E3%81%B8%E6%8E%A5%E7%B6%9A/