jsondiff: JSONの構造の一部を無視して差分をとれるGoのライブラリを書いた

github.com

背景

仕事でお世話になっているkayac/ecspressoの機能の中にローカルのタスク・サービス定義と現在使われている定義を比較して差分を出力してくれるものがある。

github.com

これから加えようとしている差分をプレビューできるだけではなく、たとえばデプロイしようとしているわけでもないのに差分があればローカルの定義が古びていることがわかるのでCIに組み込めると便利。

しかし実際に使おうとすると困る点が見つかった。

たとえばタスク定義にイメージタグを書く際に {{ must_env 'IMAGE_TAG' }} のように環境変数を参照している時に「イメージタグ 以外 に差分がない」ことを確認するのが難しいということ。

理想的には image を無視したJSONの構造を比較して差分が出せると良い。あるいは出力されるdiffをパースして image の差分は無視するとかが考えられる。

が、いずれもecspressoの機能とするにはだいぶ領分を外れているので、Pull Requestを送ろうにもましな実装が思いつかないのでどうしたものか。

ECSのタスク・サービス定義に限った問題ではなくJSONのdiffを取る時に一部構造を無視できれば良いので、JSONの差分を取るライブラリを作ることにした。 それがaereal/jsondiffになる。

紹介

ドキュメントのExampleを見てもらうとわかりやすい。

jsondiff.Ignore(query /* gojq.*Query: ".a" */) みたいに使う。

無視するJSON構造はgojqのクエリが使える。

github.com

gojqはid:itchynyさんによるjqのGo実装で、CLIコマンドだけではなくライブラリとしても使える。

他にJSONの構造を指定するクエリ言語のようなものというとJSON Pathが考えられたが、自分が普段JSONの構造を走査する用途ではjq (gojq) を使っているしライブラリ実装を提供していることを知っていたのでgojqを選んだ。

実装のポイント

.a, .b, .c みたいなクエリを .a = null | .b = null | .c = null のようなクエリに変換し、これを実行した結果を比較することで特定のキーを無視する機能を実現している。

元となる .a, .b, .c というクエリの構文木は以下のようになっている。

f:id:aereal:20220324001407p:plain

これを変換して .a = null | .b = null | .c = null というクエリにしたい。構文木としては以下の通り。

f:id:aereal:20220324001404p:plain

この木を作るには右端から作り、最後に処理したノードの左辺を更新していくのが合理的。

リストの右から処理していくといえばfoldRightである。しかしGoにそんな高級な関数はないのでfor文でデクリメントしていく実装になった。

see jsondiff/diff.go at 7e60f563b3601f48e6d4bf8c43210e4ac4614087 · aereal/jsondiff · GitHub

これを可視化すると以下のようになる。

f:id:aereal:20220324001415p:plain f:id:aereal:20220324001412p:plain f:id:aereal:20220324001409p:plain

こう淡々と書くと「ふーん」という感じだけれど、 len(xs) - 1 から開始してデクリメントしていけば末尾からイテレートできることに気がついた時はちょっと気持ち良くなれた。

むすび

当初欲しいと思えたものが作れて満足。

これをecspressoやlambrollに入れるかというと、どうしようかなと思っている。

というのもCLIコマンドも作ったので、 ecspresso render させてそれを比較させるのでも良いのではと思っている。

いずれにせよライブラリとCLIコマンドの両方を作っておくことで、外部からも単体でも利用しやすくなるというのはgojqのお世話になって実感したので良い物作りができた。