Info
Content

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
Back to top