Jsonnet是Google推出的一门JSON模板配置语言,可以完全兼容JSON并提供一些新的特性:注释、引用、算术运算、条件操作符,数组和对象内含,引入,函数,局部变量,继承等。

1. Jsonnet 基本操作

1.1 环境安装

ubuntu 环境

1
sudo apt-get install jsonnet

官方安装说明(Go 实现版)

1
go get github.com/google/go-jsonnet/cmd/jsonnet

如果提示“The program ‘go’ is currently not installed.”,说明没有安装go环境,请先安装,具体安装方法自行搜索,这里不在啰嗦。

1.2 使用

1
2
3
4
5
6
# 基本使用
# 解释运行一个 jsonnet 源码文件
jsonnet demo.jsonnet

# 若想将转换后的内容保存到文件中,直接重定向
jsonnet demo.jsonnet > demo.json

2. 语法

Jsonnet的语法跟C语言和Python很像,很多语法结构一模一样,如果有C和Python基础,学起来应该会非常快。

任何JSON文档都是合法的Jsonnet源码文件。因此,下面只介绍Jsonnet特有的特性。

2.1 注释

Jsonnet接受两种风格的注释:C语言风格(//、/* … */)和Python风格(# ..)。

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
/* A C-style comment. */
# A Python-style comment.
{
// A C-style comment.
/*
A
Multi-line comment
*/
# A Python-style comment.
name: "Li Ming"
}
1
2
3
{
"name": "Li Ming"
}

2.2 变量

避免重复的最简单方式是使用变量。

  • 关键字 local 定义一个局部变量
  • 在字段旁边定义的变量以逗号( , )结尾
  • 所有其他情况都以分号( ; )结束
Jsonnet JSON
1
2
3
4
5
6
7
local name = 'Li Ming';
{
local age = 18,
name: name,
age: age
}

1
2
3
4
{
"age": 18,
"name": "Li Ming"
}

2.3 引用

另一种避免重复的方法是引用结构的其他部分。

  • self 指向当前对象
  • $ 指向最外层对象
  • ['foo'] 查找一个字段
  • .f 会把一个字段当做标识符来引用
  • [10] 查找数组的元素
  • 允许任意长的路径。
  • 支持Python风格的数组索引语法,如 arr[10:20:2]
  • 字符串也支持索引或切片操作,以Unicode码值为单位
Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
'Tom Collins': {
ingredients: [
{ kind: "Farmer's Gin", qty: 1.5 },
{ kind: 'Lemon', qty: 1 },
{ kind: 'Simple Syrup', qty: 0.5 },
{ kind: 'Soda', qty: 2 },
{ kind: 'Angostura', qty: 'dash' },
],
garnish: 'Maraschino Cherry',
served: 'Tall',
},
Martini: {
ingredients: [
{
// Use the same gin as the Tom Collins.
kind:
$['Tom Collins'].ingredients[0].kind,
qty: 2,
},
{ kind: 'Dry White Vermouth', qty: 1 },
],
garnish: 'Olive',
served: 'Straight Up',
},
// Create an alias.
'Gin Martini': self.Martini,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
{
"Gin Martini": {
"garnish": "Olive",
"ingredients": [
{
"kind": "Farmer's Gin",
"qty": 2
},
{
"kind": "Dry White Vermouth",
"qty": 1
}
],
"served": "Straight Up"
},
"Martini": {
"garnish": "Olive",
"ingredients": [
{
"kind": "Farmer's Gin",
"qty": 2
},
{
"kind": "Dry White Vermouth",
"qty": 1
}
],
"served": "Straight Up"
},
"Tom Collins": {
"garnish": "Maraschino Cherry",
"ingredients": [
{
"kind": "Farmer's Gin",
"qty": 1.5
},
{
"kind": "Lemon",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 0.5
},
{
"kind": "Soda",
"qty": 2
},
{
"kind": "Angostura",
"qty": "dash"
}
],
"served": "Tall"
}
}

为了引用当前对象和最外层对象之间的对象,我们使用一个变量为该关卡创建一个名称:

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
Martini: {
local drink = self,
ingredients: [
{ kind: "Farmer's Gin", qty: 1 },
{
kind: 'Dry White Vermouth',
qty: drink.ingredients[0].qty,
},
],
garnish: 'Olive',
served: 'Straight Up',
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"Martini": {
"garnish": "Olive",
"ingredients": [
{
"kind": "Farmer's Gin",
"qty": 1
},
{
"kind": "Dry White Vermouth",
"qty": 1
}
],
"served": "Straight Up"
}
}

2.4 算术

Jsonnet支持数字运算(加减乘除)以及其他的运算

  • 支持浮点数运算、位运算,布尔逻辑
  • 字符串可以使用 + 来连接,如果 + 一边是字符串,另一边不是字符串时,会将非字符串转为字符串
  • 支持两个字符串之间使用 < 比较(根据Unicode码位比较)
  • 对象可以与 + 组合,两边字段类型不同时以右边的字段为准。
  • 支持操作符 in 来判断字段是否在一个对象中
  • == is deep value equality.
  • Python兼容的字符串格式化 。 结合 ||| 这可用于模板文本中(定义多行文本可以使用 ||| 包住)。
Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
concat_array: [1, 2, 3] + [4],
concat_string: '123' + 4,
equality1: 1 == '1',
equality2: [{}, { x: 3 - 1 }]
== [{}, { x: 2 }],
ex1: 1 + 2 * 3 / (4 + 5),
// Bitwise operations first cast to int.
ex2: self.ex1 | 3,
// Modulo operator.
ex3: self.ex1 % 2,
// Boolean logic
ex4: (4 > 3) && (1 <= 3) || false,
// Mixing objects together
obj: { a: 1, b: 2 } + { b: 3, c: 4 },
// Test if a field is in an object
obj_member: 'foo' in { foo: 1 },
// String formatting
str1: 'The value of self.ex2 is '
+ self.ex2 + '.',
str2: 'The value of self.ex2 is %g.'
% self.ex2,
str3: 'ex1=%0.2f, ex2=%0.2f'
% [self.ex1, self.ex2],
// By passing self, we allow ex1 and ex2 to
// be extracted internally.
str4: 'ex1=%(ex1)0.2f, ex2=%(ex2)0.2f'
% self,
// Do textual templating of entire files:
str5: |||
ex1=%(ex1)0.2f
ex2=%(ex2)0.2f
||| % self,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"concat_array": [
1,
2,
3,
4
],
"concat_string": "1234",
"equality1": false,
"equality2": true,
"ex1": 1.6666666666666665,
"ex2": 3,
"ex3": 1.6666666666666665,
"ex4": true,
"obj": {
"a": 1,
"b": 3,
"c": 4
},
"obj_member": true,
"str1": "The value of self.ex2 is 3.",
"str2": "The value of self.ex2 is 3.",
"str3": "ex1=1.67, ex2=3.00",
"str4": "ex1=1.67, ex2=3.00",
"str5": "ex1=1.67\nex2=3.00\n"
}

2.5 函数

与Python语法一样,函数具有位置参数,命名参数和默认参数。还支持封闭包。下面示例是一些语法示例。此外,标准库中还定义了很多常用函数。

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Define a local function.
// Default arguments are like Python:
local my_function(x, y=10) = x + y;

// Define a local multiline function.
local multiline_function(x) =
// One can nest locals.
local temp = x * 2;
// Every local ends with a semi-colon.
[temp, temp + 1];

local object = {
// A method
my_method(x): x * x,
};

{
// Functions are first class citizens.
call_inline_function:
(function(x) x * x)(5),

call_multiline_function: multiline_function(4),

// Using the variable fetches the function,
// the parens call the function.
call: my_function(2),

// Like python, parameters can be named at
// call time.
named_params: my_function(x=2),
// This allows changing their order
named_params2: my_function(y=3, x=2),

// object.my_method returns the function,
// which is then called like any other.
call_method1: object.my_method(3),

standard_lib:
std.join(' ', std.split('foo/bar', '/')),
len: [
std.length('hello'),
std.length([1, 2, 3]),
],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"call": 12,
"call_inline_function": 25,
"call_method1": 9,
"call_multiline_function": [8, 9],
"len": [
5,
3
],
"named_params": 12,
"named_params2": 5,
"standard_lib": "foo bar"
}

另一个示例

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// This function returns an object. Although
// the braces look like Java or C++ they do
// not mean a statement block, they are instead
// the value being returned.
local Sour(spirit, garnish='Lemon twist') = {
ingredients: [
{ kind: spirit, qty: 2 },
{ kind: 'Egg white', qty: 1 },
{ kind: 'Lemon Juice', qty: 1 },
{ kind: 'Simple Syrup', qty: 1 },
],
garnish: garnish,
served: 'Straight Up',
};

{
'Whiskey Sour': Sour('Bulleit Bourbon',
'Orange bitters'),
'Pisco Sour': Sour('Machu Pisco',
'Angostura bitters'),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"Pisco Sour": {
"garnish": "Angostura bitters",
"ingredients": [
{
"kind": "Machu Pisco",
"qty": 2
},
{
"kind": "Egg white",
"qty": 1
},
{
"kind": "Lemon Juice",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 1
}
],
"served": "Straight Up"
},
"Whiskey Sour": {
"garnish": "Orange bitters",
"ingredients": [
{
"kind": "Bulleit Bourbon",
"qty": 2
},
{
"kind": "Egg white",
"qty": 1
},
{
"kind": "Lemon Juice",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 1
}
],
"served": "Straight Up"
}
}

2.6 条件

条件表达式看起来像 if b then e else e ,其中 else 分支可选,默认为 null

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
local Mojito(virgin=false, large=false) = {
// A local next to fields ends with ','.
local factor = if large then 2 else 1,
// The ingredients are split into 3 arrays,
// the middle one is either length 1 or 0.
ingredients: [
{
kind: 'Mint',
action: 'muddle',
qty: 6 * factor,
unit: 'leaves',
},
] + (
if virgin then [] else [
{ kind: 'Banks', qty: 1.5 * factor },
]
) + [
{ kind: 'Lime', qty: 0.5 * factor },
{ kind: 'Simple Syrup', qty: 0.5 * factor },
{ kind: 'Soda', qty: 3 * factor },
],
// Returns null if not large.
garnish: if large then 'Lime wedge',
served: 'Over crushed ice',
};

{
Mojito: Mojito(),
'Virgin Mojito': Mojito(virgin=true),
'Large Mojito': Mojito(large=true),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
"Large Mojito": {
"garnish": "Lime wedge",
"ingredients": [
{
"action": "muddle",
"kind": "Mint",
"qty": 12,
"unit": "leaves"
},
{
"kind": "Banks",
"qty": 3
},
{
"kind": "Lime",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 1
},
{
"kind": "Soda",
"qty": 6
}
],
"served": "Over crushed ice"
},
"Mojito": {
"garnish": null,
"ingredients": [
{
"action": "muddle",
"kind": "Mint",
"qty": 6,
"unit": "leaves"
},
{
"kind": "Banks",
"qty": 1.5
},
{
"kind": "Lime",
"qty": 0.5
},
{
"kind": "Simple Syrup",
"qty": 0.5
},
{
"kind": "Soda",
"qty": 3
}
],
"served": "Over crushed ice"
},
"Virgin Mojito": {
"garnish": null,
"ingredients": [
{
"action": "muddle",
"kind": "Mint",
"qty": 6,
"unit": "leaves"
},
{
"kind": "Lime",
"qty": 0.5
},
{
"kind": "Simple Syrup",
"qty": 0.5
},
{
"kind": "Soda",
"qty": 3
}
],
"served": "Over crushed ice"
}
}

2.7 计算命名字段

Jsonnet对象可以像 std::map 或来自常规语言的类似数据结构一样使用。

  • 字段查找可以通过 obj[e] 进行计算。
  • 等价的定义是 {[e]: ...}
  • 在计算命名字段时不能访问 self 或对象局部变量,因为对象尚未构造。
  • 如果一个字段名在对象构造期间计算为空,那么该字段将被省略。这很好地配合了条件句的默认false分支(见下面)。
Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
local Margarita(salted) = {
ingredients: [
{ kind: 'Tequila Blanco', qty: 2 },
{ kind: 'Lime', qty: 1 },
{ kind: 'Cointreau', qty: 1 },
],
[if salted then 'garnish']: 'Salt',
};
{
Margarita: Margarita(true),
'Margarita Unsalted': Margarita(false),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"Margarita": {
"garnish": "Salt",
"ingredients": [
{
"kind": "Tequila Blanco",
"qty": 2
},
{
"kind": "Lime",
"qty": 1
},
{
"kind": "Cointreau",
"qty": 1
}
]
},
"Margarita Unsalted": {
"ingredients": [
{
"kind": "Tequila Blanco",
"qty": 2
},
{
"kind": "Lime",
"qty": 1
},
{
"kind": "Cointreau",
"qty": 1
}
]
}
}

2.8 数组和对象

Jsonnet支持Python风格的数组和对象构造。

  • 支持forif 嵌套使用
Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
local arr = std.range(5, 8);
{
array_comprehensions: {
higher: [x + 3 for x in arr],
lower: [x - 3 for x in arr],
evens: [x for x in arr if x % 2 == 0],
odds: [x for x in arr if x % 2 == 1],
evens_and_odds: [
'%d-%d' % [x, y]
for x in arr
if x % 2 == 0
for y in arr
if y % 2 == 1
],
},
object_comprehensions: {
evens: {
['f' + x]: true
for x in arr
if x % 2 == 0
},
// Use object composition (+) to add in
// static fields:
mixture: {
f: 1,
g: 2,
} + {
[x]: 0
for x in ['a', 'b', 'c']
},
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"array_comprehensions": {
"evens": [
6,
8
],
"evens_and_odds": [
"6-5",
"6-7",
"8-5",
"8-7"
],
"higher": [
8,
9,
10,
11
],
"lower": [
2,
3,
4,
5
],
"odds": [
5,
7
]
},
"object_comprehensions": {
"evens": {
"f6": true,
"f8": true
},
"mixture": {
"a": 0,
"b": 0,
"c": 0,
"f": 1,
"g": 2
}
}
}

2.9 导入

可以从其他文件中导入代码和原始数据。

  • 导入构造类似于复制/粘贴Jsonnet代码。
  • 按照惯例为导入而设计的文件以 .libsonnet 结尾
  • 原始JSON也可以通过这种方式导入。
  • importstr 构造用于逐字UTF-8文本。

通常,导入的Jsonnet内容存储在顶级本地变量中。这类似于其他编程语言处理模块的方式。Jsonnet库通常返回一个对象,因此可以很容易地扩展它们。这两种约定都没有强制执行。

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
local martinis = import 'martinis.libsonnet';

{
'Vodka Martini': martinis['Vodka Martini'],
Manhattan: {
ingredients: [
{ kind: 'Rye', qty: 2.5 },
{ kind: 'Sweet Red Vermouth', qty: 1 },
{ kind: 'Angostura', qty: 'dash' },
],
garnish: importstr 'garnish.txt',
served: 'Straight Up',
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"Manhattan": {
"garnish": "Maraschino Cherry",
"ingredients": [
{
"kind": "Rye",
"qty": 2.5
},
{
"kind": "Sweet Red Vermouth",
"qty": 1
},
{
"kind": "Angostura",
"qty": "dash"
}
],
"served": "Straight Up"
},
"Vodka Martini": {
"garnish": "Olive",
"ingredients": [
{
"kind": "Vodka",
"qty": 2
},
{
"kind": "Dry White Vermouth",
"qty": 1
}
],
"served": "Straight Up"
}
}

导入的两个本地文件中内容分别为:

martinis.libsonnet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
'Vodka Martini': {
ingredients: [
{ kind: 'Vodka', qty: 2 },
{ kind: 'Dry White Vermouth', qty: 1 },
],
garnish: 'Olive',
served: 'Straight Up',
},
Cosmopolitan: {
ingredients: [
{ kind: 'Vodka', qty: 2 },
{ kind: 'Triple Sec', qty: 0.5 },
{ kind: 'Cranberry Juice', qty: 0.75 },
{ kind: 'Lime Juice', qty: 0.5 },
],
garnish: 'Orange Peel',
served: 'Straight Up',
},
}

garnish.txt

1
Maraschino Cherry

2.10 错误

错误可能来自语言本身(例如数组溢出)或Jsonnet代码抛出。堆栈跟踪为错误提供上下文。

  • 抛出一个错误: error "foo"
  • 在表达式前添加一条断言: assert "foo";
  • 一个自定义的错误消息: assert "foo": "message";
  • Assert字段的一个属性: Assert self.f == 10,
  • 使用自定义失败消息: assert "foo": "message",

尝试修改下面的代码以触发断言失败,并观察结果中的错误消息和堆栈跟踪。

关于除零错误为什么在相等比较时不会触发,因为该代码发生在std.length()检查之前。Jsonnet是一种惰性语言,因此变量初始化器在使用变量之前不会被计算。评估顺序很难注意到。只有在代码抛出错误或执行时间较长时,它才有意义。关于懒惰的更多讨论,请参阅设计原理

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Extend above example to sanity check input.
local equal_parts(size, ingredients) =
local qty = size / std.length(ingredients);
// Check a pre-condition
if std.length(ingredients) == 0 then
error 'Empty ingredients.'
else [
{ kind: i, qty: qty }
for i in ingredients
];

local subtract(a, b) =
assert a > b : 'a must be bigger than b';
a - b;

assert std.isFunction(subtract);

{
test1: equal_parts(1, ['Whiskey']),
test2: subtract(10, 3),
object: {
assert self.f < self.g : 'wat',
f: 1,
g: 2,
},
assert std.isObject(self.object),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"object": {
"f": 1,
"g": 2
},
"test1": [
{
"kind": "Whiskey",
"qty": 1
}
],
"test2": 7
}

2.11 参数化的整个配置(Parameterize Entire Config)

Jsonnet是封闭的:无论执行环境如何,它总是生成相同的数据。尽管这是一个重要的特性,但在一些情况下,还是需要能够获取顶层的一些参数(类似全局变量)。有两种不同方法可以做到访问顶层参数。

  • 外部变量,在配置文件或者其他任意文件中,可以使用 std.extVar("foo") 获取。
  • 顶级参数,完整的配置被表示为一个函数。

为了说明差异,我们将展示如何在每种情况下表达相同的示例。

2.11.1 外部变量

下面的例子中绑定了两个外部变量,在下面列出来了。

  • prefix 绑定到字符串 "Happy Hour "
  • brunch 绑定到 true

当Jsonnet 虚拟机启动的时候完成上述内容的绑定。有两种方式可以实现:1) Jsonnet 代码;2) 或者使用一个原始字符串。 后者更加方便一些,因为要将Jsonnet代码放入字符串中比较麻烦且不直观。

使用下面的命令行配置上边的变量:

1
2
jsonnet --ext-str prefix="Happy Hour " \
--ext-code brunch=true ...
Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
local lib = import 'library-ext.libsonnet';
{
[std.extVar('prefix') + 'Pina Colada']: {
ingredients: [
{ kind: 'Rum', qty: 3 },
{ kind: 'Pineapple Juice', qty: 6 },
{ kind: 'Coconut Cream', qty: 2 },
{ kind: 'Ice', qty: 12 },
],
garnish: 'Pineapple slice',
served: 'Frozen',
},

[if std.extVar('brunch') then
std.extVar('prefix') + 'Bloody Mary'
]: {
ingredients: [
{ kind: 'Vodka', qty: 1.5 },
{ kind: 'Tomato Juice', qty: 3 },
{ kind: 'Lemon Juice', qty: 1.5 },
{ kind: 'Worcestershire', qty: 0.25 },
{ kind: 'Tobasco Sauce', qty: 0.15 },
],
garnish: 'Celery salt & pepper',
served: 'Tall',
},

[std.extVar('prefix') + 'Mimosa']:
lib.Mimosa,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{
"Happy Hour Bloody Mary": {
"garnish": "Celery salt & pepper",
"ingredients": [
{
"kind": "Vodka",
"qty": 1.5
},
{
"kind": "Tomato Juice",
"qty": 3
},
{
"kind": "Lemon Juice",
"qty": 1.5
},
{
"kind": "Worcestershire",
"qty": 0.25
},
{
"kind": "Tobasco Sauce",
"qty": 0.15
}
],
"served": "Tall"
},
"Happy Hour Mimosa": {
"garnish": "Orange Slice",
"ingredients": [
{
"kind": "Cheap Sparkling Wine",
"qty": 3
},
{
"kind": "Orange Juice",
"qty": 3
}
],
"served": "Champagne Flute"
},
"Happy Hour Pina Colada": {
"garnish": "Pineapple slice",
"ingredients": [
{
"kind": "Rum",
"qty": 3
},
{
"kind": "Pineapple Juice",
"qty": 6
},
{
"kind": "Coconut Cream",
"qty": 2
},
{
"kind": "Ice",
"qty": 12
}
],
"served": "Frozen"
}
}

其中 library-ext.libsonnet 内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local fizz = if std.extVar('brunch') then
'Cheap Sparkling Wine'
else
'Champagne';
{
Mimosa: {
ingredients: [
{ kind: fizz, qty: 3 },
{ kind: 'Orange Juice', qty: 3 },
],
garnish: 'Orange Slice',
served: 'Champagne Flute',
},
}

2.11.2 顶级参数

也可以使用顶级参数编写相同的代码,其中整个配置都是作为函数编写的。这与使用外部变量不同点有:

  • 值必须显式地通过文件进行线程化
  • 可以提供默认值
  • 具有顶级参数的配置也可以作为库导入,并作为传入参数的函数调用

一般来说,顶级参数是对整个配置进行参数化的更安全、更简单的方法,因为变量不是全局的,配置的哪些部分依赖于它们的环境是很清楚的。然而,它们确实需要更显式地将值线程化到其他导入的代码中。下面是Jsonnet命令行工具的等效调用:

1
jsonnet --tla-str prefix="Happy Hour " --tla-code brunch=true ...
Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
local lib = import 'library-tla.libsonnet';

// Here is the top-level function, note brunch
// now has a default value.
function(prefix, brunch=false) {

[prefix + 'Pina Colada']: {
ingredients: [
{ kind: 'Rum', qty: 3 },
{ kind: 'Pineapple Juice', qty: 6 },
{ kind: 'Coconut Cream', qty: 2 },
{ kind: 'Ice', qty: 12 },
],
garnish: 'Pineapple slice',
served: 'Frozen',
},

[if brunch then prefix + 'Bloody Mary']: {
ingredients: [
{ kind: 'Vodka', qty: 1.5 },
{ kind: 'Tomato Juice', qty: 3 },
{ kind: 'Lemon Juice', qty: 1.5 },
{ kind: 'Worcestershire', qty: 0.25 },
{ kind: 'Tobasco Sauce', qty: 0.15 },
],
garnish: 'Celery salt & pepper',
served: 'Tall',
},

[prefix + 'Mimosa']: lib.Mimosa(brunch),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{
"Happy Hour Bloody Mary": {
"garnish": "Celery salt & pepper",
"ingredients": [
{
"kind": "Vodka",
"qty": 1.5
},
{
"kind": "Tomato Juice",
"qty": 3
},
{
"kind": "Lemon Juice",
"qty": 1.5
},
{
"kind": "Worcestershire",
"qty": 0.25
},
{
"kind": "Tobasco Sauce",
"qty": 0.15
}
],
"served": "Tall"
},
"Happy Hour Mimosa": {
"garnish": "Orange Slice",
"ingredients": [
{
"kind": "Cheap Sparkling Wine",
"qty": 3
},
{
"kind": "Orange Juice",
"qty": 3
}
],
"served": "Champagne Flute"
},
"Happy Hour Pina Colada": {
"garnish": "Pineapple slice",
"ingredients": [
{
"kind": "Rum",
"qty": 3
},
{
"kind": "Pineapple Juice",
"qty": 6
},
{
"kind": "Coconut Cream",
"qty": 2
},
{
"kind": "Ice",
"qty": 12
}
],
"served": "Frozen"
}
}

其中 library-tla.libsonnet 内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
// Note that the Mimosa is now
// parameterized.
Mimosa(brunch): {
local fizz = if brunch then
'Cheap Sparkling Wine'
else
'Champagne',
ingredients: [
{ kind: fizz, qty: 3 },
{ kind: 'Orange Juice', qty: 3 },
],
garnish: 'Orange Slice',
served: 'Champagne Flute',
},
}

2.12 面向对象

一般来说,面向对象使得从单个“基”定义许多变体变得很容易。与Java、C++和Python中类扩展其他类不同,在Jsonnet中,对象扩展其他对象。我们已经讨论了其中的一些原始因素,尽管是孤立的:

  • 对象(从JSON继承的)
  • 对象合成运算符 + ,它合并两个对象,当字段碰撞时选择右边
  • self 关键字,是对当前对象的引用

当这些功能结合在一起,并与以下新功能,事情变得更加有趣:

  • :: 定义的隐藏字段不会出现在生成的JSON中
  • super 关键字的含义与Python中一样
  • +: 字段语法用于覆盖深度嵌套的字段

下面的示例是虚构的,但是使用了经典的面向对象语言“derived”和“base”来演示一些特性。

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local Base = {
f: 2,
g: self.f + 100,
};

local WrapperBase = {
Base: Base,
};

{
Derived: Base + {
f: 5,
old_f: super.f,
old_g: super.g,
},
WrapperDerived: WrapperBase + {
Base+: { f: 5 },
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"Derived": {
"f": 5,
"g": 105,
"old_f": 2,
"old_g": 105
},
"WrapperDerived": {
"Base": {
"f": 5,
"g": 105
}
}
}

让我们把它弄得更具体一些,从一个模板中提取出它们的相似之处,然后推导出一些非常相似的鸡尾酒。在这些例子中,+ 运算符实际上是隐式的。在常见情况下,你写 foo + {...},+ 后面紧跟着 {+ 可以省略。试着在下面的4个例子中显式地添加 +

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
local templates = import 'templates.libsonnet';

{
// The template requires us to override
// the 'spirit'.
'Whiskey Sour': templates.Sour {
spirit: 'Whiskey',
},

// Specialize it further.
'Deluxe Sour': self['Whiskey Sour'] {
// Don't replace the whole sweetner,
// just change 'kind' within it.
sweetener+: { kind: 'Gomme Syrup' },
},

Daiquiri: templates.Sour {
spirit: 'Banks 7 Rum',
citrus+: { kind: 'Lime' },
// Any field can be overridden.
garnish: 'Lime wedge',
},

"Nor'Easter": templates.Sour {
spirit: 'Whiskey',
citrus: { kind: 'Lime', qty: 0.5 },
sweetener+: { kind: 'Maple Syrup' },
// +: Can also add to a list.
ingredients+: [
{ kind: 'Ginger Beer', qty: 1 },
],
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
{
"Daiquiri": {
"garnish": "Lime wedge",
"ingredients": [
{
"kind": "Banks 7 Rum",
"qty": 2
},
{
"kind": "Lime",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 0.5
}
],
"served": "Straight Up"
},
"Deluxe Sour": {
"garnish": "Lemon Juice twist",
"ingredients": [
{
"kind": "Whiskey",
"qty": 2
},
{
"kind": "Lemon Juice",
"qty": 1
},
{
"kind": "Gomme Syrup",
"qty": 0.5
}
],
"served": "Straight Up"
},
"Nor'Easter": {
"garnish": "Lime twist",
"ingredients": [
{
"kind": "Whiskey",
"qty": 2
},
{
"kind": "Lime",
"qty": 0.5
},
{
"kind": "Maple Syrup",
"qty": 0.5
},
{
"kind": "Ginger Beer",
"qty": 1
}
],
"served": "Straight Up"
},
"Whiskey Sour": {
"garnish": "Lemon Juice twist",
"ingredients": [
{
"kind": "Whiskey",
"qty": 2
},
{
"kind": "Lemon Juice",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 0.5
}
],
"served": "Straight Up"
}
}

其中

templates.libsonnet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
// Abstract template of a "sour" cocktail.
Sour: {
local drink = self,

// Hidden fields can be referred to
// and overrridden, but do not appear
// in the JSON output.
citrus:: {
kind: 'Lemon Juice',
qty: 1,
},
sweetener:: {
kind: 'Simple Syrup',
qty: 0.5,
},

// A field that must be overridden.
spirit:: error 'Must override "spirit"',

ingredients: [
{ kind: drink.spirit, qty: 2 },
drink.citrus,
drink.sweetener,
],
garnish: self.citrus.kind + ' twist',
served: 'Straight Up',
},
}

使Jsonnet面向对象的关键是由于 self 关键字是“后期绑定”。换句话说,就是自 self.foo 可以通过将其重写到 a + 的右边来改变其含义。这在简单的 + 的“对象合并”语义中不是这样的,其中,在执行 + 之前, self.foo 将被完全计算为一个具体的值。在上面的例子中,+ 的左边是模板。 Sour ( + 是隐含的)和右手边是一个覆盖 spirit 的对象。在 templates.Sour 定义(在 templates.libsonnet 文件中),导致引用 drink.spirit。在成分列表中返回 "Whiskey",而不是抛出错误 'Must override "spirit"'

上面例子中的大多数抽象都可以通过函数而不是面向对象来实现。实际上,上次我们谈到酸时,我们使用函数来实现这个目的。一般来说,选择使用函数还是面向对象是一个相当复杂的问题,它既可以由本能驱动,也可以由其他因素驱动。最明显的区别是,对于面向对象,您可以覆盖结果对象中的任何文件,而对于函数,您只能传递函数的特定参数的参数。例如,如果模板 templates.Sour是作为一个函数来实现的,它没有参数化配料表的一般方式,那么就不可能添加额外的配料来形成东北复活节。

到目前为止,每个例子都在 + 的右边使用了一个文字对象。这与大多数面向对象语言是一致的。但Jsonnet更通用,它有mixin。Mixin允许你使用一些“覆盖”,而不是仅仅将它们应用到一个现有的对象来获得一个新对象,你可以将它们作为第一类值传递给其他对象,并将它们应用到任何你想要的对象。下面的例子展示了mixins用于以一般方式修改鸡尾酒:

Jsonnet JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
local sours = import 'sours-oo.jsonnet';

local RemoveGarnish = {
// Not technically removed, but made hidden.
garnish:: super.garnish,
};

// Make virgin cocktails
local NoAlcohol = {
local Substitute(ingredient) =
local k = ingredient.kind;
local bitters = 'Angustura Bitters';
if k == 'Whiskey' then [
{ kind: 'Water', qty: ingredient.qty },
{ kind: bitters, qty: 'tsp' },
] else if k == 'Banks 7 Rum' then [
{ kind: 'Water', qty: ingredient.qty },
{ kind: 'Vanilla Essence', qty: 'dash' },
{ kind: bitters, qty: 'dash' },
] else [
ingredient,
],
ingredients: std.flattenArrays([
Substitute(i)
for i in super.ingredients
]),
};

local PartyMode = {
served: 'In a plastic cup',
};

{
'Whiskey Sour':
sours['Whiskey Sour']
+ RemoveGarnish + PartyMode,

'Virgin Whiskey Sour':
sours['Whiskey Sour'] + NoAlcohol,

'Virgin Daiquiri':
sours.Daiquiri + NoAlcohol,

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{
"Virgin Daiquiri": {
"garnish": "Lime wedge",
"ingredients": [
{
"kind": "Water",
"qty": 2
},
{
"kind": "Vanilla Essence",
"qty": "dash"
},
{
"kind": "Angustura Bitters",
"qty": "dash"
},
{
"kind": "Lime",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 0.5
}
],
"served": "Straight Up"
},
"Virgin Whiskey Sour": {
"garnish": "Lemon Juice twist",
"ingredients": [
{
"kind": "Water",
"qty": 2
},
{
"kind": "Angustura Bitters",
"qty": "tsp"
},
{
"kind": "Lemon Juice",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 0.5
}
],
"served": "Straight Up"
},
"Whiskey Sour": {
"ingredients": [
{
"kind": "Whiskey",
"qty": 2
},
{
"kind": "Lemon Juice",
"qty": 1
},
{
"kind": "Simple Syrup",
"qty": 0.5
}
],
"served": "In a plastic cup"
}
}

其中

sours-oo.jsonnet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
local templates = import 'templates.libsonnet';

{
// The template requires us to override
// the 'spirit'.
'Whiskey Sour': templates.Sour {
spirit: 'Whiskey',
},

// Specialize it further.
'Deluxe Sour': self['Whiskey Sour'] {
// Don't replace the whole sweetner,
// just change 'kind' within it.
sweetener+: { kind: 'Gomme Syrup' },
},

Daiquiri: templates.Sour {
spirit: 'Banks 7 Rum',
citrus+: { kind: 'Lime' },
// Any field can be overridden.
garnish: 'Lime wedge',
},

"Nor'Easter": templates.Sour {
spirit: 'Whiskey',
citrus: { kind: 'Lime', qty: 0.5 },
sweetener+: { kind: 'Maple Syrup' },
// +: Can also add to a list.
ingredients+: [
{ kind: 'Ginger Beer', qty: 1 },
],
},
}

templates.libsonnet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
// Abstract template of a "sour" cocktail.
Sour: {
local drink = self,

// Hidden fields can be referred to
// and overrridden, but do not appear
// in the JSON output.
citrus:: {
kind: 'Lemon Juice',
qty: 1,
},
sweetener:: {
kind: 'Simple Syrup',
qty: 0.5,
},

// A field that must be overridden.
spirit:: error 'Must override "spirit"',

ingredients: [
{ kind: drink.spirit, qty: 2 },
drink.citrus,
drink.sweetener,
],
garnish: self.citrus.kind + ' twist',
served: 'Straight Up',
},
}

如果还想要进一步了解语法,请阅读官方文档

3. 解析

如果不想使用命令行解析 jsonnet,也有针对Python、Go和C的官方库。

3.1 Python

安装

1
pip install jsonnet 

Python模块提供了两个函数,evaluate_file(filename)evaluate_snippet(filename, expr)

下面是一个简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import json
import _jsonnet

jsonnet_str = '''
{
person1: {
name: "Alice",
welcome: "Hello " + self.name + "!",
},
person2: self.person1 {
name: std.extVar("OTHER_NAME"),
},
}
'''

json_str = _jsonnet.evaluate_snippet(
"snippet", jsonnet_str,
ext_vars={'OTHER_NAME': 'Bob'})

json_obj = json.loads(json_str)
for person_id, person in json_obj.iteritems():
print('%s is %s, greeted by "%s"' % (
person_id,
person['name'],
person['welcome']))