$loadと$concatが仲間に加わりました。

github.com

$loadと$concatが仲間に加わりました。これでようやくちょっとは便利な何かとして機能し始めてきたような気がします。これら2つが具体的にはどのような効果を持つのかを少し説明したいと思います。すごく雑にいうと以下の様な感じです。

  • $load – 他のファイルのデータを読み込むaction(関数)
  • $concat – 複数のデータ片をまとめるaction(関数)

それぞれについてもう少し詳しく説明していきます。

$load

はじめは$loadについて説明します。

$loadは別のファイルのデータを読み込む機能

$loadは以下の別のファイルの情報を読み込むactionと言いました。つまり入力として渡すyamlのファイルが分けられるようになったということです。例えば以下の様なmain.yaml,color.yamlというファイルをデータとして渡すと、

main.yaml

color:
  $load: ./color.yaml

color.yaml

color:
  cmyk:
    - C
    - M
    - Y
    - K
  rgb:
    - R
    - G
    - B

以下のような出力になります。

python main.py main.yaml
color:
  color:
    cmyk:
    - C
    - M
    - Y
    - K
    rgb:
    - R
    - G
    - B

yamlだと紛らわしいかもしれませんが。jsonで出力すると以下の様な形なのでcmykの子としてC,M,Y,Kなどが存在しています。

python main.py main.yaml
{
  "color": {
    "color": {
      "cmyk": [
        "C",
        "M",
        "Y",
        "K"
      ],
      "rgb": [
        "R",
        "G",
        "B"
      ]
    }
  }
}

一部分だけを取り出したい場合

一部分だけ取り出したい場合はどうすれば良いでしょう?幸い$loadはjson referenceの形式での読み込みに対応しています。なので先程の例で言えばRGBだけを取り出したい場合には以下の様に書くことができます。

main2.yaml

color:
  $load: ./color.yaml#/color/rgb

rgbだけを取り出すことができます。

color:
- R
- G
- B

例えば、設定ファイル全体の一部分だけを取り出して使うみたいなことができるようになります。

json referenceについて

ところでjson referenceと言うのはどういう形式かということも説明しておいたほうが良いかもしれません。雑にだけ説明します。

json referenceは、リソースのリンク先などを表す時に使います。例えばjsonschemaだったりswagger spec(OAS2.0)だったりでも使われています。簡単に言えばドキュメントの外部のリンク先とその特定のドキュメントの内部のリンク先を指す2つの階層から成り立っています。

<external area>#<internal area>

これらはそれぞれ省略することができます。外部のリンク先の方を省略した場合には現在のドキュメント(ファイル)自身、内部のリンク先の方を省略した場合にはドキュメント(ファイル)全体を表します。ちなみに内部のリンク先の指定の方法をjson pointerという形式で指定します。

両方指定した場合

// ./foo.yamlファイルの.defitions.fooの部分
./foo.yaml#/definitions/foo

外部リンク部分だけの場合

// ./foo.yamlファイル全体
./foo.yaml

内部リンク部分だけの場合

// 現在開いているファイル(自分自身)の.definitions.bar部分
#/definitions/bar

(~0などを使ったquoteなどの話は省略します)

相対パスの解決方法について

ちなみに$loadの相対パスの解決方法についても詳しく説明すると、例えば以下のようなネストした参照があった場合にはネストされたファイル自身からの相対パスになります。例えば以下のようなファイル構造のときに、それぞれ以下のようなファイルがあった場合には、

.
├── b
│   └── b.yaml
├── c
│   └── c.yaml
├── end.yaml
└── start.yaml

start.yaml

a:
  $load: "./b/b.yaml"  # start.yamlからの相対位置

b/b.yaml

b:
  $load: "../c/c.yaml"  # b/b.yamlからの相対位置

c/c.yaml

c:
  $load: "../end.yaml"  # c/c.yamlからの相対位置

end.yaml

end

以下のような結果になります。普通ですね。

a:
  b:
    c: end

(swagger周りでの愚痴: 全くこれとは関係ない単なる愚痴ですが。OAS(Open API Specification)2.0の頃のswagger parserはこういうような扱いではなく全てトップレベルのファイル(start.yaml)からの位置で計算するみたいな感じでした。ひどい。一方それとは別のswayはまとも。ちなみにそれに関してはissueが建てられて直していこうみたいな話になってます(正確にいうとOAS3.0までは扱いが決まっていなかったという感じ?っぽいです。元々ファイルは分割しても良い位のニュアンスで仕様自体も1つのファイルを前提にしていたりはしているようでした))

$concatについて

次は$concatについてです。これは簡単です。$loadだけでは対応できない部分を補完するためにあります。別のファイルからデータを取ってきて切り貼りしたいのですが。切りまでしかできない感じでした。複数のファイルのデータを1つにまとめて利用したいということになったら必要になるのが$concatです。こんな感じで使います。

person:
  $concat:
    - name: foo
    - age: 10

これは以下の様に出力されます。まとめられます。

person:
  name: foo
  age: 10

おしまい。

$load,$concatの2つを使ってできるようになること

$load、$concatの2つを使ってできるようになることは色んな所にあるデータのつまみ食いみたいなことです。これでようやく作っていた言語にも実用性の芽のようなものが見えてくる様になってきました。

ちょっとした課題

ところで設定ファイルなどのmergeのためには$load,$concatで十分なのですが。swaggerやjsonschemaなどを使った場合にはちょっとだけ困る事があったりします。jsonschemaなどでは$refという表記を使って別のリソースへの参照を表す機能があります(ここでの$refは$で始まっているけれども。現在作っているtoy言語(zenmai)の関数(action)ではありません)。複数のファイルにあるデータを$loadなどで1つファイルにまとめた場合に、この$refの位置を調整してやる必要があるかもしれません。

例えばこういう感じのときです。それぞれ別のファイルで以下の様な内容だとします。

main.yaml

definitions:
  foo:
    $load: data/foo.yaml

data/foo.yaml

type: object
properties:
  value:
    $ref: ../primitive/value.yaml  # ここでの相対位置はdata/foo.yamlを基準にしたもの

これを1つにマージしてしまったときにvalueの$refが指す位置が変わってしまいます。

definitions:
  foo:
    type: object
    properties:
      value:
        $ref: ../primitive/value.yaml  # ここでの相対位置はdata/foo.yamlを基準にしたもの

マージしたタイミングで$refの位置はmain.yamlを基準にしたもの(./primitive/value.yaml)になってほしいところですが。安易に持ってくるだけだと参照先の位置がずれてしまいます。なのでもし対象がjsonschemaやswagger specなのでしたら$refを特別扱いする必要がありそうです。

実はもう1つ新たな問題というか把握しておかなければいけないことがあります。それは$loadを加えた結果、最初の読み込みファイル(entry point)の位置という情報が重要で不可欠なものになってしまうということです。これはリンク先の位置を現在のファイルからの相対パスで指定することを許可するようになったからです。パイプなどで渡されてきたデータに対する位置情報がないものなどについてリンク先の位置を決める事が事実上できなくなります。

そろそろなんとなく

そろそろ毎回試す度にコードを書くのも面倒になってきたのでコマンドを作りたくなってきました。