逆引き Jsonnet

これは、「Jsonnetで○○するには、どうしたらいいの？」について、つらつらと列挙するためのノートです。随時更新。
質問や「もっといいやり方があるよ」などあればコメント欄で教えていただけると助かります。

## 参照

### 他の場所で定義された値を参照する

これにはいくつかやり方がある。
まずは最も単純な `self.<フィールド名>` で参照する方法。

```jsonnet
{
  f1: 1,
  f2: self.f1 + self.f1,
}
```

↓

```json
{
   "f1": 1,
   "f2": 2
}
```

この Jsonnet のトップレベルオブジェクトの `f1` フィールドの値は `1` なので、 それを参照して計算する `f2` の値は $1+1 = 2$ となる。

次に、 `$` を利用する方法。トップレベルオブジェクトを `$` で参照できるので、これを利用できる。

```jsonnet
{
  f1: 2,
  f2_child: {
    grandchild: {
      f3: $.f1 * 3,
    },
  },
}
```

↓

```json
{
   "f1": 2,
   "f2_child": {
      "grandchild": {
         "f3": 6
      }
   }
}
```

`grandchild` はトップレベルオブジェクトからすると孫にあたる階層だが、 `$.f1`  と書くことで一息でトップレベルオブジェクトの `f1` フィールドを参照できる。

なお、直接の親を参照する名前、さらに言えば上方向に相対的に参照する名前は存在しないようだ。

```jsonnet
{
  f1: 2,
  f2_child: {
    f3: 3,
    grandchild: {
      f4: parent.f3 * 3,
    },
  },
}
```

↓

```shell
$ jsonnet foo.jsonnet
STATIC ERROR: ref3.jsonnet:6:11-17: Unknown variable: parent
```

例えば上のように、 `grandchild` から見て直接の親を参照できる `parent` のような識別子があると便利だが、これはエラーになる。 Jsonnet の予約されたキーワードは `assert else error false for function if import importstr in local null tailstrict then self super true` なので、 `parent` 的な意味に相当するものはなさそうである。残念。（ `super` は親オブジェクトではなく継承元オブジェクトを参照するキワードである）

親オブジェクトを参照するには、 `local` によるローカル変数宣言を行えば、これに近いことが可能になる。

```jsonnet
{
  f1: 2,
  f2_child: {
    f3: 3,
    local parent = self,
    grandchild: {
      f4: parent.f3 * 3,
    },
  },
}
```

↓

```json
{
   "f1": 2,
   "f2_child": {
      "f3": 3,
      "grandchild": {
         "f4": 9
      }
   }
}
```

先程との違いは `f2_child` 内で `local parent = self,` という行を足してあるだけだ。
これで `parent` という名前で `f2_child`  のオブジェクトを参照可能になる。

`local` で宣言・定義した変数のスコープは、記述上の当該オブジェクトおよびその子供以下全体である。（`local` 行より上の行でも同一オブジェクト内なら参照可能だ。もちろん読みやすさを考えるとやらないほうが良い。）




## 文字列操作

### フォーマット

`+` で必要な文字を連結して組み立てるか、 Python 風フォーマット書式を使う。

```jsonnet
{
  // `+` での連結
  f1: 'foo' + 'bar',

  // Python風フォーマット書式
  local target = 'world',
  f2: 'Hello %s! (1)' % target,

  // 変数のリスト指定
  local greeting = 'hello',
  f3: '%s %s! (%d)' % [greeting, target, 2],

  // 変数のオブジェクト指定
  local args = {
    greeting: 'hello',
    target: 'world',
    count: 3,
  },
  f4: '%(greeting)s %(target)s! (%(count)d)' % args,
}
```
↓

```json
{
   "f1": "foobar",
   "f2": "Hello world! (1)",
   "f3": "hello world! (2)",
   "f4": "hello world! (3)"
}
```

### スライス

Python風のスライスが可能。ただし、負の数のインデックスやステップ指定はエラー。文字数カウントはUTFコードポイントで行われる（つまり、例えば日本語文字列であれば日本語の1文字を1とカウントする）。

```jsonnet
{
  local s = 'All work and no play makes Jack a dull boy',
  f1: s[1:],
  f2: s[:5],
  f3: s[:10:2],
  local t = '犬も歩けば棒に当たる',
  f4: t[1:],
  f5: t[:5],
  f6: t[:10:2],
}
```
↓

```json
{
   "f1": "ll work and no play makes Jack a dull boy",
   "f2": "All w",
   "f3": "Alwr ",
   "f4": "も歩けば棒に当たる",
   "f5": "犬も歩けば",
   "f6": "犬歩ばにた"
}
```

### キャピタライズ（頭文字を大文字化）

文字列全部を大文字にするには `std.asciiUpper()` を使えばよいが、文字列の先頭だけを大文字にする方法は標準になさそうである。
この場合次のようにすればOK

```jsonnet
local capitalize = function(s) std.asciiUpper(s[:1]) + s[1:];
{
  f1: 'abc',
  f2: capitalize(self.f1),
  f3: std.asciiUpper(self.f1),
}

```
↓

```json
{
   "f1": "abc",
   "f2": "Abc",
   "f3": "ABC"
}
```

### フィールド名に記号やスペースを含めたい

`_` 以外の記号をフィールド名に含めたい場合、クォートすればOK

```jsonnet
{
  'f1-foo': 1,
  'f2 foo': 2,
  "f3'foo": 3,
  'f4"foo': 4,
}
```
↓

```json
{
   "f1-foo": 1,
   "f2 foo": 2,
   "f3'foo": 3,
   "f4\"foo": 4
}
```
## インポート

### 別ファイルの内容を値として読み込む

`importstr` を使う。例えば data.txt に

```text
hello world
```
と書いてあるとすると、次のようになる。

```jsonnet
{
  field: importstr 'data.txt',
}
```
↓

```json
{
   "field": "hello world\n"
}
```

### JSON を読み込む

Jsonnetファイル同様、 JSON ファイルの場合も import で読み込めば良い。例は省略。


## マージ

### オブジェクトをマージする

`+` でマージできる。同じ名前のフィールドがある場合、＋の左辺がベースとなり、右辺で上書きされる。
各フィールドの値も再帰的にマージしたい場合、 `std.mergePatch` を使う。

```jsonnet
{
  obj1:: {
    field1: 'value1',
    field2: 'value2',
  },
  obj2:: {
    field2: 'value2-2',
    field3: 'value3',
  },

  obj1_on_obj2: self.obj2 + self.obj1,
  obj2_on_obj1: self.obj1 + self.obj2,
}
```
↓

```json
{
   "obj1_on_obj2": {
      "field1": "value1",
      "field2": "value2",
      "field3": "value3"
   },
   "obj2_on_obj1": {
      "field1": "value1",
      "field2": "value2-2",
      "field3": "value3"
   }
}
```

### オブジェクトを再帰的にマージする

単純な `+` だと各フィールドは右辺のオブジェクトの値で上書きされるが、 `std.mergePatch` で再帰的にマージできる。
ただし、値が配列の場合（次の例の `field4` 参照）は第2引数側のオブジェクトで上書きされるので注意。

```jsonnet
{
  obj1:: {
    field1: 1,
    field2: {
      field3: 3,
    },
    field4: [4.1, 4.2],
  },
  obj2:: {
    field2: {
      field5: 5,
    },
    field4: [4.3, 4.4],
    field6: 6,
  },

  merged: std.mergePatch(self.obj1, self.obj2),
  shallow: self.obj1 + self.obj2,
}
```
↓

```json
{
   "merged": {
      "field1": 1,
      "field2": {
         "field3": 3,
         "field5": 5
      },
      "field4": [
         4.2999999999999998,
         4.4000000000000004
      ],
      "field6": 6
   },
   "shallow": {
      "field1": 1,
      "field2": {
         "field5": 5
      },
      "field4": [
         4.2999999999999998,
         4.4000000000000004
      ],
      "field6": 6
   }
}
```

## フィールド操作

### フィールド名を動的に決める

`[field_string]: value` のように `[ ]` でフィールド名を囲い、そのなかに string の値を入れると動的に決定した値をフィールド名にできる。

```jsonnet
{
  local prefix = 'foo',
  local name1 = 'bar',
  local name2 = 'buz',


  obj: {
    [prefix + name1]: 1,
    [prefix + name2]: 2,
  },
}
```
↓

```json
{
   "obj": {
      "foobar": 1,
      "foobuz": 2
   }
}
```

ただし、今定義しようとしているフィールドと同じ階層のフィールドの値や変数を用いることはできない。これらは、そのオブジェクトの作成が完了した時点で値が決まるので、今定義しようとしているフィールドを作る段階ではまだ値が決定していないからである。

```jsonnet
{
  obj: {
    local prefix = 'foo',
    local name1 = 'bar',
    local name2 = 'buz',

    [prefix + name1]: 1,
    [prefix + name2]: 2,
  },
}
```
↓

```text
STATIC ERROR: dynamic-fieldname.jsonnet:7:6-12: Unknown variable: prefix
```

### 条件によってフィールドを追加する

これは [フィールド名を動的に決める](フィールド名を動的に決める) のようにフィールドを動的に決定した場合に、`[]`の評価結果が null だとフィールドが削除されることを応用して、 `[if <cond> then <fieldname>]: <value>`  のようにすれば良い。
今、定義しようとしているオブジェクトのフィールドやローカル変数は `[]` 内から参照できない点も注意が必要。

```jsonnet
{
  gen(cond1, cond2, value):: {
    [if cond1 then 'field1']: value,
    [if cond2 then 'field2']: value,
    field3: 3,
  },

  obj1: self.gen(true, false, 1),
  obj2: self.gen(false, true, 2),
}
```
↓

```jsonnet
{
   "obj1": {
      "field1": 1,
      "field3": 3
   },
   "obj2": {
      "field2": 2,
      "field3": 3
   }
}
```

### オブジェクトの特定のフィールドを削除したい

出力されたJSONを利用するアプリケーション側が、「存在しないフィールド」と「値がnullのフィールド」を区別してしまう場合、フィールドの削除が必要になることがある。
フィールドを削除するいくつかの方法が知られているが、どれも一長一短であり、将来的に[より簡易な方法がサポートされる可能性がある](https://github.com/google/jsonnet/issues/312)。

* hidden field を持つオブジェクトとマージする
* 当該フィールドが `null` なオブジェクトを `std.mergePatch()` でマージする
* 当該フィールドが `null` なオブジェクトとマージしたうえで `std.prune()` に通す
* 当該フィールド以外のフィールドを移植する

詳細はそれぞれ次のとおり：

#### hidden field を持つオブジェクトとマージする

次の `del` のように hidden field （ `::` で定義されるフィールド）を持つオブジェクトをマージすると、そのフィールドが不可視になり、最終的に出力される `obj1` のJSONオブジェクトからは削除されたように見える。

```jsonnet
{
  local base = {
    field1: 1,
    field2: 2,
    field3: 3,
  },

  local del = {
    field2:: null,
  },

  obj1: base + del,
  obj2: self.obj1 {
    field4: self.field2,
  },
}
```
↓

```json
{
   "obj1": {
      "field1": 1,
      "field3": 3
   },
   "obj2": {
      "field1": 1,
      "field3": 3,
      "field4": null
   }
}
```
この方法は最も手軽にフィールドの削除が可能である。ただし、以下の点に注意が必要：

* この方法では出力の直前まで削除されないので、 `obj2` のような場合にエラーにならず、消したはずのフィールドの値が見える。
* コードを見ても削除の意図が分かりづらい。上の例のように削除するためだけのオブジェクトだとまだ意図を把握できるが、 フィールドの追加や削除が入り乱れるとわかりにくくなる。

#### 当該フィールドが `null` なオブジェクトを `std.mergePatch()` でマージする

`std.mergePatch(target, patch)` は `target` オブジェクトを `patch` オブジェクトで上書きするような動作をするが、 patch 側に値が null のフィールドがあると、そのフィールドは削除される。これを利用して target 側のフィールドを削除できる。

```jsonnet
{
  local base = {
    field1: 1,
    field2: 2,
    field3: 3,
    child: {
      do_not_delete: null,
    },
  },

  obj1: std.mergePatch(base, {
    field2: null,
  }),
}
```

↓

```JSON
{
   "obj1": {
      "child": {
         "do_not_delete": null
      },
      "field1": 1,
      "field3": 3
   }
}
```

これも比較的削除の意図が読み取りづらく、初見では少し驚くかもしれない。それでも patch 側で指定されていないフィールドについては触らないので、次の方法よりはマシだろう。

#### 当該フィールドが `null` なオブジェクトとマージしたうえで `std.prune()` に通す

`std.prune()` は値が `null` なフィールドを削除する関数であり、これを利用すると任意のフィールドを削除可能（`prune` は「不要な枝を切り取る」というイメージの英単語であり、削除の意図はわかりやすい）。この方法であれば、次の例でコメント化してある部分のように削除した値を参照するとエラーになる。削除対象の値は `null` 以外に空オブジェクト `{}` および空配列 `[]` がある。空文字列 `''`は削除されない。

```jsonnet
{
  local base = {
    field1: 1,
    field2: 2,
    field3: 3,
    child: {
      do_not_delete: null,
    },
    empty_obj: {},
    empty_string: '',
    empty_array: [],
  },

  local del = {
    field2: null,
  },

  obj1: std.prune(base + del),

  // the following becomes error
  // obj2: std.prune(self.obj1 {
  //   field4: self.field2,
  // }),
}
```
↓

```json
{
   "obj1": {
      "empty_string": "",
      "field1": 1,
      "field3": 3
   }
}
```

ただし、この方法では、値が `null` なフィールドは再帰的に削除されるので、意図しない削除が発生しないように注意が必要。削除対象には、上記の `base.child` のようにフィールドの削除によって中身が空になったオブジェクトも含まれる。


#### 当該フィールド以外のフィールドを移植する

最も手間がかかるが、細かいコントロールはしやすい。

```jsonnet
{
  local base = {
    field1: 1,
    field2: 2,
    field3: 3,
    child: {
      do_not_delete: null,
    },
  },

  obj1: {
    field1: base.field1,
    field3: base.field3,
    child: base.child,
  },
}
```
↓

```json
{
   "obj1": {
      "child": {
         "do_not_delete": null
      },
      "field1": 1,
      "field3": 3
   }
}
```
この方法であっても、コピー漏れなどが発生しやすい、コードの見通しが悪くなるといった欠点があるので注意が必要。

なお、同様の移植をオブジェクト内包表記でおこなうことで記述量を削減できるが、内包表記に慣れていないと読みづらさがある。

```jsonnet
{
  local base = {
    field1: 1,
    field2: 2,
    field3: 3,
    child: {
      do_not_delete: null,
    },
  },

  obj1: {
    [f]: base[f]
    for f in std.objectFieldsAll(base)
    if f != 'field2'
  },
}
```

↓

```JSON
{
   "obj1": {
      "child": {
         "do_not_delete": null
      },
      "field1": 1,
      "field3": 3
   }
}
```

さらにこれを応用して次のような関数を定義しておくことで簡易な削除が可能になるが、再帰関数等についてそれなりの理解が必要になる

```jsonnet
local delSingleField(obj, field) = {
  [f]: obj[f]
  for f in std.objectFieldsAll(obj)
  if f != field
};

local delMultiField(obj, fields) =
  if std.length(fields) == 0 then
    obj
  else
    delSingleField(delMultiField(obj, fields[1:]), fields[0]);

local delField(obj, field) =
  local tp = std.type(field);
  if tp == 'string' then
    delSingleField(obj, field)
  else if tp == 'array' then
    delMultiField(obj, field)
  else
    error 'delField(): second parameter must be a string or an array of strings';

{
  local base = {
    field1: 1,
    field2: 2,
    field3: 3,
    child: {
      do_not_delete: null,
    },
  },

  obj1: delField(base, 'field2'),
  obj2: delField(base, ['field2', 'field3']),
}
```

↓

```JSON
{
   "obj1": {
      "child": {
         "do_not_delete": null
      },
      "field1": 1,
      "field3": 3
   },
   "obj2": {
      "child": {
         "do_not_delete": null
      },
      "field1": 1
   }
}
```

## 関数

### 文字列を返す関数を定義したい。数値を返す関数を定義したい

よくある関数はオブジェクトや配列を返すように定義するが、単体の文字列や数値を返す関数も定義できる。
ただその場合、返却値を組み立てるための事前計算はどこに記述すればよいのだろうか？
これは、オブジェクトのキーと値の間に local 変数を定義することが可能なので、そこで計算すれば良い。

```jsonnet
local lib = {
  fn(a):
    local b = 2 * a;
    local c = a * a;
    '%d + %d = %d' % [b, c, b + c],
};
{
  foo: lib.fn(3),
}
```

↓

```json
{
   "foo": "6 + 9 = 15"
}
```

### 再帰関数を定義したい

普通に書けば定義できる。
オブジェクトを返さない関数であれば、自分自身は `self.<関数名>` で参照できるし、オブジェクトを返す関数であれば、 `$.<関数名>` などで参照すれば良い。（オブジェクトのキーやlocal変数は、値を記述する場所もスコープ内である）

```jsonnet
local lib = {
  fib(n):
    if n < 2 then
      n
    else
      self.fib(n - 1) + self.fib(n - 2),

  fib2(o): {
    n:
      if o.n < 2 then
        o.n
      else
        $.fib2({ n: o.n - 1 }).n + $.fib2({ n: o.n - 2 }).n,
  },
};
{
  fib: [
    lib.fib(x)
    for x in std.range(1, 10)
  ],
  fib2: [
    lib.fib2({ n: x }).n
    for x in std.range(1, 10)
  ],
}
```

↓

```json
{
   "fib": [
      1,
      1,
      2,
      3,
      5,
      8,
      13,
      21,
      34,
      55
   ],
   "fib2": [
      1,
      1,
      2,
      3,
      5,
      8,
      13,
      21,
      34,
      55
   ]
}

```

### 繰り返し処理

複雑な繰り返し処理は再帰関数で行う。
まず、いわゆる map / reduce な処理、つまり配列やオブジェクトの変換、また配列の要素を一定の演算で1つの値にまとめる処理、については配列内包表記 (例えば `[x * 2 for x in arr]` ) や `std.map`, `std.foldl` naどを使えば良い。
ただし、これらの抽象で実現可能なこと以上に複雑な繰り返し処理は、再帰関数で行う必要がある。

例えば配列 arr の数値を合計する関数 sumOfArray は次のようにかける。

```jsonnet
local sumOfArray(arr) =
  if arr == [] then
    0
  else
    arr[0] + sumOfArray(arr[1:]);

sumOfArray([1, 2, 3, 4])
```

↓

```
10
```

これはいかにも「再帰っぽい」書き方になっているが、これをもっと「繰り返し処理」っぽく書くこともできる。

```jsonnet
local sumOfArray(arr) =
  local iter(sum, a) =
    if a == [] then
      sum
    else
      iter(sum + a[0], a[1:]);
  iter(0, arr);
```

この書き換えで多少パフォーマンスは良くなる場合もあるが、そこまで大きくは変わらないはずなので、好みの問題だ。ただし、後述する TCO をかける場合は、末尾呼び出しする必要があるので後者の書き方のほうが末尾呼び出しにしやすい。

### スタックあふれへの対処

さきほどの sumOfArray に、要素数260程度の配列を入力すると最大スタックフレームの制限にあたってプログラムが実行エラーになる。

```jsonnet
local sumOfArray(arr) =
  if arr == [] then
    0
  else
    arr[0] + sumOfArray(arr[1:]);

local n = 260;

sumOfArray(std.range(1, n))
```

↓

```
$ jsonnet x.jsonnet
RUNTIME ERROR: max stack frames exceeded.
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	<std>:219:22-42	thunk from <thunk from <function <build>>>
	...
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:5:14-33	function <sumOfArray>
	x.jsonnet:9:1-28	$
	During evaluation


shell returned 1
```

これは繰り返し処理風に記述しても同じだ。
これを回避するには、 jsonnet コマンドラインオプションに -s <フレーム上限> を追加する

```jsonnet
$ jsonnet -s 1000 x.jsonnet
33930
```

ただし、限度があるので注意が必要。


### 関数のテスト

できるだけJSON＋多少の拡張、的な使い方をするのがベストプラクティスではあると思うが、プロジェクトの規模が多くなってくると、関数の利用は避けられない。そして関数を利用しはじめると、その関数に対するテストが必要になる。

ではテストはどうするのか。公式に提供されている方法はないが、今の所はテスト自体も Jsonnet で記述してしまうのが良いのではないかと思う。
たとえばユーティリティ関数を集めた util.libsonnet ファイルのテストを util_test.libsonnet に記述する、という感じだ。

ポイントとしては `jsonnet util_test.libsonnet` を実行するだけでテストが走るようにしておき、
テストの実行は適切なタスクランナーに任せる、というものだ。



## その他

### JSON を Jsonnet に変換する

JSON は Jsonnet のサブセットであり、 Jsonnet ファイルとしても読み込める。これを利用して、 Jsonnet 標準のフォーマッタ `jsonnetfmt` を使うと JSON を Jsonnet に変換できる。

```shell
$ cat foo.json
{
  "foo": 1,
  "bar": "hello world"
}
```

↓

```shell
$ jsonnetfmt foo.json
{
  foo: 1,
  bar: 'hello world',
}
```

オブジェクトキーの不要なクォートが省略されていること、ぶら下がりカンマがあることなどから、Jsonnetフォーマットであることが分かる。 
また、実用的には、エディタに Jsonnet サポートプラグインなどをインストール済みであれば、ファイル名のサフィックスを json から jsonnet に変更して当該エディタで開き、セーブすることで暗黙的に jsonnetfmt を呼び出して Jsonnet 化することも可能である。

## できないこと

(もしかしたら新しいバージョンではできるようになっているかも)

### import するファイル名の動的に決定する（文字列を組み立てるなどで）

できない。
次のように記述して、import するファイルを動的に切り替えたいと思うかもしれないけど、これは無理。
次の例では外部変数 TARGET の値を使っているが、外部変数の値でなくても同様。

```jsonnet
local target = std.extVar('TARGET');
local vars = (import target + '-vars.libsonnet');
{
  field1: vars.var1,
}
```
↓

```
STATIC ERROR: dynamic-import-incorrect.jsonnet:2:22-48: computed imports are not allowed.
```

かわりの方法としては、次のようにファイル名部分はベタ書きにして、それを if 文 で分岐させて読ませる、などがある。

```jsonnet
// dynamic-import.jsonnet
local target = std.extVar('TARGET');
local vars = if target == 'prod' then
  (import 'prod-vars.libsonnet')
else if target == 'staging' then
  (import 'staging-vars.libsonnet')
else
  (import 'devel-vars.libsonnet');
{
  field1: vars.var1,
}
```

```jsonnet
// prod-vars.libsonnet
{
  var1: 'this is production',
}
```

```jsonnet
// staging-vars.libsonnet
{
  var1: 'this is staging',
}
```
↓

```shell
$ jsonnet -V TARGET=prod  dynamic-import.jsonnet
{
   "field1": "this is production"
}
```

```shell
$ jsonnet -V TARGET=staging  dynamic-import.jsonnet
{
   "field1": "this is staging"
}
```