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

こんにちは。VASILYでフロントエンドエンジニアをしている@Horie1024です。主にAndroidアプリの開発をしています。この記事はVASILY Advent Calendar 2017 16日目の記事になります。

この記事は、[Swaggerで定義したAPI仕様からRetrofitで使用するinterfaceを自動生成してみる Part1](https://qiita.com/Horie1024/items/cd4e161414466f9deff3)の続きとなります。併せて御覧ください。

# swagger.jsonのパース

[swagger-api/swagger-parser](https://github.com/swagger-api/swagger-parser)を使用してパースします。パース結果はSwaggerオブジェクトの格納され、簡単に参照できます。現在のところOpenAPI Specification 3.0には対応していませんが、3.0に対応した[swagger-parser v2.0.0がrc2までリリース](https://github.com/swagger-api/swagger-parser/releases/tag/v2.0.0-rc2)されています。

```kotlin
val swagger: Swagger = SwaggerParser().read("swagger.json")
```

# 取得した情報によるコード生成

取得したSwaggerオブジェクトの情報を元にどのようにコードを生成するか、[http://petstore.swagger.io/v2/swagger.json](http://petstore.swagger.io/v2/swagger.json)をサンプルとして解説します。

## data class

data classはswagger.jsonのdefinitionsフィールドから生成します。definitionsフィールドは`swagger.definitions`で参照できます。

forEachで`typeName`と`parameter`を取り出し、`TypeSpec.classBuilder`でclassを組み立てます。この時、`KModifier.DATA`を指定することでdata classの作成が可能です。

```kotlin
swagger.definitions.forEach { typeName, parameter ->
　　　　val typeSpecBuilder = TypeSpec.classBuilder(typeName).addModifiers(KModifier.DATA)
}
```

さらに、`parameter.properties`でプロパティを取得できるので、各プロパティについて`PropertySpec.builder`でプロパティを組み立て`TypeSpec.classBuilder`の`addProperty`で追加します。同時に、`FunSpec.constructorBuilder`にプロパティと同名のパラメーターを追加します。

```kotlin

swagger.definitions.forEach { typeName, parameter ->

    val typeSpecBuilder = TypeSpec.classBuilder(typeName).addModifiers(KModifier.DATA)

    val funSpecBuilder = FunSpec.constructorBuilder()
    parameter.properties.forEach {

        val propertyName = it.key.split("_").let { it[0] + it.getOrElse(1, { "" }).capitalize() }

        val type = when (it.value.type) {
            "string" -> String::class.asTypeName()
            "number" -> Int::class.asTypeName()
            "integer" -> Int::class.asTypeName()
            "array" -> {
                val type = it.value.getDefinitionsTypeName()
                if (type != null) {
                    ParameterizedTypeName.get(List::class.asTypeName(), ClassName("", type))
                } else {
                    Any::class.asTypeName()
                }
            }
            else -> {
                ・・・
            }
        }

        typeSpecBuilder
                .addProperty(PropertySpec.builder(propertyName, type)
                        .initializer(propertyName)
                        .build())
        funSpecBuilder.addParameter(propertyName, type)
    }
}
```

最後に`FileSpec.builder`でファイルとしてまとめればdata classの完成です。

```kotlin

swagger.definitions.forEach { typeName, parameter ->

    val typeSpecBuilder = TypeSpec.classBuilder(typeName).addModifiers(KModifier.DATA)

    val funSpecBuilder = FunSpec.constructorBuilder()
    parameter.properties.forEach {

        val propertyName = it.key.split("_").let { it[0] + it.getOrElse(1, { "" }).capitalize() }

        val type = when (it.value.type) {
            "string" -> String::class.asTypeName()
            "number" -> Int::class.asTypeName()
            "integer" -> Int::class.asTypeName()
            "array" -> {
                val type = it.value.getDefinitionsTypeName()
                if (type != null) {
                    ParameterizedTypeName.get(List::class.asTypeName(), ClassName("", type))
                } else {
                    Any::class.asTypeName()
                }
            }
            else -> {
                ・・・
            }
        }

        typeSpecBuilder
                .addProperty(PropertySpec.builder(propertyName, type)
                        .initializer(propertyName)
                        .build())
        funSpecBuilder.addParameter(propertyName, type)
    }

    FileSpec.builder(dataClassPackageName, typeName)
            .addType(typeSpecBuilder.primaryConstructor(
                    funSpecBuilder.build())
                    .build())
            .build()
            .writeTo(System.out)
}
```

`writeTo`で書きだすと以下のようなdata classが生成されます。

```kotlin
import kotlin.Int
import kotlin.String

data class User(
    val id: Int,
    val username: String,
    val firstName: String,
    val lastName: String,
    val email: String,
    val password: String,
    val phone: String,
    val userStatus: Int
)
```


## Retrofit Interface

Retrofitで使用するInterfaceを生成するまでの流れを以下のようになります。

1. tagsでpathをまとめる
2. tagに紐付いたpathについて抽象メソッドを作成
3. ファイルとしてまとめ、コードを生成

### tagsでpathをまとめる

swagger.jsonではpathsフィールドで仕様が定義されています。このpath毎にファイルを作成を作成しても良いのですが、その場合`/pet`と`/pet/findByStatus`についての抽象メソッドがそれぞれ別のinterfaceとして別ファイルに定義されます。これらはpetに対する操作なので、一つのinterfaceにまとめてしまうのが自然な気がします。したがって、各path定義の`tags`フィールドの値でpathをグルーピングします。

```kotlin

val tagList = mutableMapOf<String, MutableMap<String, MutableList<Map.Entry<HttpMethod, Operation>>>>()
swagger.paths.forEach { path, spec ->
    spec.operationMap.map {
        tagList.getOrPut(it.value.tags[0], { mutableMapOf() }).getOrPut(path, { mutableListOf() }).add(it)
    }
}
```

### tagに紐付いたpathについて抽象メソッドを作成

各tagに紐付けられたpathについてforEachを回し、get、postと行ったメソッド毎に抽象メソッドを作成しています。`operationMap`というパラメータ名でMapを受け取り、operationMapを`method`と`operation`に分解宣言します。operationにはmethodが行う操作についての情報が含まれるので、そこから`FunSpec.builder`で抽象メソッドを組み立てていきます。

抽象メソッド名には、`operationId`を使用し、`KModifier.ABSTRACT`を指定して抽象メソッドとします。`FunSpec.Builder.buildHttpMethodAnnotation`、`Operation.getReturnType`、`FunSpec.Builder.buildParameters`は独自に定義した拡張関数で、operation情報からAnnotation、戻り値、パラメータの情報を抜き出し、抽象メソッドへ追加します。こうして組み立てた抽象メソッドは`funList`にまとめておきます。

```kotlin
tagList.forEach {
    val (tag, paths) = it
    val funList = mutableListOf<FunSpec>()

    paths.forEach { path, operationMap ->
        operationMap.forEach {
            val (method, operation) = it

            val returnType = operation.getReturnType()

            FunSpec.builder(operation.operationId)
                    .addModifiers(KModifier.ABSTRACT)
                    .buildHttpMethodAnnotation(method, path, operation.getReturnType())
                    .buildParameters(operation.parameters)
                    .let { funList.add(it.build()) }
        }
    }
}
```

### ファイルとしてまとめ、コードを生成

各tagに紐付けられたpathについて、抽象メソッドを組み立てたので、これらをファイルにまとめます。`FileSpec.builder`に指定するファイル名は、tagのsuffixにServiceを付けたものにします。`TypeSpec.interfaceBuilder`で組み立てるintefaceも同名にしておきます。先程組み立てた抽象メソッドのリスト`funList`から抽象メソッドを取り出し、`addFunction`で追加します。これでinterfaceの組み立てが完了します。

```kotlin
tagList.forEach {
    val (tag, paths) = it
    val funList = mutableListOf<FunSpec>()

    paths.forEach { path, operationMap ->
        operationMap.forEach {
            val (method, operation) = it

            val returnType = operation.getReturnType()

            FunSpec.builder(operation.operationId)
                    .addModifiers(KModifier.ABSTRACT)
                    .buildHttpMethodAnnotation(method, path, returnType)
                    .buildParameters(operation.parameters)
                    .let { funList.add(it.build()) }
        }
    }

    FileSpec.builder(serviceClassPackageName, "${tag.capitalize()}Service")
            .addType(TypeSpec
                    .interfaceBuilder("${tag.capitalize()}Service")
                    .apply { funList.forEach { addFunction(it.build()) } }
                    .build())
            .build()
            .writeTo(System.out)
}
```

`writeTo`で書きだすと以下のようなinterfaceが生成されます。`Single<Unit>`が戻り値として定義されてしまったり、GET以外のメソッドが正しく生成されていないのは課題ですね。

```kotlin
import io.reactivex.Single.Single
import kotlin.String
import kotlin.Unit
import retrofit2.http.DELETE.DELETE
import retrofit2.http.GET.GET
import retrofit2.http.POST.POST
import retrofit2.http.PUT.PUT
import retrofit2.http.Path.Path
import retrofit2.http.Query.Query

interface UserService {
  @POST("/user")
  fun createUser(): Single<Unit>

  @POST("/user/createWithArray")
  fun createUsersWithArrayInput(): Single<Unit>

  @POST("/user/createWithList")
  fun createUsersWithListInput(): Single<Unit>

  @GET("/user/login")
  fun loginUser(@Query("username") username: String, @Query("password") password: String): Single<Unit>

  @GET("/user/logout")
  fun logoutUser(): Single<Unit>

  @GET("/user/{username}")
  fun getUserByName(@Path("username") username: String): Single<User>

  @PUT("/user/{username}")
  fun updateUser(@Path("username") username: String): Single<Unit>

  @DELETE("/user/{username}")
  fun deleteUser(@Path("username") username: String): Single<Unit>
}
```

# Gradle plugin化

data class、interfaceの生成をGradle Plugin化して簡単に実行できるようにします。今回は[こちら](https://www.thedroidsonroids.com/blog/how-to-create-gradle-plugin-in-kotlin)の記事を参考にKotlinで開発しました。

ソースコードはこちらです。

https://github.com/horie1024/gradle-generate-data-layer-plugin

## 使い方

まだportalに公開はしていないので公開したら更新しますが、classpathにプラグインのjarを指定し、`apply plugin`で有効化します。

pluginのjarファイルは、以下のコマンドで`build/libs`以下に生成されます。

```bash
./gradlew jar
```

DSLとして`generate_data_layer`を用意していて、ファイルの出力先や生成元にするswagger.jsonを指定できるようになっています。このままだと足りないので改善してく予定です。

```groovy

buildscript {
    dependencies {
        classpath files('build/libs/gradle-generate-data-layer-plugin-0.0.1.jar')
    }
}

apply plugin: 'com.horie1024.plugins.generate.datalayer'

generate_data_layer {
    data_layer_path = 'src/main/kotlin/data'
    swagger_json_path = 'http://petstore.swagger.io/v2/swagger.json'
}
```

`generateDataLayer`を実行するとdata classとinterfaceが生成されます。

```bash
./gradlew generateDataLayer
```

# まとめ

Swaggerで定義したAPI仕様からRetrofitで使用するdata classやinterfaceを生成することができました。不完全な部分や足りない部分について引き続き開発を続けていこうと思います。