swagger-codegen テンプレートとロジックのカスタマイズ

swagger-codegenのテンプレとロジックのカスタマイズ

前回の記事から引き続き swagger-codgenをさわっていきます.

今回はswagger-codegen(以下codegen)のカスタマイズ方法を記載します. カスタマイズ方法については以下の記事を参考にしていますが, この記事に乗っていないことをなるべく記述していきたいと思います!

こんなに簡単! Swagger Codegenのカスタマイズ https://qiita.com/Quramy/items/c583f3213f0b77ff1bac

なお最終的なファイル構成をgithubに上げているのでそちらも併せてご確認ください.

既存のテンプレートで動かす

まずはカスタマイズする前にどのような出力が行われるかを確認してみます. 前回の記事にもjarを使用した実行方法を記載していますが,今回はgradleを使用した方法を紹介します.

gradleの準備

gradleは依存解決+ビルドを行ってくれる支援ツールです. jvm系言語のビルドによく使用されますがC++などのビルドにも対応しているらしいです. 公式ページに記載があるように,パッケージマネージャからinstallするのがおすすめです.

macの場合はbrewで,windowsの場合はchocolaty経由でインストールしましょう. (chocolatyはwindows向けのパッケージマネージャです.インストールもinstallにあるスクリプトをcmdに張り付けるだけなのでぜひ導入しましょう.)

 brew install gradle # mac
 choco install gradle # windows

設定ファイルの準備

codegenを実行するためのbuild.gradleを任意のディレクトリに用意します. また生成に使用するサンプルとしてここからサンプルのSwagger定義を取得してswagger.jsonとして保存します

build.gradleはgradle用の設定ファイルです.

plugins {
  id 'org.hidetake.swagger.generator' version '2.12.0'
}

repositories {
  jcenter()
}

dependencies {
  // Add dependency for Swagger Codegen CLI
  swaggerCodegen 'io.swagger:swagger-codegen-cli:2.3.1'
}

swaggerSources {
  example{
    inputFile = file('swagger.json') //swagger定義ファイル
    code {
      language = 'spring'  //生成先のフレームワーク/言語
    }
  }
}

この設定を用意することでgradle-swagger-generator-plugin を利用してcodegenを動作させられます. また同時にgroovyを使用できるようプラグインを設定しています.

実行

以下のコマンドでcodegenプラグインを実行できます.

gradle generateSwaggerCode

これでbuildディレクトリ以下にソースコードが生成されました. 生成対象はbuild.gradlelanguage = 'spring'と設定したため,springbootを使用したサーバサイドのコードが生成されました.

# tree build
build
└── swagger-code-example
    ├── README.md
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── io
            │       └── swagger
            │           ├── RFC3339DateFormat.java
            │           ├── Swagger2SpringBoot.java
            │           ├── api
            │           │   ├── ApiException.java
            │           │   ├── ApiOriginFilter.java
            │           │   ├── ApiResponseMessage.java
            │           │   ├── NotFoundException.java
            │           │   ├── PetApi.java
            │           │   ├── PetApiController.java
            │           │   ├── StoreApi.java
            │           │   ├── StoreApiController.java
            │           │   ├── UserApi.java
            │           │   └── UserApiController.java
            │           ├── configuration
            │           │   ├── CustomInstantDeserializer.java
            │           │   ├── HomeController.java
            │           │   ├── JacksonConfiguration.java
            │           │   └── SwaggerDocumentationConfig.java
            │           └── model
            │               ├── Category.java
            │               ├── ModelApiResponse.java
            │               ├── Order.java
            │               ├── Pet.java
            │               ├── Tag.java
            │               └── User.java
            └── resources
                └── application.properties

language='go'と設定してやるとgolang向けのクライアントのコードを生成できます.

# tree build
build
└── swagger-code-example
    ├── README.md
    ├── api
    │   └── swagger.yaml
    ├── api_client.go
    ├── api_response.go
    ├── category.go
    ├── configuration.go
    ├── docs
    │   ├── Category.md
    │   ├── ModelApiResponse.md
    │   ├── Order.md
    │   ├── Pet.md
    │   ├── PetApi.md
    │   ├── StoreApi.md
    │   ├── Tag.md
    │   ├── User.md
    │   └── UserApi.md
    ├── git_push.sh
    ├── model_api_response.go
    ├── order.go
    ├── pet.go
    ├── pet_api.go
    ├── store_api.go
    ├── tag.go
    ├── user.go
    └── user_api.go

カスタマイズしたい対象を見つける

カスタマイズ対象のピックアップ

さて今回のテーマはカスタマイズですが,まずはカスタマイズしたい個所をピックアップします. codegenのテンプレートは様々な言語に対応していますが 中には手で修正しないとビルドに成功しないテンプレートもあります. 今回はcodegenの設定を以下のように設定してコードを生成します.

build.gradle

//抜粋
  example{
    inputFile = file('mytemplate/ninja.yaml') //swagger定義
    code {
      language = 'java' 
      configFile = file('config.json')
    }
  }

mytemplate/ninja.yaml

# 抜粋
definitions:
  Person:
    type: "object"
    required:
    - "name"
    properties:
      id:
        type: "integer"
        format: "int64"
      name:
        type: "string"
        example: "doggie"
      group:
        type: "string"
        description: "pet status in the store"
        enum:
        - "伊賀"
        - "甲賀"
        - "はぐれ"

すると,コンパイルした際に以下のようなエラーが出ます.

 警告: 識別子として'_'が使用されました
    _("伊賀"),

これはEnumクラスの定義として以下のようなコードが出力されているためです.

  /**
   * pet status in the store
   */
  @JsonAdapter(GroupEnum.Adapter.class)
  public enum GroupEnum {
    _("伊賀"),
    
    _("甲賀"),
    
    _("はぐれ");

    private String value;

すこし調べたところ,これはswagger定義にあるEnumを以下のようにEnumの定数名に変換しているためでした.

String var = value.replaceAll("\\W+", "_").toUpperCase();

当該コード

この処理で全角文字をすべて_に変換しているために起きている問題のようです. 今回はこの問題を回避するようなカスタマイズを行います. (ちなみにこの全角をEnumに設定するとうまくコード生成されないのがただのバグなのかSwaggerの定義にEnumに全角を設定してはいけないせいなのかは知りません!だれか詳しい人がいたら教えてください!!!><)

カスタマイズをはじめる

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

codegen参考記事にあるようにテンプレートとそのテンプレートに流し込むデータを作るロジック部に大別されます. イメージとしては以下のような感じです.

(swagger定義yaml)->ロジック部->(中間データ構造)->mustacheテンプレート

language毎のテンプレートファイルはcodegenのリポジトリのresouces以下で確認できます. language=javaの場合のテンプレートはこれです.

さて問題のコードがどのテンプレートファイルから生まれたのかをgrep等を使用して探してみると,どうやらmodelInnerEnum.mustacheが問題のコードに対応するようです.

テンプレートは一部のファイルだけのカスタマイズが可能なため,modelInnerEnum.mustachemytemplate/template/以下に保存し, build.gradleを以下のように設定します. こうすることでもともとのテンプレートをtemplateDir以下の同名ファイルでオーバーライドすることができます.

// 抜粋
  example{
    inputFile = file('mytemplate/ninja.yaml') //swagger定義
    code {
      language = 'java' 
      templateDir = file('mytemplate/template/')
    }
  }

Mustacheの文法に注意しながらテンプレートを適当に編集して保存します.

mytemplate/template/modelInnerEnum.mustache

   public enum {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} {
     {{#allowableValues}}
       {{#enumVars}}
+      // こんなこめんとをいれました
     {{{name}}}({{{value}}}){{^-last}},
     {{/-last}}{{#-last}};{{/-last}}
       {{/enumVars}}

この状態でgradle generateSwaggerCodeをしたら以下のように出力が変化しています.

  /**
   * pet status in the store
   */
  @JsonAdapter(GroupEnum.Adapter.class)
  public enum GroupEnum {
      // こんなこめんとをいれました
    _("伊賀"),
    
      // こんなこめんとをいれました
    _("甲賀"),
    
      // こんなこめんとをいれました
    _("はぐれ");

    private String value;

このようにテンプレートに追加した文字列がそのまま出力されるため, 簡単なカスタマイズであればこのテンプレートのカスタマイズだけで十分目的を果たせると思います.

ロジックのカスタマイズ

さて先ほど

(swagger定義yaml)->ロジック部->(中間データ構造)->mustacheテンプレート

と紹介しましたが,このようにテンプレートに埋め込む中間データ構造の値を変化させたい場合はロジックのカスタマイズが避けられません.

ロジック部のデータモデル

さてロジック部において各プロパティはCodegenPropertyクラスで扱われています.今回はこのへんenumVars.nameを別名で上書きすることにします.

Vender Extensions

ところで,Swagger定義では,Extensionsと呼ばれるx-から始まるカスタムプロパティを様々な項目に設定することができます. こちらの記事では,このextensions(Vender Extensions)を使用して中間データ構造に値を設定し,その値をテンプレートで参照することでカスタマイズを行っています. 今回はこのextension機能を利用してCodegenPropertyに値を渡し,ロジック内で特定のextensionがついていた場合はその値ででEnumの定数名を上書きします.

ロジックの継承とオーバライド

さきほどからlanguage='hoge'のように出力先の名前を指定していますが, ここの設定にはCodegenConfigインターフェースを継承したクラスのFQCN(パッケージ名とクラス名)を指定すればそのクラスを使用してくれます. したがって,カスタマイズしたいクラスを継承したクラスを自分で定義してそれをクラスパスに追加することで,任意の処理を既存の処理に挟み込むことができます.

というわけでlanguage='java'の際に使用されるJavaClientCodegenを継承したクラスをGroovyで定義してコンパイルしてクラスパスに追加して,そのクラス名をlanguageに指定して実行することでロジックのカスタマイズができました. (最終的ファイルをgithubに上げておいたので参考にしてください)

mytemplate/logic/MySwaggerLogic.groovy

import io.swagger.codegen.*;
import io.swagger.codegen.languages.*;

class MySwaggerLogic extends JavaClientCodegen {
    @Override
    public void updateCodegenPropertyEnum(CodegenProperty var) {
        super.updateCodegenPropertyEnum(var)
        if(var.vendorExtensions.containsKey("x-enum-names")){
            def alternames=var.vendorExtensions['x-enum-names']
            def enums=var.allowableValues["enumVars"]
            if(alternames.size()!=enums.size()){
                return
            }
            alternames.eachWithIndex{str,i->
                enums[i]["name"]=str
            }
        }
    }
}

build.gradle

plugins {
  id 'groovy'  // groovyでロジックを記述する際に使用
  id 'org.hidetake.swagger.generator' version '2.12.0'
}

repositories {
  jcenter()
}

dependencies {
  // Add dependency for Swagger Codegen CLI
  swaggerCodegen 'io.swagger:swagger-codegen-cli:2.3.1'
  // groovyのクラスファイルをクラスパスに追加
  swaggerCodegen files('build/classes/main/')
  swaggerCodegen localGroovy()
  // groovyコンパイル用の設定
  compile 'io.swagger:swagger-codegen-cli:2.3.1'
  compile localGroovy()
}

swaggerSources {
  example{
    inputFile = file('mytemplate/ninja.yaml') //swagger定義
    code {
      language = 'MySwaggerLogic' 
      configFile = file('config.json')
      templateDir = file('mytemplate/template/')
    }
  }
}
// swaggerSourceの前にgroovyをコンパイル
swaggerSources.example.code.dependsOn compileGroovy 
//ロジック記述ファイルの場所
sourceSets.main.groovy.srcDirs=['mytemplate/logic']

結果

  /**
   * pet status in the store
   */
  @JsonAdapter(GroupEnum.Adapter.class)
  public enum GroupEnum {
      // こんなこめんとをいれました
    Iga("伊賀"),
    
      // こんなこめんとをいれました
    Kouga("甲賀"),
    
      // こんなこめんとをいれました
    Alone("はぐれ");

    private String value;

想定したとおりにカスタマイズすることができました.

まとめと補足

本記事ではswagger-codegenを使用してコードを生成する方法,そしてテンプレートとロジックのカスタマイズ方法を解説しました.

個人的には前回の記事 にscaffoldingにcodegenを使ってみてはと書いた手前あれですが,用意されているテンプレートによってはいろいろなカスタマイズが必要になるのでなかなか良しあしがあるように感じます.本当に最初だけにとどめてそのあとは直接生成したコードを編集するのが楽な気がします...

補足

  • codegenのテンプレートはバージョンが変わるとところどころ変わっていることがあるので,github等でテンプレートを見る際は手元のcodegenのバージョンに注意してください
  • swagger仕様には2.0,3.0,3.1?の主なバージョンがありますが,codegenの3.0対応はあまり進んでいないらしい上にそれが原因でフォークプロジェクトができているようです