openapi-generator をカスタマイズする方法

この記事は[オプトテクノロジーズアドベントカレンダー(2018)](https://qiita.com/advent-calendar/2018/opt-technologies) 9 日目のエントリーです。

## 前書き

ウェブアプリケーションを作ると API とその仕様のドキュメントを用意することが多いと思います。最近は API の仕様をある規格にしたがって書き下し、それを元にクライアントを自動生成するような仕組みも広まってきています。

この記事では、そのような規格の一つである Open API を使ってクライアントを自動生成し、その出力結果をカスタマイズする方法について紹介します。

## OpenAPI とは

正確なところは下記のリンク先を参照していただくとして、大まかなところでいうと REST API の入出力を記述するための仕様です。  
元々は Swagger という名前でしたが、現在は OpenAPI という名前になっています。その流れで、関連ツールは `swagger-*` という命名のものも多いです。

- [GitHub](https://github.com/OAI/OpenAPI-Specification)
- [RESTful APIの記述標準化を目指す「Open API Initiative」をマイクロソフト、Google、IBMらが立ち上げ。Swaggerをベースに － Publickey](https://www.publickey1.jp/blog/15/open_api_initiative.html)

## openapi-generator

[OpenAPI Document](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-document) から API のクライアントやスタブ実装を生成するツールでメジャーなものとして `swagger-codegen` と `openapi-generator` の 2 つがあります。  
ここでは後者の　`openapi-generator` のカスタマイズ方法について記載していきます。

- 両者の関係性について: [OpenAPI Generator - community drivenで成長するコードジェネレータ - 暁](https://ackintosh.github.io/blog/2018/05/12/openapi-generator/)

## カスタマイズ

さて、いよいよ本題です。

基本は[Quramy さんの記事](https://qiita.com/Quramy/items/c583f3213f0b77ff1bac)を参考にして書いています。  
swagger-codegen の話ですが現時点ではそこまで乖離もないので眺めてもらうと generator カスタマイズの基本がわかると思うのでまずはご一読ください。

そうです。 openapi-generator では mustache を採用しているので条件分岐などはできないのです[^branch]。そんなに多くはないですが稀に条件分岐したいこともあるので今回はそんなケースに立ち向かっていきます。

[^branch]: 条件分岐するためのデータを埋め込んで **ランタイム** での分岐は可能です。


### まずは自前の Generator を作ってみる

イメージを掴むためほぼ継承元の挙動を変更しないようにして自前の Generator を作ってみます。

```groovy:gen1.groovy
@Grab(group = 'org.openapitools', module = 'openapi-generator-cli', version = '3.3.4')

import org.openapitools.codegen.*
import org.openapitools.codegen.languages.*

class MyCodegen extends TypeScriptNodeClientCodegen {

  static main(String[] args) {
    OpenAPIGenerator.main(args)
  }

  MyCodegen() {
    super()
  }

  String name = "my-codegen"
}

MyCodegen.main(args)
```

みての通りほとんど何も変更をしていません。これに公式のサンプルを与えてみます。

```bash
$ wget "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/3.0.2/examples/v3.0/petstore.yaml"
$ ls
gen1.groovy   petstore.yaml
$ groovy \
  ./gen1.groovy \
  generate \
  -i ./petstore.yaml \
  -g MyCodegen \
  -o ./output
$ tree output
output
├── api
│   ├── apis.ts
│   └── petsApi.ts
├── api.ts
├── git_push.sh
└── model
    ├── modelError.ts
    ├── models.ts
    └── pet.ts

2 directories, 7 files
```

### テンプレートのカスタマイズ

一から　Generator を作っても良いのですが、ここでは `MyCodegen` をそのまま使っていきます。  
まずはテンプレートをコピーします。(方法はなんでも良いです)

```bash
$ BASE_URL="https://raw.githubusercontent.com/OpenAPITools/openapi-generator/v3.3.4/modules/openapi-generator/src/main/resources/typescript-node"
$ mkdir template && cd template
$ wget "$BASE_URL"/gitignore
$ wget "$BASE_URL"/{api-all,api-single,api,git_push.sh,licenseInfo,model,models,package,tsconfig}.mustache
```

template フォルダ配下に typescript-node のテンプレートがコピーできたら、以下のようにして実行します。

```bash
$ rm -rf ./output
$ groovy \
  ./gen1.groovy \
  generate \
  -i ./petstore.yaml \
  -g MyCodegen \
  -t ./template \
  -o ./output
```

先ほどとの違いは `-t` 引数にテンプレートが置かれているディレクトリを指定したかどうかです。  
カスタマイズしようとしている内容がテンプレートの修正で達成できるのであれば、ここまでわかっていれば問題ありません。

### Generator のカスタマイズ

さていよいよ本丸です。まずは次の OpenAPI Document をご覧ください。

```yaml
openapi: "3.0.0"
info:
  version: 1.0.0
  title: random
paths:
  /random:
    get:
      operationId: random
      responses:
        default:
          description: "いろんな型が返ってくる"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Random"
components:
  schemas:
    Random:
      oneOf:
      - $ref: "#/components/schemas/TypeA"
      - $ref: "#/components/schemas/TypeB"
    TypeA:
      required:
        - type
        - name
      properties:
        type:
          type: string
        name:
          type: string
    TypeB:
      required:
        - type
        - age
      properties:
        type:
          type: string
        name:
          type: string
```

注目していただきたいのは `Random` の定義です。 `oneOf` を用いて定義されています。　現状の openapi-generator のコードではこれをうまく処理することができません[^oneof-support]。

[^oneof-support]: https://github.com/OpenAPITools/openapi-generator/pull/1360 この PR により、基本的な部分はテンプレートで処理できるようになっています。　しかし、任意の oneOf を表現しようと思うと言語レベルでそういったものが用意されていないとなかなか難しいでしょう。

幸いなことに TypeScript ではそういった機能(Union Types)が用意されているためテンプレートでうまく使ってやれば簡単に対応できそうです。
そうは言うものの　openapi-generator には特定のモデルだけ別のテンプレートを使うなどといった機能は用意されていないので、かなり大胆なことをする必要があります。

```groovy:gen2.groovy
@Grab(group = 'org.openapitools', module = 'openapi-generator-cli', version = '3.3.4')

import groovy.transform.EqualsAndHashCode
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.FromString
import org.codehaus.groovy.runtime.InvokerHelper
import org.openapitools.codegen.*
import org.openapitools.codegen.languages.*

@EqualsAndHashCode
class MyCodegenModel extends CodegenModel {

  boolean random = false
  boolean isRandom() { return this.random }

  String interfacesUnion = null

  static MyCodegenModel copy(CodegenModel source) {
    def target = new MyCodegenModel()

    InvokerHelper.setProperties(target, source.properties)

    return target
  }

}

class ModelProcessor {

  private static Map<String, Object> extractModelHash(Map<String, Object> objs, String modelName) {
    return objs?.get(modelName)?.get("models")?.get(0)
  }

  private static CodegenModel extractModel(Map<String, Object> objs, String modelName) {
    return extractModelHash(objs, modelName)?.get("model")
  }

  private static void setModel(Map<String, Object> objs, String modelName, CodegenModel model) {
    extractModelHash(objs, modelName).put("model", model)
  }

  static void replaceModel(Map<String, Object> objs, String modelName, @ClosureParams(value=FromString, options=["MyCodegenModel"]) Closure block) {
    def originalModel = extractModel(objs, modelName)
    def targetModel = MyCodegenModel.copy(originalModel)

    block(targetModel)

    setModel(objs, modelName, targetModel)
  }

}

class MyCodegen extends TypeScriptNodeClientCodegen {

  static main(String[] args) {
    OpenAPIGenerator.main(args)
  }

  MyCodegen() {
    super()
  }

  String name = "my-codegen"

  @Override
  Map<String, Object> postProcessAllModels(Map<String, Object> objs) {
    objs = super.postProcessAllModels(objs)

    ModelProcessor.replaceModel(objs, "Random") {
      it.random = true
      it.interfacesUnion = it.interfaces.join(" | ")
    }

    return objs
  }
}

MyCodegen.main(args)
```

`postProcessAllModels` と言うメソッドの挙動を書き換えて、独自のプロパティクラスへ差し替えることでテンプレート中で使える変数を増やして無理やり分岐できるようにしています。

```mustache:model.mustache
{{>licenseInfo}}
{{#models}}
{{#model}}
{{#tsImports}}
import { {{classname}} } from './{{filename}}';
{{/tsImports}}
{{^isRandom}}{{>model_generic}}{{/isRandom}}{{#isRandom}}{{>model_random}}{{/isRandom}}
{{/model}}
{{/models}}
```

```mustache:model_random.mustache
{{#description}}
/**
* {{{description}}}
*/
{{/description}}
export type {{classname}} = {{interfacesUnion}}
```

これ以外にも、もともと `model.mustache` だったものを `model_generic.mustache` にしていくらか行を削除しています。

どう考えても筋が良くないのですが、このようにして使える変数を増やさないと完全に別物のファイルを生成するのは難しいです。あるモデルに特化したテンプレートを指定できる仕組みが本体に入ると良さそうですが、提案できるほど考えられていないと言うのが現状です。

## まとめ

API 定義を複雑にすると、コードの生成周りが非常に厄介になると言うのがこの記事の教訓です。記事の中では言及していませんが、 openapi-generator の内部構造はなかなかわかりにくく、 IntelliJ などの IDE を使い、デバッガーを起動できないとカスタマイズは不可能なように思われます。生半可な覚悟でカスタマイズをしようとするのは避けた方が良いでしょう。