OpenAPI Generator でAndroid コードを自動生成する

# [The OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification) (OAS)
OAS はHTTP API の仕様を記述するためのフォーマットです。
主な内容は以下のように、

- OAS のバージョン番号
- 記述するAPI のバージョン番号、タイトルなどの情報
- サーバへの接続情報
- 各エンドポイントの情報

からなります。（より詳しい書き方は https://swagger.io/specification/ を参照）

```openapi.yaml
openapi: 3.0.3
info:
  version: 0.0.0
  title: OpenAPI Generator test with Qiita API
servers:
  - url: https://qiita.com/api/v2/
paths:
  /users:
    get:
      parameters:
        - ...
      responses:
        200:
          ...
```

OAS は特定のプログラミング言語に依存しない形で記述できるため、API のドキュメントとしてだけではなく、そこからクライアント・サーバそれぞれのコードを生成することができます。
[OpenAPI Generator](https://openapi-generator.tech/)（[github](https://github.com/OpenAPITools/openapi-generator)）はOAS の形式で書かれたyaml やjson ファイルからコード生成を自動でやってくれるソフトウェアです。

今回は[Qiita API](https://qiita.com/api/v2/docs) を題材にして、それを叩くAndroid アプリを作ります。

# 完成品
コマンドを叩くだけで以下のようなファイルが自動生成されるようになります。

<details><summary>UserApi.kt</summary><div>

```UserApi.kt
import ...

interface UserApi {
    /**
     * 全てのユーザの一覧を作成日時の降順で取得します。
     * getAllUser の詳細です。
     * Responses:
     *  - 200: 取得成功です。
     *  - 0: Unexpected error
     * 
     * @param page ページ番号 (1から100まで) (optional)
     * @param perPage 1ページあたりに含まれる要素数 (1から100まで) (optional)
     * @return [Call]<[kotlin.collections.List<User>]>
     */
    @GET("users")
    fun getAllUser(@Query("page") page: kotlin.String? = null, @Query("per_page") perPage: kotlin.String? = null): Call<kotlin.collections.List<User>>


    ...
}
```
</div></details>
<details><summary>User.kt</summary><div>

```User.kt
import ...

/**
 * Qiita上のユーザを表します。
 * @param description 自己紹介文
 * @param facebookId Facebook ID
 * @param ...
 */

data class User (
    /* 自己紹介文 */
    @Json(name = "description")
    val description: kotlin.String? = null,
    /* Facebook ID */
    @Json(name = "facebook_id")
    val facebookId: kotlin.String? = null,
    ...
}
```
</div></details>

# 環境
Android studio: 4.0.1
OpenAPI Generator: 5.0.0-beta2

# 仕様書を作る
Qiita API に対応するファイルが見つからなかったので、練習も兼ねて自分で書いていくことにしました。
ドキュメントからyaml ファイルを作っていきますが、全部のAPI を書くのは辛いので、今回は以下のAPI に絞りました。

- `GET /api/v2/users`
- `GET /users/{user_id}`
- `GET /api/v2/items`
- `POST /api/v2/items`

このファイルの作成には [Swagger Editor](https://editor.swagger.io/) という便利なサイトがあるので活用していきます。
このサイトでは、yaml を編集すると、逐次右側の画面が更新され、正しく解釈されているかを確認できたり、実際にAPI を叩いて結果を確認したりすることが可能です。

![スクリーンショット 2020-10-08 14.49.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193108/fb44f191-b7fc-2d50-c41d-8f66fb69a548.png)
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193108/fcab9423-103f-cf68-39ae-7facf469e32a.png  width=600px>

## tags
まず、各API をグルーピングします。これはOAS での`tag` に対応します。
`tag` は以下のようにyaml ファイルのルート に記述し、後述する各path の中でも指定します。

```openapi.yaml
tags:
  - name: user # api のインターフェース名になる
    description: ユーザに関係する操作
  - name: item
    description: 投稿に関係する操作
paths:
  /users:
    get:
      tags:
        - user
      parameters:
        - ...
```

このようにグルーピングすることで、OpenAPI Generator から生成されるAPI 用のinterface を記述するファイルが分けられます。
![スクリーンショット 2020-10-08 11.54.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193108/d4a0ccec-f4e9-2714-f0d3-e44b464d1823.png)

## components
### schemas
次に各API から返ってくるデータの詳細を記述します。これはOAS での`components.schemas` に対応します。
`components` は以下のようにyaml ファイルのルート に記述し、その一階層下に`schemas` を記述します。これも、後述するpath の中で指定することで、各API に対応付けることができます。

```openapi.yaml
components:
  schemas:
    User: # モデルクラス名になる
      description: Qiita上のユーザを表します。
      type: object
      properties:
        description:
          description: 自己紹介文
          type: string
          nullable: true
          example: Hello, world.
        ...
```

これらはOpenAPI Generator ではモデルクラスに対応します。
![スクリーンショット 2020-10-08 12.14.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193108/0002cc24-909e-bbe4-c847-2dfacc742788.png)

### securitySchemes
こちらはHTTP 認証やAPI キー、OAuth2 などのセキュリティ情報の詳細を記述します。これはOAS での`components.securitySchemes` に対応します。
`securitySchemes` は上記の`schemas` と同じ階層に記述し、これも後述するpath の中で指定することで、各API に対応付けることができます。

```openapi.yaml
components:
  securitySchemes:
    Bearer:
      type: http
      scheme: bearer
      description: Access token for API
```

## paths
最後に各API の情報を記述していきます。これはOAS での`paths` に対応します。
`paths` は以下のようにyaml ファイルのルート に記述します。

```openapi.yaml
paths:
  /users: # エンドポイント
    get: # HTTP メソッド
      tags:
        - user # ここで指定するtag に対応するapi ファイルに振り分けられる
      summary: 全てのユーザの一覧を作成日時の降順で取得します。
      description: getAllUser の詳細です。
      operationId: getAllUser # api ファイルでのメソッド名になる
      parameters:
        - name: page
          in: query
          description: ページ番号 (1から100まで)
          example: 1
          required: false
          schema:
            type: string
            pattern: /^[0-9]+$/
        - name: per_page
          in: query
          description: 1ページあたりに含まれる要素数 (1から100まで)
          example: 20
          required: false
          schema:
            type: string
            pattern: /^[0-9]+$/
      responses:
        200:
          description: 取得成功です。
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User' # スキーマを指定
        default:
          description: Unexpected error
```
`paths` の下にはエンドポイント、そのさらに下にHTTP method を記述し、その下に詳細を記述します。
細かい仕様は https://swagger.io/specification/ を参照してください。

出来上がった `openapi.yaml` ファイルはプロジェクトのルートに置いておきます。

# Gradle の記述
Gradle を使って自動生成ができるようにします。
Gradle での記述方法は [OpenAPI Generator Gradle Plugin](https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-gradle-plugin/README.adoc) に詳しく載っています。

今回はapp モジュールとは別にapi モジュールを作り、そこで自動生成されたファイルをapp モジュールから参照する形にします。

```api/build.gradle.kts
plugins {
    kotlin("jvm")

    // OpenAPI Generator
    id("org.openapi.generator") version "5.0.0-beta2"
    id("kotlinx-serialization")
}

dependencies {
    implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))

    // kotlin, retrofit, moshi などの依存
    implementation(Dependency.kotlinStdLibJdk8)
    implementation(Dependency.retrofit)
    implementation(Dependency.moshi)
    ...
}

// API 名
val apiName = "qiita"

// ビルド先ディレクトリ
val buildApiDir = "$buildDir/openApiGenerator/$apiName"

// 自動生成先のパッケージ名
val basePackage = "com.example.openapigenerationtestapi"

fun String.packageToDir() = replace('.', '/')

task<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("generate") {
    doFirst {
        delete(file(buildApiDir))
    }

    // OpenAPI Generator のオプションを指定
    generatorName.set("kotlin")
    library.set("jvm-retrofit2")
    // templateDir.set("$rootDir/template") // テンプレートを指定、詳細は後述
    inputSpec.set("$rootDir/openapi.yaml")
    outputDir.set(buildApiDir)
    packageName.set(basePackage)
    apiPackage.set("$basePackage.$apiName.api")
    modelPackage.set("$basePackage.$apiName.model")
    configOptions.set(mapOf(
        "dateLibrary" to "java8"
    ))
    additionalProperties.set(mapOf(
        // ここでRx やCoroutine を利用するか指定できます
        "doNotUseRxAndCoroutines" to "true" 
    ))
    generateApiTests.set(false)
}

task<Copy>("copy") {
    val dirFrom = "$buildApiDir/src/main/kotlin/${basePackage.packageToDir()}/"
    val dirInto = "$projectDir/src/main/java/${basePackage.packageToDir()}/"

    doFirst {
        delete(file(dirInto))
    }

    dependsOn("generate")
    from(dirFrom)
    into(dirInto)
}

task("buildApi") {
    dependsOn("generate", "copy")
}
```

`generate` がbuild ディレクトリに自動生成を行い、`copy` でsrc 以下に必要なファイルをコピーします。
コマンドラインからは `buildApi` を実行することでこれらをまとめて実行するようにします。

```command.sh
./gradlew :api:buildApi
```
これを実行すると、自動生成されたファイルを見ることができます。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193108/0ba9989d-6b97-7e85-bb2d-02a350da5361.png  width=400px>

# テンプレートの修正
OpenAPI Generator も完璧ではありません。
例えば、`v5.0.0-beta2` では自動生成される `ApiClient.kt` ファイルで最新版のRetrofit2 に対してのエラーが発生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193108/e9736df0-105a-aa16-7ff5-149631d1d87d.png)
これをテンプレートを修正することで解決してみます。（この問題は現在 [PR](https://github.com/OpenAPITools/openapi-generator/pull/7556) が出されており、次の `v5.0.0` では修正されると思います。[^1]）

まず、プロジェクトのルートに `template` ディレクトリを作り、OpenAPI Generator にある [テンプレート](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/kotlin-client) をダウンロードして配置します。
`ApiClient.kt` に対応するファイルが `libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache` なのでこれを編集していきます。

```libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache
...
    fun <S> createService(serviceClass: Class<S>): S {
# REMOVE these lines
#        var usedClient: OkHttpClient? = null
#        this.okHttpClient?.let { usedClient = it } ?: run {usedClient = clientBuilder.build()}
# ADD this line
        val usedClient: OkHttpClient = this.okHttpClient ?: clientBuilder.build()
        return retrofitBuilder.client(usedClient).build().create(serviceClass)
    }
...
```

この変更を加えたのち、`Gradle の記述` の部分でコメントアウトしていたテンプレートの指定を行い、再生成を行うことで正しいファイルが生成できます。
今回はOAS の内容に依らない修正だったのでそのままkotlin を書くように編集するだけでしたが、OAS の内容に合わせて変化するコードを記述することも可能です。
詳しい内容は https://openapi-generator.tech/docs/templating/ を参照してください。

# 実際に呼ぶ部分の記述
ここまでこれば、あとはアプリを作っていくだけです。
`ApiClient.kt` などの便利なクラスを利用することでかなり楽に書くことができます。

```kotlin
val userApi: UserApi = ApiClient(authName = "Bearer", bearerToken = "[MY_TOKEN]")
        .createService(UserApi::class.java)

fun getAllUser(callback: Callback<List<User>>, page: Int? = null, perPage: Int? = null) {
    val call = userApi.getAllUser(page?.toString(), perPage?.toString())
    call.enqueue(callback)
}
...
```

# Coroutine 対応コードを生成する
上ではAPI インタフェースは `retrofit2.Call` を返す関数を定義しています。
そのため、自動生成以外の自分で書くコードではCallback を定義する必要があり、少し面倒です。
ここでは、Coroutine で使えるようなsuspend 関数を返すように変更します。

```api/build.gradle.kts
    additionalProperties.set(mapOf(
// REMOVE this line
//        "doNotUseRxAndCoroutines" to "true"
// ADD this line
        "useCoroutines" to "true"
    ))
```

以上です。ありがたいことにこれだけでCoroutine で使えるコードが生成されます。

<details><summary>ItemApi.kt</summary><div>

```ItemApi.kt
interface ItemApi {
    /**
     * 記事の一覧を作成日時の降順で返します。
     * getAllItems の詳細です。
     * Responses:
     *  - 200: 取得成功です。
     * 
     * @param page ページ番号 (1から100まで) (optional)
     * @param perPage 1ページあたりに含まれる要素数 (1から100まで) (optional)
     * @param query 検索クエリ (optional)
     * @return [kotlin.collections.List<Item>]
     */
    @GET("items")
    suspend fun getAllItems(@Query("page") page: kotlin.String? = null, @Query("per_page") perPage: kotlin.String? = null, @Query("query") query: kotlin.String? = null): Response<kotlin.collections.List<Item>>

    /**
     * 新たに記事を作成します。
     * postItem の詳細です。
     * Responses:
     *  - 201: 作成成功です。
     *  - 0: Unexpected error
     * 
     * @param inlineObject  
     * @return [Unit]
     */
    @POST("items")
    suspend fun postItem(@Body inlineObject: InlineObject): Response<Unit>

}

```
</div></details>

あとは、実際の呼び出し側も修正します。

```kotlin
suspend fun getAllItems(page: Int? = null, perPage: Int? = null, query: String? = null)
        = itemApi.getAllItems(page?.toString(), perPage?.toString(), query)
```

# 終わりに
OpenAPI Generator はクライアント側だけでなく、サーバー側のコードも自動生成することができるので、ドキュメントとしてOAS に沿ったyaml ファイルを作っておくだけで、クライアント・サーバーで一貫性の取れたコードを書くことができます。
また、API に変更があれば、yaml ファイルに変更を適用し、再生成を行うだけですぐにAPI を利用することができるため、開発の負担を大きく減らすことが期待できます。
SwaggerEditor で簡単に試すことができるので、OpenAPI Generator をやってみてはいかがでしょうか。

# 参考
https://tech-blog.optim.co.jp/entry/2020/04/13/100000
https://techblog.asahi-net.co.jp/entry/2019/03/04/102734
https://qiita.com/amuyikam/items/e8a45daae59c68be0fc8
https://qiita.com/sengok/items/1d958348215647a5eaf0

今回作成したサンプルプロジェクト
https://github.com/warahiko/OpenApiGenerationTest


[^1]: 実は気づかずに同じような [PR](https://github.com/OpenAPITools/openapi-generator/pull/7592) を出してしまっていました。。。