dictknife,jsonknifeの機能を整理

github.com

はじめに

json,yamlを扱う上で便利そうな機能を含んだ十得ナイフっぽい感じのライブラリを作っていた。それにコマンドをもたせた結果便利になってきたのでどういうときに何を使うかを整理しておく(完全に自分用のメモ)。

dictknife

dictknifeとjsonknifeという2つのコマンドを用意している。jsonknifeはjsonschemaやswaggerというかJSON関連の諸々に関連した機能を持っている。一方dictknifeはもう少しprimitiveなもの。

提供しているコマンドは以下

Usage: dictknife [OPTIONS] COMMAND [ARGS]...

Options:
  --log [NOTSET|ERROR|WARNING|WARN|INFO|DEBUG|CRITICAL]
                                  logging level
  -h, --help                      Show this message and exit.

Commands:
  concat     concat dicts
  diff       diff dict
  transform  transform dict

特に機能を網羅して紹介や説明をするという気も起きないし。あんまり意味が無い気がするのでどういうふうに使うかだけ書く。

concat

concatはその名前の通りに何かと何かを結合させるためのものではある。例えば以下の様な感じに。

$ dictknife concat --format json <(echo '{"name": "foo"}') <(echo '{"age": 20}')
{
  "name": "foo",
  "age": 20
}

ふつうのconcatと異なるのはネストした表現にも対応しているところ。

$ cat <<-EOS > person.yaml
definitions:
  person:
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
EOS
$ cat <<-EOS > group.yaml
definitions:
  group:
    type: object
    properties:
      members:
        type: array
        items: "#/definitions/person"
EOS
$ dictknife concat person.yaml group.yaml
definitions:
  person:
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
  group:
    type: object
    properties:
      members:
        type: array
        items: '#/definitions/person'

フォーマットを変換したい場合に

concatという名前の通り元々のファイルをつなげる事が目的のコマンドだった。とは言え1つのものに対してフォーマットを変えたい場合にも使う。意外と便利。 --dst で拡張子を付けても良いし。 –output-formatで出力するformatを変えても良い。

$ dictknife concat --output-format yaml --input-format json <(echo '{"name": "foo"}') <(echo '{"age": 20}')
name: foo
age: 20
$ dictknife concat --output-format toml --input-format json <(echo '{"name": "foo"}') <(echo '{"age": 20}')
name = "foo"
age = 20

対応している拡張子は今のところ.yaml(.yml),.json.toml。

diff

diffはその名の通りdiff。ソートした結果に対してdiffを調べてくれるので順序の処理が甘い感じのコマンドの出力結果を調べるのに便利。

$ cat <<-EOS > person0.yaml
person:
  name: foo
  age: 20
EOS
$ cat <<-EOS > person1.yaml
person:
  age: 20
  name: foo
EOS
$ dictknife diff person{0,1}.yaml

もちろんふつうのdiffでは差分が存在する。

$ diff -u person{0,1}.yaml
--- person0.yaml    2017-05-27 22:59:26.000000000 +0900
+++ person1.yaml    2017-05-27 22:58:47.000000000 +0900
@@ -1,3 +1,3 @@
 person:
-  name: foo
   age: 20
+  name: foo

deserializeされた後のsictに対するdiffを見るので順序には依存していない。ただ出力結果がJSONになっているのがちょっと不便といえば不便かもしれない。

$ cat <<-EOS > person2.yaml
person:
  age: 20
  name: bar
  nickname: b
EOS
$ dictknife diff person{0,2}.yaml
--- person0.yaml
+++ person2.yaml
@@ -1,6 +1,7 @@
 {
   "person": {
     "age": 20,
-    "name": "foo"
+    "name": "bar",
+    "nickname": "b"
   }
 }

あと、昔の実装では単にjson.dumps時にsort_keysをTrueにすれば良いだろうとだけ思っていたのだけれど。そうでもない。例えば、yaml上の表現としてkeyに数値が許されるのだけれど。JSON上はkeyは文字列と規程されている。そのため以下の様なデータを渡したとき失敗していた。

$ cat <<-EOS > status.yaml
200:
  ok
default:
  hmm
EOS
$ dictknife diff status.yaml status.yaml
TypeError: unorderable types: str() < int()

これに --normalize オプションを付けてあげるとkeyを文字列に変換してから比較してくれるので問題がなくなる。

$ dictknife diff --normalize status.yaml status.yaml

transform

dictknife transformはその名の通り変換。よく使うのは --code でのeval的な記述。

$ cat status.yaml
200:
  ok
default:
  hmm

$ cat status.yaml | dictknife transform --code='lambda d: [d,d,d]'
- 200: ok
  default: hmm
- 200: ok
  default: hmm
- 200: ok
  default: hmm

上の例では特に意味のない変換だったけれど。昔はjsonknife derefとの組み合わせで使う事があった(今ではunwrap,wrapのペアやrefの指定方法の進化で不要になった)。

これが必要になったらある意味負け。ただloadとdumpを繰り返すような変換処理があった場合にこれ経由で呼びだすと便利みたいなことはある。

$ cat <<-EOS > upcase.py
def upcase(d):
    if hasattr(d, "keys"):
        return {upcase(k): upcase(v) for k, v in d.items()}
    elif isinstance(d, (list, tuple)):
        return [upcase(d) for e in d]
    else:
        return str(d).upper()
EOS
$ cat <<-EOS > person.yaml
definitions:
  person:
    type: object
    properties:
      name:
        type: string
      age:
        type: integer
EOS
$ cat person.yaml | dictknife transform --function "./upcase.py:upcase"
DEFINITIONS:
  PERSON:
    PROPERTIES:
      AGE:
        TYPE: INTEGER
      NAME:
        TYPE: STRING
    TYPE: OBJECT