29
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-08

The 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 Generatorgithub)はOAS の形式で書かれたyaml やjson ファイルからコード生成を自動でやってくれるソフトウェアです。

今回はQiita API を題材にして、それを叩くAndroid アプリを作ります。

完成品

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

UserApi.kt
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>>


    ...
}
User.kt
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,
    ...
}

環境

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 という便利なサイトがあるので活用していきます。
このサイトでは、yaml を編集すると、逐次右側の画面が更新され、正しく解釈されているかを確認できたり、実際にAPI を叩いて結果を確認したりすることが可能です。

スクリーンショット 2020-10-08 14.49.55.png

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

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

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 に詳しく載っています。

今回は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

これを実行すると、自動生成されたファイルを見ることができます。

テンプレートの修正

OpenAPI Generator も完璧ではありません。
例えば、v5.0.0-beta2 では自動生成される ApiClient.kt ファイルで最新版のRetrofit2 に対してのエラーが発生します。
image.png
これをテンプレートを修正することで解決してみます。(この問題は現在 PR が出されており、次の v5.0.0 では修正されると思います。1

まず、プロジェクトのルートに template ディレクトリを作り、OpenAPI Generator にある テンプレート をダウンロードして配置します。
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 などの便利なクラスを利用することでかなり楽に書くことができます。

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 で使えるコードが生成されます。

ItemApi.kt
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>

}

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

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 を出してしまっていました。。。

29
17
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?