Tutorial
Syntax
Любой JSON это валидная Jsonnet программа, поэтому сфокусируемся на том что jsonnet добавляет в json
Начнем с примера в котором не происходит никаких вычислений но используется новый синтаксис:
-
Поля которые являются допустимыми идентификаторами - не нуждаются в кавычках
-
Завершающие запятые на концах массивов и объектов (в голом json нельзя написать вот так
{"a":1,"b":2,"c":3,}
(запятая после тройки))
Пример:
Имеем jsonnet с запятой после тройки🚀 cat commas.jsonnet { a: 1, b: 2, c: 3, }
Рендерим (запятой нет)
🚀 jsonnet commas.jsonnet | jq -c {"a":1,"b":2,"c":3}
То есть получается валидный json (json не позволяет иметь конечную запятую)
🚀 cat commas.json { "a": 1, "b": 2, "c": 3, } 🚀 cat commas.json | jq parse error: Expected another key-value pair at line 5, column 1
-
Комментарии
/* A C-style comment. */ # A Python-style comment. // Comment
-
Можно использовать как двойные так и одинарные кавычки
Их можно миксовать, например как тут - использованы двойные -"Farmer's Gin"
вместо одинарных с экранированием'Farmer\'s Gin'
-
Текстовые блоки позволяют описывать дословные многострочные тексты -
|||
🚀 cat main.jsonnet [ ||| A clear \tred drink. |||, ] 🚀 jsonnet main.jsonnet [ "A clear\n\\tred drink.\n" ] 🚀 jsonnet main.jsonnet | jq .[] -r A clear \tred drink.
-
@
перед строкой превращает однострочную строку в дословную (aka сырая строка)🚀 cat main.jsonnet [ @'1. A clear \n\tred drink.', '2. A clear \n\tred drink.', ] 🚀 jsonnet main.jsonnet | jq .[] -r 1. A clear \n\tred drink. 2. A clear red drink.
Variables
Переменные это простейший путь избежать дублирования
Ключевое слово local
определяет переменную
Объявление переменной рядом с другими полями оканчивается запятой, в остальных случаях точкой с запятой
🚀 cat main.jsonnet
// A regular definition.
local house_rum = 'Banks Rum';
{
// A definition next to fields.
local pour = 1.5,
Daiquiri: {
ingredients: [
{ kind: house_rum, qty: pour },
],
served: 'Straight Up',
}
}
🚀 jsonnet main.jsonnet
{
"Daiquiri": {
"ingredients": [
{
"kind": "Banks Rum",
"qty": 1.5
}
],
"served": "Straight Up"
}
}
References
Другой способ избежать дублирования - ссылаться на объекты:
self
ссылается на текущий объект$
ссылается на внешний объект['foo']
обращаемся к полю объекта.foo
можно использовать если имя поля является идентификатором[10]
обращаемся к элементу массива по номеру- Разрешены произвольно длинные пути
- Разрешены срезы массивов как
arr[10:20:2]
(как в Python) - Строки можно искать/нарезать по юникодным кодам
🚀 cat main.jsonnet
local full_name = {firstname: 'Ivan', lastname: 'Dudin'};
{
local age = 24,
local username = 'vandud',
list: [
{
full_name: full_name,
age: age,
username: username
},
$['list'][0].username,
],
list2: self.list
}
🚀 jsonnet main.jsonnet
{
"list": [
{
"age": 24,
"full_name": {
"firstname": "Ivan",
"lastname": "Dudin"
},
"username": "vandud"
},
"vandud"
],
"list2": [
{
"age": 24,
"full_name": {
"firstname": "Ivan",
"lastname": "Dudin"
},
"username": "vandud"
},
"vandud"
]
}
Arithmetic
Арифметика включает в себя числовые операции такие как умножение, а так же различные операции над другими типами:
- Арифметика с плавающей точкой, побитовые операции, булева логика
- Строки можно конкатенировать через оператор
+
, который неявно преобразует один из операндов к строке если потребуется - Две строки можно сравнить через оператор
<
(unicode codepoint order) - Объекты могут быть скомбинированы через оператор
+
где правосторонний объект победит при конфликте полей - Тест есть ли поле в объекте через
in
==
глубокое равенство значений- Python-совместимое форматирование строк доступно через
%
. При комбинировании с|||
может быть использовано для темплейтинга текстовых файлов
{ | {
concat_array: [1, 2, 3] + [4], | "concat_array": [
| 1,
| 2,
| 3,
| 4
| ],
concat_string: '123' + 4, | "concat_string": "1234",
equality1: 1 == '1', | "equality1": false,
equality2: [{}, { x: 3 - 1 }] | "equality2": true,
== [{}, { x: 2 }], |
ex1: 1 + 2 * 3 / (4 + 5), | "ex1": 1.6666666666666665,
// Bitwise operations first cast to int. |
ex2: self.ex1 | 3, | "ex2": 3,
// Modulo operator. |
ex3: self.ex1 % 2, | "ex3": 1.6666666666666665,
// Boolean logic |
ex4: (4 > 3) && (1 <= 3) || false, | "ex4": true,
// Mixing objects together |
obj: { a: 1, b: 2 } + { b: 3, c: 4 }, | "obj": {
| "a": 1,
| "b": 3,
| "c": 4
| },
// Test if a field is in an object |
obj_member: 'foo' in { foo: 1 }, | "obj_member": true,
// String formatting |
str1: 'The value of self.ex2 is ' | "str1": "The value of self.ex2 is 3.",
+ self.ex2 + '.', |
str2: 'The value of self.ex2 is %g.' | "str2": "The value of self.ex2 is 3.",
% self.ex2, |
str3: 'ex1=%0.2f, ex2=%0.2f' | "str3": "ex1=1.67, ex2=3.00",
% [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' | "str4": "ex1=1.67, ex2=3.00",
% self, |
// Do textual templating of entire files: |
str5: ||| | "str5": "ex1=1.67\nex2=3.00\n"
ex1=%(ex1)0.2f |
ex2=%(ex2)0.2f |
||| % self, |
} | }
local is_even(x) = x % 2 == 0;
{ | {
result1: is_even(5), | "result1": false,
result2: is_even(4) | "result2": true
} | }
Functions
Как в питоне, функции имеют позиционные параметры, именованные параметры и дефолтные аргументы
Прерывания также поддерживаются
Множество функций уже реализовано в stdlib - https://jsonnet.org/ref/stdlib.html
// 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]),
],
}
Conditionals
Условные конструкции выглядят как if b then e else e
. Ветвь 'else' опциональна и по умолчанию возвращает 'null'
🚀 cat main.jsonnet
local my_func(s) =
if std.asciiLower(s) == 'vandud' then {
username: s,
firstname: 'Ivan',
lastname: 'Dudin',
age: '24',
position: 'DevOps'
} else {
username: s,
};
[
my_func('test'),
my_func('vAnDuD')
]
🚀 jsonnet main.jsonnet
[
{
"username": "test"
},
{
"age": "24",
"firstname": "Ivan",
"lastname": "Dudin",
"position": "DevOps",
"username": "vAnDuD"
}
]
Computed Field Names
Jsonnet объекты могут быть использованы как std::map
или как похожие структуры данных из обычных языков
- Напомним что поиск по полю можно сделать так
obj[e]
- Определить поле в объекте можно так
{[e]: ... }
self
или локальные переменные объекта не могут быть использованы при вычислении полей, поскольку объект еще не создан- Если при вычислении объекта значение поля становится
null
, то поле опускается. Это хорошо работает с условным ветвлением по умолчанию
🚀 cat main.jsonnet
local func(s) = {
[if s == 'a' then 'Abracadabra'
else if s == 'b' then 'Bob'
else if s == 'c' then 'Cucumber']: s,
};
{
a: func('a'),
b: func('b'),
c: func('c'),
}
🚀 jsonnet main.jsonnet
{
"a": {
"Abracadabra": "a"
},
"b": {
"Bob": "b"
},
"c": {
"Cucumber": "c"
}
}
Array and Object Comprehension
Что если ты хочешь создать массив или объект и ты не знаешь как много элементов/полей они будут содержать в рантайме. Jsonnet имеет Python-style конструкторы для массивов и объектов
- Может быть использована любая вложенность для
for
иif
- Вложение ведет себя как вложенный цикл, хотя тело будет записано первым
Imports
Можно импортировать и код и данные из файлов
- Конструкция
import
работает как копипаст Jsonnet-кода - Jsonnet-файлы созданные для импорта должны иметь расширение
.libsonnet
- Сырой JSON можно импортировать тем же путем
importstr
иimportbit
для импорта utf-8 и бинарных данных соответственно
Обычно импортированный Jsonnet контент складывается в локальную переменную верхнего уровня. Это похоже на способ хранения модулей из других языков программирования. Jsonnet-библиотеки обычно возвращают объект, так они могут быть легко расширены. Независимо от того соблюдается ли описанная конвенция
🚀 cat mylib.libsonnet
{
name: 'vandud',
age: 25,
gender: 'male'
}
🚀 cat textfile.txt
Jsonnet - это очень круто!
🚀 cat main.jsonnet
local mylib = import 'mylib.libsonnet';
{
name: mylib.name,
text: importstr 'textfile.txt'
}
🚀 jsonnet main.jsonnet
{
"name": "vandud",
"text": "Jsonnet - это очень круто!\n"
}
Errors
Ошибки могут возникать из-за работы языка или из-за логики кода. Трассировка даст контекст ошибки
- Чтобы вызвать ошибку:
error "foo"
- Для утверждения условия перед выражением:
assert "foo";
- Кастомный текст ошибки:
assert "foo" : "message";
- Assert поля имеют свойства:
assert self.f == 10
Parameterize Entire Config
Jsonnet герметичен: он всегда генерирует одинаковые данные вне зависимости от среды выполнения. Это важное свойство, но бывают моменты когда ты хочешь иметь выбираемые параметры на верхнем уровне. Есть два способа достичь этого:
- External variables - переменные которые доступны по всему конфигу через
std.extVar("foo")
- Top-level arguments - когда конфиг представлен в виде функции
External variables
Следующий пример привязывает две внешние переменные. Любое Jsonnet-значение может быть привязано к внешней переменной, даже функции:
prefix
привязано к строке"Happy Hour "
brunch
привязано кtrue
Значения конфигурируются когда виртуальная машина Jsonnet инициализируется за счет прокидывания либо 1) Jsonnet кода (который вычислит значение) 2) сырой строки
jsonnet --ext-str prefix="Happy Hour " \
--ext-code brunch=true ...
А в коде переменная может учитываться как-то так:
local fizz = if std.extVar('brunch') then
'Cheap Sparkling Wine'
else
'Champagne';
Top-level arguments
Альтернативно этот же самый код может быть написан с использованием аргументов верхнего уровня, когда весь конфиг описан в виде функции
Отличается следующим:
- Значения должны быть протянуты через файлы
- Должны иметься дефолтные значения
- Конфиг с аргументами верхнего уровня также может быть импортирован как библиотека и вызван как функция с прокинутыми параметрами
В общем, аргументы верхнего уровня это более безопасный и простой способ параметризации всего конфига, потому что переменные не глобальны и ясно, какие части конфигурации зависят от их окружения. Однако они требуют более явного переноса значений в другой импортируемый код. Вот эквивалентный вызов Jsonnet cli:
jsonnet --tla-str prefix="Happy Hour " \
--tla-code brunch=true ...
В коде выглядит так:
local fizz = if brunch then
'Cheap Sparkling Wine'
else
'Champagne',
(Почти то же самое)
Object-Orientation
Обычно ориентированность на объекты позволяет легко определять множество вариаций от единой "базы"
В отличие от Java, C++ и Python, где классы расширяют другие классы, в Jsonnet объекты расширяют другие объекты
- Объекты (которые мы наследуем от JSON)
- Оператор компоновки объектов
+
, который объединяет два объекта, выбирая правую часть при коллизиях - Ключевое слово
self
, ссылка на текущий объект
Когда эти функции объединяются вместе со следующими новыми функциями, все становится намного интереснее:
- Скрытые поля, определенные с помощью
::
, которые не отображаются в сгенерированном JSON-файле - Ключевое слово
super
используется для доступа к полям в базовом объекте - Синтаксис
+:
field для переопределения глубоко вложенных полей
No Comments