サーバーがJsonModel編集したら自動でAndroid側にプルリクが飛んでくる機構を作ってみた
釘宮愼之介
この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2017 の投稿記事です。
こんにちは、釘宮 (@kgmyshin) です。
serverがjson model変更したら ci 回って、android用にswagger-codegeで自動生成して、androidプロジェクトでjson modelをreplaceして、プルリクを投げる
まで自動かしたい
— 有象無象 (@kgmyshin) August 29, 2017
今回はこれを実際にやってみることにしました。
ところどころ力技感あったり、やり方があれだったりするかもしれないのは時間が足りなかったということをお察しいただきたく、また参考にされる場合は各自やりやすい方法でカスタマイズしていただけるとありがたいです。
早速やって行きましょう!
Android側で最新swagger.jsonを取ってきて更新できるようにする
swagger-codegeを使ってみる
まずはswagger-codegen-cliのダウンロードをします。
wget http://central.maven.org/maven2/io/swagger/swagger-codegen-cli/2.2.3/swagger-codegen-cli-2.2.3.jar -O swagger-codegen-cli.jar
次に swagger.json
をダウンロードします。
wget https://kgmyshin.github.io/swagger-auto-server-sample/swagger.json
ダウンロードした swagger-codegen-cli と swagger.json を使って generate コマンドを実行します。
java -jar swagger-codegen-cli.jar generate -i swagger.json -l kotlin --model-package com.kgmyshin.swagger.sample.api.json --model-name-suffix Json -o dist
生成された com/kgmyshin/swagger/sample/api/json/Pet.kt
を見てみます。
data class PetJson (
val name: kotlin.String,
val photo_urls: kotlin.Array,
val id: kotlin.Long? = null,
val category: CategoryJson? = null,
val tags: kotlin.Array? = null,
val status: kotlin.String? = null
) {
}
swagger-codegenをカスタマイズする
先の生成されたものをより自分が望むものに修正していきます。今回修正したいことは次の通り。
- それぞれに
@SerializedName("param_name")
をつけたい - メンバーがスネークケースになっているのを、ローワーキャメルケースにしたい
- ファイル名を
PetJson.kt
にしたい kolin.Int
などではなくInt
としたい
1 ) はテンプレートの修正によって対応できますが、 2 )、3 )、4 ) に関しては Codegen
クラスを作って対応することになります。
1. それぞれに @SerializedName("param_name") をつけたい
codegen-cli
はそれぞれの言語に対応したテンプレートを元に生成しています。テンプレートは自分でカスタマイズしたものを -t
オプションで設定することが可能です。
kotlin用のテンプレート をダウンロードしてきて、カスタマイズして行きます。
例えば元の data_class_opt_var.mustache
は下記のようになっています。
{{#description}}
/* {{{description}}} */
{{/description}}
val {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}}? = {{#defaultvalue}}{{defaultvalue}}{{/defaultvalue}}{{^defaultvalue}}null{{/defaultvalue}}
これを修正して @SerializedName("param_name")
をつけます。
{{#description}}
/* {{{description}}} */
{{/description}}
@SerializedName("{{{name}}}") val {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}}? = {{#defaultvalue}}{{defaultvalue}}{{/defaultvalue}}{{^defaultvalue}}null{{/defaultvalue}}
他にも諸々修正したものを こちら に上げておきました。
2. メンバーがスネークケースになっているのを、ローワーキャメルケースにしたい
codegen-cli
では -l
オプションに自作の CodeGen
クラスを渡すことが可能です。これによって、テンプレートの編集では不可能だったカスタマイズが可能になります。早速クラスパスに swagger-codegen-cli
を通したプロジェクトを作り、 自作 CodeGen
クラスを作って行きます。
もともと存在する KotlinClientCodegen
では、各プロパティの name
には snake_caseで、 baseName
にも snake_caseで, nameInCamelCase
には CamelCaseで格納されています。つまりlowerCamelCase形式のnameは存在しません。これを name
には snake_caseのまま、baseName
にCamelCaseを、 nameInCamelCase
に lowerCamelCase形式のnameを入れるように修正します。
override fun fromProperty(name: String?, p: Property?): CodegenProperty {
val property = super.fromProperty(name, p)
property.baseName = DefaultCodegen.camelize(name, false)
property.nameInCamelCase = DefaultCodegen.camelize(name, true)
return property
}
3.ファイル名を PetJson.kt にしたい
引き続き CodeGen
クラスを修正して行きます。下記のように toModelFilename
を override
してモデル名と合わせるようにすることで対処可能です。
override fun toModelFilename(name: String?): String {
return toModelName(name)
}
4. kolin.Int などではなく Int としたい
こちらはもともと this.typeMapping.put("integer", "kotlin.Int")
などととマッピングされていたところを上書きします。
this.typeMapping.put("string", "String")
this.typeMapping.put("boolean", "Boolean")
this.typeMapping.put("integer", "Int")
this.typeMapping.put("float", "Float")
this.typeMapping.put("long", "Long")
this.typeMapping.put("double", "Double")
this.typeMapping.put("array", "List")
this.typeMapping.put("list", "List")
this.typeMapping.put("map", "Map")
this.typeMapping.put("object", "Any")
this.typeMapping.put("binary", "List")
作成した CodeGenのプロジェクトは こちら にあります。
紹介しとことろ以外にも細々した修正箇所があるので、参考にする場合は合わせてご覧ください。
修正後の対応を確認する
以上でやりたいことの準備は整ったのであとはbuildして、コマンドを打ちます。
java -jar swagger-kotlin-codegen-1.0-SNAPSHOT.jar generate -i swagger.json -l com.kgmyshin.swagger.codgen.kotlin.PokoGenConfig -t kotlin-client --model-package jp.co.globis.hodai.infra.api.json --model-name-suffix Json -o dist
これによって望み通りの PetJson.kt
が生成されるようになりました。
data class PetJson (
@SerializedName("id") val id: Long,
@SerializedName("category") val category: CategoryJson,
@SerializedName("name") val name: String?,
@SerializedName("photo_urls") val photoUrls: List?,
@SerializedName("tags") val tags: List?,
@SerializedName("status") val status: String
)
シェルスクリプトにする
シェルスクリプトから一括で生成および既存コードのアップデートも行えるようにしておきましょう。自分の generate.sh
は下記のようになりました1)シェルスクリプト得意ではないので、変なところありましたら教えてくださいませ。
SCRIPT_DIR=$(cd $(dirname $0); pwd)
echo $SCRIPT_DIR
rm $SCRIPT_DIR/swagger.json
wget https://kgmyshin.github.io/swagger-auto-server-sample/swagger.json -O $SCRIPT_DIR/swagger.json
$SCRIPT_DIR/swagger-kotlin-codegen/gradlew -b $SCRIPT_DIR/swagger-kotlin-codegen/build.gradle jar
java -jar $SCRIPT_DIR/swagger-kotlin-codegen/build/libs/swagger-kotlin-codegen-1.0-SNAPSHOT.jar generate -i $SCRIPT_DIR/swagger.json -l com.kgmyshin.swagger.codgen.kotlin.PokoGenConfig -t $SCRIPT_DIR/kotlin-client --model-package com.kgmyshin.swagger.sample.api.json --model-name-suffix Json -o $SCRIPT_DIR/dist
find $SCRIPT_DIR/../src/main/kotlin/com/kgmyshin/swagger/sample/api/json | grep -v -E '/internal/' | grep "Json.kt" | xargs rm
cp -r $SCRIPT_DIR/dist/src/main/kotlin/com/kgmyshin/swagger/sample/api/json $SCRIPT_DIR/../src/main/kotlin/com/kgmyshin/swagger/sample/api
rm -rf $SCRIPT_DIR/dist
ここでやってることは下記のとおり。
- 古い
swagger.json
の削除 - 新しい
swagger.json
の取得 - カスタマイズした swagger-codegen-cli の build
- 生成コマンド実行
- 生成されたものを上書き
- お掃除
次のように gradleのタスクとしておくと良いでしょう。
task updateJson(type: Exec) {
commandLine(projectDir.absolutePath + "/scripts/generate.sh")
}
server側のciでpull-requestを送るようにする
こちら側での大雑把に流れは下記となります。
- ciが走ってdeployされる
- swagger.jsonが更新されているかどうか確認
- swagger.jsonが更新されていたらAndroidを更新
- プルリクエストを送る
swagger.jsonが更新されているかどうか確認
新しいjsonは wget などで取得。古い json は Androidのrepositoryから取得して diff コマンドで確認します。
wget https://kgmyshin.github.io/swagger-auto-server-sample/swagger.json swagger.json
DIFF_COUNT=$(diff swagger.json ./android/swagger.json | wc -l)
echo $DIFF_COUNT
if [ $DIFF_COUNT -eq 0 ]; then
echo "更新なし"
fi
echo "更新あり"
swagger.jsonが更新されていたらAndroidを更新
こちらは git clone して、先ほど作った generate.sh
を叩くだけです。
git clone git@github.com:kgmyshin/swagger-auto-android-sample.git
cd swagger-auto-android-sample
./app/scripts/generate.sh
プルリクエストを送る
今回は hub コマンドを使いました。
hubコマンドのsetup
hubコマンドをダウンロードしてpathを通します。また GITHUB_TOKEN
という環境変数に githubのtokenを入れておく必要があるので、ciの設定から入れておきましょう。
export HUB_VERSION=2.2.9
mkdir -p ~/bin/$HUB_VERSION
if [[ ! -e ~/bin/${HUB_VERSION}/hub ]]; then
wget --no-verbose https://github.com/github/hub/releases/download/v${HUB_VERSION}/hub-linux-amd64-${HUB_VERSION}.tgz -O h.tgz && tar xzf h.tgz && mv hub-linux-amd64-${HUB_VERSION}/bin/hub ~/bin/${HUB_VERSION}/;
fi
export PATH=~/bin/$HUB_VERSION/:$PATH
git config --global user.name &"username&"
git config --global user.email &"email&"
プルリクエストを送る
あとは編集をpushして hub pull-request
でプルリクエストを送るだけです。
git checkout -b pojo_update_$HASH
git add .
git commit -m "update pojo"
git push origin pojo_update_$HASH
hub pull-request -m "ref kgmyshin/swagger-auto-android-sample/commit/$HASH"
ciに設定する
以上のコマンドたちを一つのシェルスクリプトにして ci でdeploy後に走るようにしておきます。
今回は CircleCIを使いました。 config.yml
は下記のようになっております。 java
と node
が使える環境にしておきます。
version: 2
jobs:
build:
working_directory: ~/swagger-auto-server-sample
docker:
- image: joakimbeng/java-node
steps:
- checkout
: buildとかdeployとか
- run: ./scripts/pr-for-client-if-needed.sh
試してみる
サーバー側のswagger.ymlを編集する
Pet
で required
から photo_urls
を外してみます。
type: &"object&"
required:
- &"name&"
- - &"photo_urls&"
properties:
id:
type: &"integer&"
Androidのプルリクが自動で飛んでくる
無事にプルリクが来ました!
@SerializedName(&"name&") val name: String?,
@SerializedName(&"parent_name&") val parentName: String,
@SerializedName(&"sex&") val sex: Long,
- @SerializedName(&"photo_urls&") val photoUrls: List<String>?,
+ @SerializedName(&"photo_urls&") val photoUrls: List<String>,
@SerializedName(&"tags&") val tags: List<TagJson>,
@SerializedName(&"status&") val status: String
所感
前からやりたいことができたので満足することができました。
やってみた後に気づいたのですが、serverのciでAndroidのプロジェクトをfetchしてごにょごにょするよりも、デイリーでswagger.jsonの差分を確認してプルリクを送る、としたほうがAndroidだけで閉じることができるのでそちらの方が筋がいいなと思いました。実際、すでに個人的なあるプロジェクトはそうしています。
この記事を参考にする方はぜひそのやり方を試してみてくださいませ。
付録: generator-openapi-repoでSwagger環境を爆速で構築
この記事を書くにあたり、実は 「 Swaggerの環境を用意するまでがちょっとしんどいな…」 と内心感じていました。そんな時に generator-openapi-repo
というのを見つけました。本当に20分で全て準備でき、いたく感動したのでご紹介します。
generator-openapi-repoを使ってみる
事実20分ほどで全て完了しました。how-to-generate-your-repository に書いてあることに従って操作します。
npm install -g yo
npm install -g generator-openapi-repo
yo openapi-repo
上記のコマンドを実行すると、下記のように対話形式でもろもろ聞かれるので、ひとつずつ答えていきます。
_-----_
| | ╭──────────────────────────╮
|--(o)--| │ Welcome to the │
`---------´ │ OpenAPI-Repo generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? Do you already have OpenAPI/Swagger spec for your API? No
? Your API name (without API) Sample
? Specify name of GitHub repo in format User/Repo: kgmyshin/swagger-auto-server-sample
? Split spec into separate files: paths/*, definitions/* [Experimental]? No
? Prepare code samples Yes
? Install SwaggerUI Yes
あとはgithubにリポジトリをpushし、 READMEに Steps to finish という項目があるのでこれに従うだけです2)実際には空でもいいので gh-pages のブランチを作ってpushするという手順が抜けてました。。
たったこれだけの手順で下記のもろもろが準備完了します。
- Documentation(ReDoc)
- Swagger UI
- Look full spec:
apiを編集する
npm start
と叩くと下記のように swagger-editor
が起動するので、そちらからAPIを編集しましょう。
> Sample-openapi-spec@0.0.1 start /Users/kgmyshin/Work/swagger-auto/swagger-auto-server-sample
> gulp serve
[03:36:23] Using gulpfile ~/Work/swagger-auto/swagger-auto-server-sample/gulpfile.js
[03:36:23] Starting 'build'...
[03:36:23] Starting 'watch'...
[03:36:23] Finished 'watch' after 9.61 ms
[03:36:23] Starting 'edit'...
[03:36:23] Finished 'edit' after 7.24 ms
[03:36:23] swagger-editor started http://localhost:5000
[03:36:28] Finished 'build' after 4.39 s
[03:36:28] Starting 'serve'...
[03:36:28] Finished 'serve' after 139 μs
[03:36:28] Server started http://localhost:3000
[03:36:28] LiveReload started on port 35729