Swaggerで定義したAPI仕様からRetrofitで使用するinterfaceを自動生成してみる Part1

こんにちは。VASILYでフロントエンドエンジニアをしている@Horie1024です。主にAndroidアプリの開発をしています。この記事は[VASILY Advent Calendar 2017](https://qiita.com/advent-calendar/2017/vasily) 3日目の記事になります。

さて、先日[potatotips #45 (iOS/Android開発Tips共有会)](https://potatotips.connpass.com/event/70161/)で「[Swaggerで定義したAPI仕様からRetrofitで使用するinterfaceを自動生成してみる](https://speakerdeck.com/horie1024/swaggerdeding-yi-sitaapishi-yang-kara-retrofitdeshi-yong-suruinterfacewozi-dong-sheng-cheng-sitemiru)」というタイトルで発表してきました。この記事では、potatotipsで発表した内容について掘り下げてご紹介しようと思います。

長くなってしまったのでPart1、Part2に分割してご紹介します。

追記:　Part2を公開しました。
https://qiita.com/Horie1024/items/ccfa0ff50c0b4f17b932

# どんな内容なのか？

VASILYでは、Androidアプリのアーキテクチャとして以下の図に示すような3層のレイヤードアーキテクチャを基本にしています。このうち「データ層で使用するAPIへリクエストするコードを自動生成する」というのが先日発表した内容になります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43438/cf691de1-af95-b237-ac6a-69aa009ed620.png)

# なぜ自動生成したいのか？

自動生成したい理由は単純で面倒だからです。まずAndroidアプリでどのようにAPIリクエストを行うのかからご紹介します。(既にご存知の方は飛ばしてください🙇)

## APIへのリクエストを行うまでの流れ

AndroidアプリでのAPIへのリクエストには、[Retrofit](http://square.github.io/retrofit/)を使用しています。Retrofitでは、API仕様に沿ったinterface用意し、そのintefaceの実装をRetrofitが提供することでアプリからのAPIリクエストを可能にします。

例としてGitHub API v3の[List user repositories](https://developer.github.com/v3/repos/#list-user-repositories)にリクエストするには以下のようなinterfaceを用意します。戻り値は[RxJava](https://github.com/ReactiveX/RxJava)のSingle型で受け取るようにしています。RxJavaを使用しない場合Call型で受け取ります。

```kotlin
interface GitHubService {
  @GET("users/{user}/repos")
  fun listRepos(@Path("user") user: String): Single<List<Repo>>
}
```

次にRetrofitオブジェクトを用意し、GitHubServiceの実装を取得します。APIリクエストのレスポンスをRxJavaのSingleで受け取るため`RxJava2CallAdapterFactory`を追加しています。また、レスポンスのJSONをKotlinのオブジェクトにデシリアライズするために[Gson](https://github.com/google/gson)を使用するため`GsonConverterFactory`を追加しています。最近では、GsonではなくKotlinと相性の良い[Moshi](https://github.com/square/moshi)を使用する場合が多いかもしれません。弊社では、data classのコンストラクタパラメータが多すぎる場合にMoshiでのデシリアライズに失敗するバグを踏んでしまいプロジェクト途中からGsonに変えたことがあります。

```Kotlin

val retrofit = Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()

val service = retrofit.create(GitHubService::class.java)
```

実際のAPIリクエストは、Retrofitが提供する実装を利用し以下のように行います。

```Kotlin
service.listRepos(user = "horie1024")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe({
        // success
    } ,{
        // error
    })
```

## どの部分が面倒に感じるか？

APIのエンドポイントが数個なら問題ないのですが、エンドポイントが無数にある場合、そのエンドポイント毎にinterfaceを作成する必要があります。加えて、レスポンスJSONのデシリアライズ先となるdata classも作成する必要があります。

API仕様からinterfaceやdata classといったコードに書き起こす作業は単純作業となりがちです。さらにAPIに仕様変更があればそれに応じでコードを修正する必要があります。

実際やってみるととても面倒です:innocent:

したがって、この2つについてコードを自動生成してみます。

- Retrofitでのリクエストに使用するinterface
- レスポンスのデシリアライズ先として使用するdata class

# どのように自動生成するか？

自動生成に何が必要になるとかいうと

- コードから簡単に扱える構造化されたAPI仕様
- コードからコードを自動生成する手段

この2つが必要です。これらについてそれぞれ以下のツールを使うことで解決します。

- [Swagger](https://swagger.io/)
- [square/kotlinpoet](https://github.com/square/kotlinpoet)

さらに自動生成を簡単に実行するための手段として、Gradle Plugin化し、ターミナルからコマンド一つで簡単に実行できるようにします。

## Swagger

SwaggerはRESTful APIの設計、開発、ドキュメント化および使用方法の把握に役立つオープンソースのツール群です。OpenAPI Specification(OAS)の仕様に沿ってAPI仕様を記述することで、各ツールがドキュメントやモックを自動生成することが可能です。OASの仕様は[こちら](https://github.com/OAI/OpenAPI-Specification)にまとまっています。

### swagger-api/swagger-parser

Swaggerで定義されたAPI仕様を[swagger-api/swagger-parser](https://github.com/swagger-api/swagger-parser)でパースする事でコードから簡単に扱えるようになります。[swagger-api/swagger-codegen](https://github.com/swagger-api/swagger-codegen)を使いサーバ/クライアントのコードを自動生成する方法もありますが、後述するKotlinPoetを使うことでKotlinのコードを簡単に生成できる事から、今回は、swagger-parser + KotlinPoetの構成で実装しています。

## square/kotlinpoet

[square/kotlinpoet](https://github.com/square/kotlinpoet)は[Square](https://squareup.com/jp)製のOSSでKotlinのコードを簡単に生成できます。KotlinPoetの概要や使い方は以下の記事、資料が参考になります。

- [Generating Kotlin code with KotlinPoet](https://medium.com/square-corner-blog/generating-kotlin-code-with-kotlinpoet-119dc20f74d4)
- [Generating Kotlin Code (KotlinConf 2017) // Speaker Deck](https://speakerdeck.com/jakewharton/generating-kotlin-code-kotlinconf-2017?slide=67)

APIの詳細については[KDoc](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/)が用意されていますので必要に応じて参照します。また、基本的に[square/javapoet](https://github.com/square/javapoet)と使い方が似ているの、どう目的のコードを生成して良いかわからない時は、JavaPoetでどうするか調べるとKotlinPoetでも上手くいく場合があります。

# 自動生成の流れ

自動生成の流れを整理すると以下のような流れになります。

1. swagger-parserでSwaggerで定義されたAPI仕様をパース
2. パースしたAPI仕様の情報を元にKotlinPoetでコードを生成
3. 任意のパスに.ktファイルを書き出し

API仕様のパースは前述した通りswagger-parserを使用すれば簡単にパースすることができるので、KotlinPoetでinterfaceやdata classをどのように生成するかをコードを書いて試してみます。

GitHub API v3の[List user repositories](https://developer.github.com/v3/repos/#list-user-repositories)を例としてinterfaceやdata classをKotlinPoetで生成してみます。

## KotlinPoetでのinterfaceの生成

以下のようなinterfaceをKotlinPoetで生成します。

```kotlin

interface UsersService {
  @GET("/users/{username}/repos")
  fun listRepos(
      @Path("username") username: String,
      @Query("type") type: String? = null,
      @Query("sort") sort: String? = null,
      @Query("direction") direction: String? = null
  ): Single<List<Repo>>
}
```

### UsersService.ktファイルとinterfaceの生成

[FileSpec](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-file-spec/index.html)クラスを使用すると.ktファイルを作成できます。[FileSpec.Builder](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-file-spec/-builder/index.html)の第一引数にはpackage名を、第二引数にはファイル名を指定します。そして、[TypeSpec.interfaceBuildfer](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-type-spec/interface-builder.html)でUserService interfaceを作成し、[addType](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-file-spec/-builder/add-type.html)関数で追加しています。[FunSpec.Builder](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-fun-spec/-builder/index.html)では関数を作成することが可能で、`listRepos`関数を作成し、TypeSpec.interfaceBuilderの[addFunction](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-type-spec/-builder/add-function.html)関数でUserService interfaceに追加しています。

```kotlin
FileSpec.builder("", "UserService")
        .addType(TypeSpec
                .interfaceBuilder("UserService")
                .addFunction(FunSpec.builder("listRepos")
                        .addModifiers(KModifier.ABSTRACT)
                        .build()
                ).build()
        ).build()
        .writeTo(System.out)
```

このコードから生成されるコードは以下のようになります。

```kotlin  
interface UsersService {  
    fun listRepos()
}
```

### Annotationの追加

`listRepos`にAnnotationを追加するには、[FunSpec.Builder](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-fun-spec/-builder/index.html)の[addAnnotation](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-fun-spec/-builder/add-annotation.html)関数を使用します。追加するAnnotationは、[AnnotationSpec.Builder](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-annotation-spec/-builder/index.html)で組み立て、builderの引数には作成したいAnnotationのクラス参照を渡します。さらに[addMember](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-annotation-spec/-builder/add-member.html)関数でメンバーを追加できます。

```kotlin
FileSpec.builder("", "UserService")
        .addType(TypeSpec
                .interfaceBuilder("UserService")
                .addFunction(FunSpec.builder("listRepos")
                        .addModifiers(KModifier.ABSTRACT)
                        .addAnnotation(AnnotationSpec.builder(GET::class)
                                .addMember("\"/users/{username}/repos\"")
                                .build())
                        .build()
                ).build()
        ).build()
        .writeTo(System.out)

```

これで`listRepos`関数に`@GET`Annotationが追加されました。

```kotlin
interface UserService {
    @GET("/users/{username}/repos")
    fun listRepos()
}
```

### 戻り値型の追加

`listRepos`関数に戻り値の型を追加するには、[FunSpec.Builder](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-fun-spec/-builder/index.html)の[returns](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-fun-spec/-builder/returns.html)関数を使用します。`Single<List<Repo>>`を戻り値の型として指定したいので、[ParameterizedTypeName](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-parameterized-type-name/index.html)を使い組み立てます。[ParameterizedTypeName.get](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-parameterized-type-name/get.html)のKDocにある通り、第二引数にネストしてParameterizedTypeNameを指定する事が可能です。また、ここでは[ClassName](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-class-name/index.html)クラスで`Repo`型を作成しています。

```kotlin
FileSpec.builder("", "UserService")
        .addType(TypeSpec
                .interfaceBuilder("UserService")
                .addFunction(FunSpec.builder("listRepos")
                        .addModifiers(KModifier.ABSTRACT)
                        .addAnnotation(AnnotationSpec.builder(GET::class)
                                .addMember("\"/users/{username}/repos\"")
                                .build())
                        .returns(ParameterizedTypeName.get(Single::class.asTypeName(),
                                ParameterizedTypeName.get(List::class.asTypeName(),
                                        ClassName("", "Repo"))
                        )).build()
                ).build()
        ).build()
        .writeTo(System.out)
```

戻り値の型として`Single<List<Repo>>`が追加されました。

```kotlin
interface UserService {
    @GET("/users/{username}/repos")
    fun listRepos(): Single<List<Repo>>  
}
```

### パラメータの追加

`listRepos`関数にパラメータを追加するには、[FunSpec.Builder](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-fun-spec/-builder/index.html)の[addParameter](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-fun-spec/-builder/add-parameter.html)関数を使用します。追加するパラメータは、[ParameterSpec.Builder](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-parameter-spec/-builder/index.html)で組み立てます。builderの第一引数にはパラメータ名、第二引数には型を指定します。`asNonNullable`を指定する事でnullを許容しないパラメータとすることが出来ます。また、パラメータでも関数と同様にAnnotationを追加できます。

```kotlin
FileSpec.builder("", "UserService")
        .addType(TypeSpec
                .interfaceBuilder("UserService")
                .addFunction(FunSpec.builder("listRepos")
                        .addModifiers(KModifier.ABSTRACT)
                        .addAnnotation(AnnotationSpec.builder(GET::class)
                                .addMember("\"/users/{username}/repos\"")
                                .build())
                        .addParameter(ParameterSpec.builder("username", String::class.asTypeName().asNonNullable())
                                .addAnnotation(AnnotationSpec.builder(Path::class)
                                        .addMember("\"username\"")
                                        .build())
                                .build())
                        .returns(ParameterizedTypeName.get(Single::class.asTypeName(),
                                ParameterizedTypeName.get(List::class.asTypeName(),
                                        ClassName("", "Repo"))
                        )).build()
                ).build()
        ).build()
        .writeTo(System.out)
```

`listRepos`関数にパラメータを追加することができました。

```kotlin
interface UserService {
    @GET("/users/{username}/repos")
    fun listRepos(@Path("username") username: String): Single<List<Repo>>
}
```

最終的に、UserService interfaceを生成するコードは以下のようになります。

```kotlin

FileSpec.builder("", "UserService")
        .addType(TypeSpec
                .interfaceBuilder("UserService")
                .addFunction(FunSpec.builder("listRepos")
                        .addModifiers(KModifier.ABSTRACT)
                        .addAnnotation(AnnotationSpec.builder(GET::class)
                                .addMember("\"/users/{username}/repos\"")
                                .build())
                        .addParameter(ParameterSpec.builder("username", String::class.asTypeName().asNonNullable())                                    
                                .addAnnotation(AnnotationSpec.builder(Path::class)
                                        .addMember("\"username\"")
                                        .build())
                                .build())
                        .addParameter(ParameterSpec.builder("type", String::class.asTypeName().asNullable())                                    
                               .addAnnotation(AnnotationSpec.builder(Query::class)
                                        .addMember("\"type\"")
                                        .build())
                                .defaultValue("null")
                                .build())
                        .addParameter(ParameterSpec.builder("sort", String::class.asTypeName().asNullable())
                                .addAnnotation(AnnotationSpec.builder(Query::class)
                                        .addMember("\"sort\"")
                                        .build())
                                .defaultValue("null")
                                .build())
                        .addParameter(ParameterSpec.builder("direction", String::class.asTypeName().asNullable())
                                .addAnnotation(AnnotationSpec.builder(Query::class)
                                        .addMember("\"direction\"")
                                        .build())
                                .defaultValue("null")
                                .build())
                        .returns(ParameterizedTypeName.get(
                                Single::class.asTypeName(),
                                ParameterizedTypeName.get(
                                        List::class.asTypeName(),
                                        ClassName("", "Repo"))
                        )).build())
                .build())
        .build()
        .writeTo(System.out)
```

## KotlinPoetでのdata classの生成

data classの生成する流れは、基本的にinterfaceの生成と同様です。[addModifiers](https://square.github.io/kotlinpoet/0.x/kotlinpoet/com.squareup.kotlinpoet/-type-spec/-builder/add-modifiers.html)関数で`KModifier.DATA`を指定することでdata classとなります。

```kotlin

FileSpec.builder("", "Repo")
        .addType(TypeSpec.classBuilder("Repo")
                .addModifiers(KModifier.DATA)
                .addProperty(PropertySpec.builder("id", String::class)
                        .initializer("id")
                        .build())
                .addProperty(PropertySpec.builder("name", String::class)
                        .initializer("name")
                        .build())
                .addProperty(PropertySpec.builder("url", String::class)
                        .initializer("url")
                        .build())
                .primaryConstructor(FunSpec.constructorBuilder()
                        .addParameter("id", String::class)
                        .addParameter("name", String::class)
                        .addParameter("url", String::class)
                        .build())
                .build())
        .build()
        .writeTo(System.out)
```

これで以下のようなdata classを生成できました。

```kotlin
data class Repo(
        val id: String,
        val name: String,
        val url: String
)
```

# パースしたAPI仕様の情報を元にしてコードを生成

ここまでで、KotlinPoetでinterfaceやdata classの生成方法について理解することができました。そして、次にやるべきことはSwaggerで定義したAPI仕様から必要なデータを抜き出し、それを使用してのコード生成です。

Part2ではswagger-parserでのAPI仕様のパースから、そのデータを用いたコード生成、Gradle Plugin化までご紹介しようと思います。