OpenAPI Generatorでフロントエンドもバックエンドもコード自動生成する

[POLプロダクト Advent Calender 2020](https://qiita.com/advent-calendar/2020/pol)の21日目です。担当の高橋です。
前回ははじめちゃん(@HHajimeW)が[次元削減について](https://qiita.com/HHajimeW/items/e46eaddeb5be8035b3f8)まとめてくれましたので、そちらも是非読んでみてください。

今回は最近僕の方でPOLのプロダクト開発への導入を試みている、Open API Generatorについて実際のコードを踏まえて紹介したいと思います。

## Open API Generatorとは

> OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (both 2.0 and 3.0 are supported). Currently, the following languages/frameworks are supported:

[openapi-generator](https://github.com/OpenAPITools/openapi-generator)より引用。

Open API GeneratorはOpen API Specificationで記載されたResfful APIの仕様に基づいてコードやドキュメントを生成してくれます。以下のように多くのフレームワークをサポートしていますが、Open API Generatorの設定を変更せずデフォルト設定でコード生成すると、ビルドが通らなかったりLintでエラーになったりする場合があると思います(僕はありました)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130513/3665ce58-ad7d-30c8-e571-bc2a61545105.png)

今回はOpenAPI3.0で記載された[PetStore APIのドキュメント](https://editor.swagger.io/)を使って、Spring Boot + Kotlinのサーバーのコード、Axiosのクライアントコードを生成するまでの設定や生成されたコードについて紹介します。

## Kotlin + Spring Boot + Gradle

build.gradle.kts(一部抜粋)


```gradle
plugins {
    val kotlinVersion = "1.3.30"
    id("jacoco")
    id("org.springframework.boot") version "2.3.4.RELEASE"
    id("io.spring.dependency-management") version "1.0.10.RELEASE"
    id("org.openapi.generator") version "4.2.1"
    kotlin("jvm") version kotlinVersion
    kotlin("plugin.spring") version "1.3.72"
}
```

自動生成されたApiUtilのコンパイルが通らないので、[このコメントを](https://github.com/OpenAPITools/openapi-generator/issues/5476#issuecomment-593407391)参考にOpen API Generatorの4.2.1を使っています。

```gradle
// https://discuss.kotlinlang.org/t/kotlin-gradle-script-add-generated-kotlin-sources/17277
sourceSets {
    main {
        kotlin {
            sourceSets["main"].apply {
                kotlin.srcDir("src/main/generated")
            }
        }
    }
}
```

build配下を参照しても良いのですが`src/kotlin/generated`にコードを配置したかったので、`src/kotlin/generated`配下をコンパイル対象に追加しておきます。


```gradle
tasks.register("moveOpenApiGeneratedCode") {
    doLast {
        copy {
            from("${buildDir}/generated/.openapi-generator".toString())
            into(".openapi-generator")
        }
        copy {
            from("${buildDir}/generated/.openapi-generator-ignore".toString())
            into("./")
        }
        copy {
            from("${buildDir}/generated/src/main/kotlin".toString())
            into("src/main/generated")
        }
        copy {
            from("${buildDir}/generated/src/test/kotlin".toString())
            into("src/test/kotlin")
        }
    }
}
```

生成したコードを`src/main/generated`配下に移動する必要があり、それを実施するタスクを定義します。

```gradle
    withType<org.openapitools.generator.gradle.plugin.tasks.GenerateTask> {
        generatorName.set("kotlin-spring")
        inputSpec.set("path/to/open-api-specification.yaml")
        outputDir.set("$buildDir/generated".toString())
        apiPackage.set("jp.co.pol.petstore.api")
        invokerPackage.set("jp.co.pol.petstore.invoker")
        modelPackage.set("jp.co.pol.petstore.model")
        additionalProperties.set(mapOf("interfaceOnly" to "true"))
        finalizedBy("moveOpenApiGeneratedKotlinCode")
    }

```

`generatorName`は`kotlin-spring`、interfaceを使いたいので`interfaceOnly`を指定しています。生成したコードを移動するタスクを`finalizedBy`で実行しています。

Gradleの`openApiGenerate`タスクを実行すると`src/main/generated`配下にコードが生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130513/09120c63-12c6-57c6-8f4e-8fa152d3953e.png)

起動時に実行するクラスがinvoker配下に生成されます。

```kotlin
package jp.co.pol.petstore.invoker

import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
import org.springframework.boot.autoconfigure.SpringBootApplication


@SpringBootApplication
@ComponentScan(basePackages = ["jp.co.pol.petstore.invoker", "jp.co.pol.petstore.api", "jp.co.pol.petstore.model"])
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}
```

実際の起動クラスは自動生成されたクラスを継承して実装します(DDDも試してみたかったのでそういったパッケージ構成にしています)。

```kotlin
@SpringBootApplication
@EnableConfigurationProperties
@ComponentScan(
    basePackages = ["jp.co.pol.petstore.application", "jp.co.pol.petstore.domain", "jp.co.pol.petstore.infrastructure", "jp.co.pol.petstore.presentation"],
    basePackageClasses = [WebMvcConfig::class]
)
class LabBaseApplication : Application() {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            runApplication<LabBaseRApplication>(*args)
        }
    }
}
```

コントローラークラスがapi配下に生成されます。実際のコントローラークラスは自動生成されたクラスを実装して作成します。

```kotlin
package jp.co.pol.petstore.api

import jp.co.pol.petstore.model.ModelApiResponse
import jp.co.pol.petstore.model.Pet
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.validation.annotation.Validated
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.beans.factory.annotation.Autowired

import javax.validation.Valid
import javax.validation.constraints.DecimalMax
import javax.validation.constraints.DecimalMin
import javax.validation.constraints.Max
import javax.validation.constraints.Min
import javax.validation.constraints.NotNull
import javax.validation.constraints.Pattern
import javax.validation.constraints.Size

import kotlin.collections.List
import kotlin.collections.Map

@Validated
@RequestMapping("\${api.base-path:/v2}")
interface PetApi {


    @RequestMapping(
            value = ["/pet"],
            consumes = ["application/json", "application/xml"],
            method = [RequestMethod.POST])
    fun addPet( @Valid @RequestBody body: Pet
): ResponseEntity<Unit> {
        return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
    }


    @RequestMapping(
            value = ["/pet/{petId}"],
            method = [RequestMethod.DELETE])
    fun deletePet( @PathVariable("petId") petId: kotlin.Long
, @RequestHeader(value="api_key", required=false) apiKey: kotlin.String?
): ResponseEntity<Unit> {
        return ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
    }

    // 長いので他のfunctionは省略
}
```

またGradleの`openApiValidate`タスクでOpenAPI Specificationのバリデーションが実行できます。CIで都度実行するように設定すると良さそうです。タスクで検証するドキュメントのパスを設定する必要があります。

```gradle
tasks.withType<org.openapitools.generator.gradle.plugin.tasks.ValidateTask> {
    inputSpec.set("path/to/open-api-specification.yaml")
}
```

## Axios

devDependenciesに`@openapitools/openapi-generator-cli`を追加します。

```bash
yarn add --dev @openapitools/openapi-generator-cli
```

scriptsに`openapi-generator-cli`のコマンドを追加。

```json
# package.json
{
  "scripts": {
    "generate-client": "openapi-generator-cli generate -g typescript-axios -i path/to/open-api-specification.yaml -o path/to/generated-code"
  },
  "devDependencies": {
    "@openapitools/openapi-generator-cli": "^2.1.4",
  }
}
```

`openapitools.json`でOpen API Generatorのバージョンを指定できます(自動生成するとjsonファイルが追加されます)。[このIssue](https://github.com/OpenAPITools/openapi-generator/issues/7282)の問題を踏んだので、Typescriptのエラーが発生しないように`5.0.0-beta2`を指定しています。

```json
{
  "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
  "spaces": 2,
  "generator-cli": {
    "version": "5.0.0-beta2"
  }
}
```

また自動生成したコードはeslintでエラーが発生してしまうので、`.eslintignore`で無視するようにしておきます。


```bash
# コード生成
yarn generate-client
```

以下のようなコードが生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130513/da5003dd-287c-2c37-ea1c-d608b4856a62.png)

`api.ts`に全Restful APIの呼び出しを実行できるクラスが実装されています。APIのリクエストとレスポンスの型も生成されているので、ドキュメントさえ書けばモデルのコードを書かなくてもすむので助かります。

```typescript
/**
 * PetApi - object-oriented interface
 * @export
 * @class PetApi
 * @extends {BaseAPI}
 */
export class PetApi extends BaseAPI {
    /**
     * 
     * @summary Add a new pet to the store
     * @param {Pet} body Pet object that needs to be added to the store
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof PetApi
     */
    public addPet(body: Pet, options?: any) {
        return PetApiFp(this.configuration).addPet(body, options).then((request) => request(this.axios, this.basePath));
    }

    /**
     * 
     * @summary Deletes a pet
     * @param {number} petId Pet id to delete
     * @param {string} [apiKey] 
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof PetApi
     */
    public deletePet(petId: number, apiKey?: string, options?: any) {
        return PetApiFp(this.configuration).deletePet(petId, apiKey, options).then((request) => request(this.axios, this.basePath));
    }

    // 長いので省略
}
```

弊社の開発ではテスト環境、本番環境ごとにAPIサーバーのベースURLを切り替えたいので、自動生成されたAPI呼び出しクラスをそのまま使うのではなく、ファクトリークラスを使ってベースURLを切り替えられるようにしています。

```typescript
// api.tsのファクトリークラス
export const PetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
    return {
        /**
         * 
         * @summary Add a new pet to the store
         * @param {Pet} body Pet object that needs to be added to the store
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        addPet(body: Pet, options?: any): AxiosPromise<void> {
            return PetApiFp(configuration).addPet(body, options).then((request) => request(axios, basePath));
        },
        /**
         * 
         * @summary Deletes a pet
         * @param {number} petId Pet id to delete
         * @param {string} [apiKey] 
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        deletePet(petId: number, apiKey?: string, options?: any): AxiosPromise<void> {
            return PetApiFp(configuration).deletePet(petId, apiKey, options).then((request) => request(axios, basePath));
        },
        // 長いので省略
    };
};

// 自前で実装しているapi.ts
import axios from 'axios';
import { PetApiFactory } from 'path/to/generated-code/api';
import { createApiClient } from '../../createApi';

const baseURL = process.env.API_BASE_URL;

const axiosInstance = axios.create({
    baseURL,
    timeout: 30000,
    headers: { 'X-Requested-With': 'XMLHttpRequest', Pragma: 'no-cache' },
});

export const petApiService = PetApiFactory(undefined, baseURL, axiosInstance);
```

## おわり

以上、Open API Generatorの紹介でした。
次回はゲバさん(@guevara-net)です！