サーバーがJsonModel編集したら自動でAndroid側にプルリクが飛んでくる機構を作ってみた

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2017 の投稿記事です。

こんにちは、釘宮 (@kgmyshin) です。

今回はこれを実際にやってみることにしました。

ところどころ力技感あったり、やり方があれだったりするかもしれないのは時間が足りなかったということをお察しいただきたく、また参考にされる場合は各自やりやすい方法でカスタマイズしていただけるとありがたいです。

早速やって行きましょう!

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をカスタマイズする

先の生成されたものをより自分が望むものに修正していきます。今回修正したいことは次の通り。

  1. それぞれに @SerializedName("param_name") をつけたい
  2. メンバーがスネークケースになっているのを、ローワーキャメルケースにしたい
  3. ファイル名を PetJson.kt にしたい
  4. 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 クラスを修正して行きます。下記のように toModelFilenameoverride してモデル名と合わせるようにすることで対処可能です。

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

ここでやってることは下記のとおり。

  1. 古い swagger.json の削除
  2. 新しい swagger.json の取得
  3. カスタマイズした swagger-codegen-cli の build
  4. 生成コマンド実行
  5. 生成されたものを上書き
  6. お掃除

次のように gradleのタスクとしておくと良いでしょう。

task updateJson(type: Exec) {
  commandLine(projectDir.absolutePath + "/scripts/generate.sh")
}

server側のciでpull-requestを送るようにする

こちら側での大雑把に流れは下記となります。

  1. ciが走ってdeployされる
  2. swagger.jsonが更新されているかどうか確認
  3. swagger.jsonが更新されていたらAndroidを更新
  4. プルリクエストを送る

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 は下記のようになっております。 javanode が使える環境にしておきます。

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を編集する

Petrequired から 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するという手順が抜けてました。

たったこれだけの手順で下記のもろもろが準備完了します。

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

脚注

脚注
1 シェルスクリプト得意ではないので、変なところありましたら教えてくださいませ
2 実際には空でもいいので gh-pages のブランチを作ってpushするという手順が抜けてました。