Java
Groovy
swagger
WACULDay 3

こんなに簡単! Swagger Codegenのカスタマイズ

More than 1 year has passed since last update.

こんなに簡単! Swagger Codegenのカスタマイズ

waculでフロントエンジニアをしている @Quramy です。

今日はswaggerの話をしようと思います。

はじめに

waculでは、API定義にはJSON Hyper Schemaを用いており、swaggerは現在利用していません。しかし、先月のOpen API Initiative発足等、にわかに盛りあがり始めたのを見て「swaggerも触ってみようか」と思い始めている状況です。

現状のサービス開発フローにおいても

  1. prmdを使ってJSON Hyper SchemaでAPI仕様を作成
  2. 1.の出力からサーバ側(golang)とフロント(TypeSciprt)用のコードを作成
  3. 2.をプログラムから利用

という流れを踏んでおり, 特に2.の部分は自作のライブラリで実現しています(この辺りの事情や実装は僕よりも @tutuming@ukyo 達が詳しいです)。
swaggerにはswagger-codegenというswaggerの定義体から各種言語のコードを吐くツールがありますが、この出力結果がそのままサービス開発のコードに組み込めるとは思えません。

実際にJSON Hyper Schemaからswaggerに乗り換えるかどうかともかく、今日は「swaggerのコード出力部分を俺色に染め上げるためには何をすれば良いか」を考えていきたいと思います。

何はともあれ動かそうぜ

OS Xならbrew install swagger-codegenでinstallするのが手っ取り早いでしょう。わざわざswaggerのためだけにMavenとか突っ込むのもアホ臭いですので。
とはいえ、僕自身はHomeBrew使ってないので、Central Repoから直接jar引っ張ってきてjava叩いて実行しています。

wget https://repo1.maven.org/maven2/io/swagger/swagger-codegen-cli/2.1.4/swagger-codegen-cli-2.1.4.jar
java -jar swagger-codegen-cli-2.1.4.jar help generate \
  -l typescript-angular

正直、出力対象とする言語(-l)は何でも構わないのですが、waculではTypeScript + AngularJS の構成で開発していますのでコイツを選択しています。

ただ動かすだけなら、下記でお終いです。

java -jar swagger-codegen-cli-2.1.4.jar generate \
  -i pet_store.json \
  -l typescript-angular \
  -o dist_dir

inputとしているjsonはswagger editorから適当にsampleをexportしています。

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

さて、吐かれたコードに早速ケチを付けようではありませんか。

/// <reference path="api.d.ts" />

namespace API.Client {
    'use strict';

    export interface Pet {

        id: number;

        name: string;

        tag: string;

        createdAt: Date;
    }

}

うーん、<reference path="..." /> の部分なんですけど、dtsmで作ったbundle.d.tsが無いと不便だなぁ... とか思ったりするわけですよ。
このレベルの内容であればテンプレートをイジれば済む話です。

codegenレポジトリのresouceフォルダからパクリたいテンプレートをコピってきてローカルに配置してやりましょう。

ちなみにswagger-codegenのテンプレートエンジンにはmustacheが採用されてます。{}だらけでちょっと読み辛い。

mode.mustache(bundle.d.ts追加してみた)
/// <reference path="api.d.ts" />
/// <reference path="../../bundle.d.ts" />

namespace {{package}} {
    'use strict';

{{#models}}
{{#model}}
{{#description}}
    /**
     * {{{description}}}
     */
{{/description}}
    export interface {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{
{{#vars}}

{{#description}}
        /**
         * {{{description}}}
         */
{{/description}}
        {{name}}{{^required}}?{{/required}}: {{#isEnum}}{{classname}}.{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}};
{{/vars}}
    }

{{#hasEnums}}
    export namespace {{classname}} {
{{#vars}}
{{#isEnum}}

        export enum {{datatypeWithEnum}} { {{#allowableValues}}{{#values}}
            {{.}} = <any> '{{.}}',{{/values}}{{/allowableValues}}
        }
{{/isEnum}}
{{/vars}}
    }
{{/hasEnums}}
{{/model}}
{{/models}}
}

そんで-tにコピったテンプレート配置したフォルダを指定してやれば終わりですね。

java -jar swagger-codegen-cli-2.1.4.jar generate \
  -i pet_store.json \
  -t custom-tmpl \
  -l typescript-angular \
  -o dist_dir

出力ロジックのカスタマイズ編

とはいえ、テンプレートレベルのカスタマイズ自由度なんて高が知れているのは想像に難くありません。
先ほどのPet Interfaceに対して、「dateTimeはDateじゃなくてStringで扱いたい」とか言われたとしましょう。
こうなるとテンプレートというよりは{{datatype}}の中身の話になりそうです。

READMEには「io.swagger.codegen.CodegenConfig を実装したClassを作り、そのFQCNを -l オプションに食わせればいい」とかさらっと書いてますが、

  • いちいちJavaのコンパイルしなきゃならない
    • ほぼ必然的にMavenだのpom.xmlだのと向き合う必要がでてくる
  • 実行時に自分でつくったClassどこにおいて、ClassPathどうやって通すか、とか考えなきゃいけない
  • そもそもJava書きたくない

なんていうNodeJS脳に慣れ切った人間にとってはウンザリする話になってしまいます。

そうです、こんなときこそGroovyの出番です。

実際にモノを見せた方が早いですね。

custom-script.groovy
@Grab('io.swagger:swagger-codegen-cli:2.1.4')
import io.swagger.codegen.*;
import io.swagger.codegen.languages.*;

class MyTypescriptClientGen extends AbstractTypeScriptClientCodegen {

  String name = "my-typescript"
  String help = "Custom Typescript code generator"

  MyTypescriptClientGen() {
    super()
    this.modelTemplateFiles["model.mustache"] = ".ts"
    this.apiTemplateFiles["api.mustache"] = ".ts"
    this.apiPackage = "API.Client"
    this.modelPackage = "API.Client"
    this.supportingFiles.add(new SupportingFile("api.d.mustache", apiPackage().replaceAll(/\./, "${File.separatorChar}"), "api.d.ts"))

    // 型変換のoverride
    this.typeMapping.DateTime = "string";
  }

  // CLIへののkick
  public static main(String[] args) {
    SwaggerCodegen.main(args)
  }

}

上記はTypeScript用のクライアントコード生成器であり、実体は殆ど-l typescript-angularと変わりません.

java -jar ... の代わりに下記を叩けば動作します.

groovy custom-script.groovy generate \
  -i pet_store.json \
  -t custom-tmpl \
  -l MyTypescriptClientGen \
  -o dist_dir
  • script先頭の@Grab アノテーションのお陰でMavenやらpom.xmlが不要ですし、本家のレポジトリを全部cloneしてimportして,,, みたいな手間も不要です
  • 実行時コンパイルなので、ちょっと手を加えたいときも気軽に編集できます
  • スクリプト、テンプレートフォルダさえあれば充分なので、サービス本体のレポジトリにちょこんと置けちゃいます
  • Groovy script自体にmainメソッドを作ってオリジナルのCLIにdelegateするようにしているので、完全に同じオプションが使えます
  • コードジェネレータにありがちなMap<String, ?> に対する処理が連想配列風に記述できるので、js使いに優しいです

等など、良い事ずくめです。

また、swagger-codegenの各言語出力Class(例えば AbstractTypeScriptClientCodegen.java)は、1言語 : 1 Java Classの構成なので、拡張しても単一のGroovy Scriptに収めやすそうです。
ただし、swagger-codegenの内部構造をある程度知らないとそもそも拡張ができないのも事実ですので、そこはレポジトリをcheckoutするなり、swagger-codegen-sources.jarを持ってくるなりして、参照しながら作業するとよいと思います。

おわりに

今回のswagger-codegenについてまとめると、下記でしょうか。

  • テンプレートの.mustacheと拡張ロジックの.groovyを用意することで、ポータビリティの高いswaggerのコード生成器を作ることが可能

Groovyを書いたのは結構久しぶりだったんですが、開発補助ツール作るにはちょうど良い言語だなと改めて実感しました。

説明したテンプレートやGroovy Scriptは Quramy/swagger-codegen-customize-sample に同一のものを格納していますので、よければ参考にしてください。

swaggerについては、@tutuming今回のAdvent Calendar中にとっても役に立つことを書いてくれると豪語していたので、そちらにも乞御期待です。