これは、「Jsonnetで○○するには、どうしたらいいの?」について、つらつらと列挙するためのノートです。随時更新。
質問や「もっといいやり方があるよ」などあればコメント欄で教えていただけると助かります。
参照
他の場所で定義された値を参照する
これにはいくつかやり方がある。
まずは最も単純な self.<フィールド名>
で参照する方法。
{
f1: 1,
f2: self.f1 + self.f1,
}
↓
{
"f1": 1,
"f2": 2
}
この Jsonnet のトップレベルオブジェクトの f1
フィールドの値は 1
なので、 それを参照して計算する f2
の値は $1+1 = 2$ となる。
次に、 $
を利用する方法。トップレベルオブジェクトを $
で参照できるので、これを利用できる。
{
f1: 2,
f2_child: {
grandchild: {
f3: $.f1 * 3,
},
},
}
↓
{
"f1": 2,
"f2_child": {
"grandchild": {
"f3": 6
}
}
}
grandchild
はトップレベルオブジェクトからすると孫にあたる階層だが、 $.f1
と書くことで一息でトップレベルオブジェクトの f1
フィールドを参照できる。
なお、直接の親を参照する名前、さらに言えば上方向に相対的に参照する名前は存在しないようだ。
{
f1: 2,
f2_child: {
f3: 3,
grandchild: {
f4: parent.f3 * 3,
},
},
}
↓
$ 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
によるローカル変数宣言を行えば、これに近いことが可能になる。
{
f1: 2,
f2_child: {
f3: 3,
local parent = self,
grandchild: {
f4: parent.f3 * 3,
},
},
}
↓
{
"f1": 2,
"f2_child": {
"f3": 3,
"grandchild": {
"f4": 9
}
}
}
先程との違いは f2_child
内で local parent = self,
という行を足してあるだけだ。
これで parent
という名前で f2_child
のオブジェクトを参照可能になる。
local
で宣言・定義した変数のスコープは、記述上の当該オブジェクトおよびその子供以下全体である。(local
行より上の行でも同一オブジェクト内なら参照可能だ。もちろん読みやすさを考えるとやらないほうが良い。)
文字列操作
フォーマット
+
で必要な文字を連結して組み立てるか、 Python 風フォーマット書式を使う。
{
// `+` での連結
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,
}
↓
{
"f1": "foobar",
"f2": "Hello world! (1)",
"f3": "hello world! (2)",
"f4": "hello world! (3)"
}
スライス
Python風のスライスが可能。ただし、負の数のインデックスやステップ指定はエラー。文字数カウントはUTFコードポイントで行われる(つまり、例えば日本語文字列であれば日本語の1文字を1とカウントする)。
{
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],
}
↓
{
"f1": "ll work and no play makes Jack a dull boy",
"f2": "All w",
"f3": "Alwr ",
"f4": "も歩けば棒に当たる",
"f5": "犬も歩けば",
"f6": "犬歩ばにた"
}
キャピタライズ(頭文字を大文字化)
文字列全部を大文字にするには std.asciiUpper()
を使えばよいが、文字列の先頭だけを大文字にする方法は標準になさそうである。
この場合次のようにすればOK
local capitalize = function(s) std.asciiUpper(s[:1]) + s[1:];
{
f1: 'abc',
f2: capitalize(self.f1),
f3: std.asciiUpper(self.f1),
}
↓
{
"f1": "abc",
"f2": "Abc",
"f3": "ABC"
}
フィールド名に記号やスペースを含めたい
_
以外の記号をフィールド名に含めたい場合、クォートすればOK
{
'f1-foo': 1,
'f2 foo': 2,
"f3'foo": 3,
'f4"foo': 4,
}
↓
{
"f1-foo": 1,
"f2 foo": 2,
"f3'foo": 3,
"f4\"foo": 4
}
インポート
別ファイルの内容を値として読み込む
importstr
を使う。例えば data.txt に
hello world
と書いてあるとすると、次のようになる。
{
field: importstr 'data.txt',
}
↓
{
"field": "hello world\n"
}
JSON を読み込む
Jsonnetファイル同様、 JSON ファイルの場合も import で読み込めば良い。例は省略。
マージ
オブジェクトをマージする
+
でマージできる。同じ名前のフィールドがある場合、+の左辺がベースとなり、右辺で上書きされる。
各フィールドの値も再帰的にマージしたい場合、 std.mergePatch
を使う。
{
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,
}
↓
{
"obj1_on_obj2": {
"field1": "value1",
"field2": "value2",
"field3": "value3"
},
"obj2_on_obj1": {
"field1": "value1",
"field2": "value2-2",
"field3": "value3"
}
}
オブジェクトを再帰的にマージする
単純な +
だと各フィールドは右辺のオブジェクトの値で上書きされるが、 std.mergePatch
で再帰的にマージできる。
ただし、値が配列の場合(次の例の field4
参照)は第2引数側のオブジェクトで上書きされるので注意。
{
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,
}
↓
{
"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 の値を入れると動的に決定した値をフィールド名にできる。
{
local prefix = 'foo',
local name1 = 'bar',
local name2 = 'buz',
obj: {
[prefix + name1]: 1,
[prefix + name2]: 2,
},
}
↓
{
"obj": {
"foobar": 1,
"foobuz": 2
}
}
ただし、今定義しようとしているフィールドと同じ階層のフィールドの値や変数を用いることはできない。これらは、そのオブジェクトの作成が完了した時点で値が決まるので、今定義しようとしているフィールドを作る段階ではまだ値が決定していないからである。
{
obj: {
local prefix = 'foo',
local name1 = 'bar',
local name2 = 'buz',
[prefix + name1]: 1,
[prefix + name2]: 2,
},
}
↓
STATIC ERROR: dynamic-fieldname.jsonnet:7:6-12: Unknown variable: prefix
条件によってフィールドを追加する
これは フィールド名を動的に決める のようにフィールドを動的に決定した場合に、[]
の評価結果が null だとフィールドが削除されることを応用して、 [if <cond> then <fieldname>]: <value>
のようにすれば良い。
今、定義しようとしているオブジェクトのフィールドやローカル変数は []
内から参照できない点も注意が必要。
{
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),
}
↓
{
"obj1": {
"field1": 1,
"field3": 3
},
"obj2": {
"field2": 2,
"field3": 3
}
}
オブジェクトの特定のフィールドを削除したい
出力されたJSONを利用するアプリケーション側が、「存在しないフィールド」と「値がnullのフィールド」を区別してしまう場合、フィールドの削除が必要になることがある。
フィールドを削除するいくつかの方法が知られているが、どれも一長一短であり、将来的により簡易な方法がサポートされる可能性がある。
- hidden field を持つオブジェクトとマージする
- 当該フィールドが
null
なオブジェクトをstd.mergePatch()
でマージする - 当該フィールドが
null
なオブジェクトとマージしたうえでstd.prune()
に通す - 当該フィールド以外のフィールドを移植する
詳細はそれぞれ次のとおり:
hidden field を持つオブジェクトとマージする
次の del
のように hidden field ( ::
で定義されるフィールド)を持つオブジェクトをマージすると、そのフィールドが不可視になり、最終的に出力される obj1
のJSONオブジェクトからは削除されたように見える。
{
local base = {
field1: 1,
field2: 2,
field3: 3,
},
local del = {
field2:: null,
},
obj1: base + del,
obj2: self.obj1 {
field4: self.field2,
},
}
↓
{
"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 側のフィールドを削除できる。
{
local base = {
field1: 1,
field2: 2,
field3: 3,
child: {
do_not_delete: null,
},
},
obj1: std.mergePatch(base, {
field2: null,
}),
}
↓
{
"obj1": {
"child": {
"do_not_delete": null
},
"field1": 1,
"field3": 3
}
}
これも比較的削除の意図が読み取りづらく、初見では少し驚くかもしれない。それでも patch 側で指定されていないフィールドについては触らないので、次の方法よりはマシだろう。
当該フィールドが null
なオブジェクトとマージしたうえで std.prune()
に通す
std.prune()
は値が null
なフィールドを削除する関数であり、これを利用すると任意のフィールドを削除可能(prune
は「不要な枝を切り取る」というイメージの英単語であり、削除の意図はわかりやすい)。この方法であれば、次の例でコメント化してある部分のように削除した値を参照するとエラーになる。削除対象の値は null
以外に空オブジェクト {}
および空配列 []
がある。空文字列 ''
は削除されない。
{
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,
// }),
}
↓
{
"obj1": {
"empty_string": "",
"field1": 1,
"field3": 3
}
}
ただし、この方法では、値が null
なフィールドは再帰的に削除されるので、意図しない削除が発生しないように注意が必要。削除対象には、上記の base.child
のようにフィールドの削除によって中身が空になったオブジェクトも含まれる。
当該フィールド以外のフィールドを移植する
最も手間がかかるが、細かいコントロールはしやすい。
{
local base = {
field1: 1,
field2: 2,
field3: 3,
child: {
do_not_delete: null,
},
},
obj1: {
field1: base.field1,
field3: base.field3,
child: base.child,
},
}
↓
{
"obj1": {
"child": {
"do_not_delete": null
},
"field1": 1,
"field3": 3
}
}
この方法であっても、コピー漏れなどが発生しやすい、コードの見通しが悪くなるといった欠点があるので注意が必要。
なお、同様の移植をオブジェクト内包表記でおこなうことで記述量を削減できるが、内包表記に慣れていないと読みづらさがある。
{
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'
},
}
↓
{
"obj1": {
"child": {
"do_not_delete": null
},
"field1": 1,
"field3": 3
}
}
さらにこれを応用して次のような関数を定義しておくことで簡易な削除が可能になるが、再帰関数等についてそれなりの理解が必要になる
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']),
}
↓
{
"obj1": {
"child": {
"do_not_delete": null
},
"field1": 1,
"field3": 3
},
"obj2": {
"child": {
"do_not_delete": null
},
"field1": 1
}
}
関数
文字列を返す関数を定義したい。数値を返す関数を定義したい
よくある関数はオブジェクトや配列を返すように定義するが、単体の文字列や数値を返す関数も定義できる。
ただその場合、返却値を組み立てるための事前計算はどこに記述すればよいのだろうか?
これは、オブジェクトのキーと値の間に local 変数を定義することが可能なので、そこで計算すれば良い。
local lib = {
fn(a):
local b = 2 * a;
local c = a * a;
'%d + %d = %d' % [b, c, b + c],
};
{
foo: lib.fn(3),
}
↓
{
"foo": "6 + 9 = 15"
}
再帰関数を定義したい
普通に書けば定義できる。
オブジェクトを返さない関数であれば、自分自身は self.<関数名>
で参照できるし、オブジェクトを返す関数であれば、 $.<関数名>
などで参照すれば良い。(オブジェクトのキーやlocal変数は、値を記述する場所もスコープ内である)
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)
],
}
↓
{
"fib": [
1,
1,
2,
3,
5,
8,
13,
21,
34,
55
],
"fib2": [
1,
1,
2,
3,
5,
8,
13,
21,
34,
55
]
}
その他
JSON を Jsonnet に変換する
JSON は Jsonnet のサブセットであり、 Jsonnet ファイルとしても読み込める。これを利用して、 Jsonnet 標準のフォーマッタ jsonnetfmt
を使うと JSON を Jsonnet に変換できる。
$ cat foo.json
{
"foo": 1,
"bar": "hello world"
}
↓
$ jsonnetfmt foo.json
{
foo: 1,
bar: 'hello world',
}
オブジェクトキーの不要なクォートが省略されていること、ぶら下がりカンマがあることなどから、Jsonnetフォーマットであることが分かる。
また、実用的には、エディタに Jsonnet サポートプラグインなどをインストール済みであれば、ファイル名のサフィックスを json から jsonnet に変更して当該エディタで開き、セーブすることで暗黙的に jsonnetfmt を呼び出して Jsonnet 化することも可能である。
できないこと
(もしかしたら新しいバージョンではできるようになっているかも)
import するファイル名の動的に決定する(文字列を組み立てるなどで)
できない。
次のように記述して、import するファイルを動的に切り替えたいと思うかもしれないけど、これは無理。
次の例では外部変数 TARGET の値を使っているが、外部変数の値でなくても同様。
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 文 で分岐させて読ませる、などがある。
// 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,
}
// prod-vars.libsonnet
{
var1: 'this is production',
}
// staging-vars.libsonnet
{
var1: 'this is staging',
}
↓
$ jsonnet -V TARGET=prod dynamic-import.jsonnet
{
"field1": "this is production"
}
$ jsonnet -V TARGET=staging dynamic-import.jsonnet
{
"field1": "this is staging"
}