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を使用できるようプラグインを設定しています.
実行
gradle generateSwaggerCode
これでbuild
ディレクトリ以下にソースコードが生成されました.
生成対象はbuild.gradle
にlanguage = '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.mustache
をmytemplate/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を使ってみてはと書いた手前あれですが,用意されているテンプレートによってはいろいろなカスタマイズが必要になるのでなかなか良しあしがあるように感じます.本当に最初だけにとどめてそのあとは直接生成したコードを編集するのが楽な気がします...