Данное руководство появилось при описании грамматики языка JSONata для плагина Dochub.
Руководство содержит краткое изложение оригинальной документации https://docs.jsonata.org/. Примеры призваны ускорить погружение в язык jsonata и помочь в описании архитектуры при использовании инструмента Dochub.
JSONata появилась в 2016 году под руководством Andrew Coleman и его коллегами! Данный язык призван помочь при работе с большими JSON-объектами, для их переформатирования и реструктуризации. Данный языка позволят создавать сложные запросы, выраженные в компактной и интуитивно понятной форме. JSONata включает в себе лучшие подходы которые используются в SQL, XPath и XQuery, что делает его универсальным и выразительным языком для работы со структурами JSON. Результаты запросов можно отформатировать в любую структуру вывода JSON, используя знакомый синтаксис объектов JSON. В сочетании с возможностью создания пользовательских функций можно создавать расширенные выражения для обработки любых запросов и задач преобразования JSON, например:
- Манипулировать строками;
- Извлечение значений;
- Создание сложных структур вывода JSON-объекта;
- позволяет осуществлять выборку, фильтрацию, сортировку;
По своей сути JSONata — это легкий язык запросов и преобразования данных.
Исходный пример описания сущности прикладного компонента архитектуры, который используется на протяжении всего руководства.
{
"title": "Компонент прикладной архитектуры",
"config": {
"root_menu": "Архитектура/Прикладная"
},
"description": "Прикладные компоненты являются составными частями прикладного сервиса",
"schema": {
"type": "object",
"additionalProperties": false,
"seaf.app.sber.component_type": {
"title": "Тип компонента",
"type": "string",
"default": "component",
"enum": [
"component",
"service"
]
},
"seaf.app.sber.profile": {
"title": "Базовое описание компонента",
"type": "object",
"properties": {
"id": {
"title": "Уникальный идентификатор компонента",
"type": "string",
"format": "uuid"
}
}
},
"live_stage": {
"title": "Этап жизненного цикла",
"enum": [
"Эскиз",
"В разработке / приобретение",
"Внедрение / Не введена в эксплуатацию",
"Опытная эксплуатация",
"Промышленная эксплуатация",
"Архивная"
]
}
}
}
JSON объекты представляется как ассоциативный массив, например: map. Для того чтобы получить доступ к нужному элементу объекта, используйте оператор '.'. Например:
entities.components.title
Оператор . позволяет осуществлять доступ к элементам нашего объекта
Вывод:
На выходе получаем тип string.
"Компонент прикладной архитектуры"
В зависимости от запроса может возвращаться числовой тип, null, undefined или массив и т.д.
Если запрашиваемого значения нет, то вернется null.
Если требуется обратиться к элементам массива, то вы можете это сделать следующим образом:
entities.components.schema.'$defs'.'seaf.app.sber.component_type'.enum[-1]
Элементы массива индексируются с 0, также есть возможность обращаться к элементам в обратном порядке, например: [-1] и т.д. Обратите внимание, если свойство объекта содержит пробелы, ключевые слова языка jsonata или составное имя, то его требуется экранировать через ковычки ''.
entities...enum[[0..1]] - в качестве индекса массива, можно использовать диапазоны
[0..1] - диапазон значений от 0 до 1, интервалы включены.
Если у нас в качестве объекта определен массив, например:
[
{
"object 1": [
1,
2
]
},
{
"object 1": [
3,
4
]
}
]
В этом случае у нас нет имени свойства и мы не можем обратиться к элементу нашего массива! Для того чтобы получить доступ к элементам массива требуется сделать следующее:
$[0] /* Оператор '$' - получить доступ ко всему документу */
Вывод:
1
$[0].'object 1' /*Получили плоскую структуру.*/
Вывод:
[
1,
2,
3,
4
]
JSonata позволяет работать с большими входными объектами и эффективно
трансформировать в нужные нам объект.
Jsonata предоставляет возможность создания новых объектов
{
"my-object": entities /* где entities поле существующего объекта */
}
Мы можем добавлять новые поля
{
"my-object": entities,
"a_new_property": "a value"
}
Создание новой структуры объекта
{
"data": {
"device": components,
"when": entities
}
}
{
"Phone": [
{
"type": "home",
"number": "0203 544 1234"
},
{
"type": "office",
"number": "01962 001234"
},
{
"type": "office",
"number": "01962 001235"
},
{
"type": "mobile",
"number": "077 7700 1234"
}
]
}
Примеры:
Phone[type='office'].number /* [type='office'] - указываем предикат, для фильтрации элементов массива*/
[выражение] - если выражение возвращает true, то значение остается, если false, то значение удаляется из результирующей выборки.
Вывод
[
"01962 001234",
"01962 001235"
]
Если при фильтрации элементов массива возвращается один элемент, но мы хотим в результате получить массив, то можно сделать следующееЖ
Phone[type='home'][] /*[] можно использовать префексную форму, так и постфиксную, например: Phone[][type='home']*/
Вывод:
[
{
"type": "home",
"number": "0203 544 1234"
}
]
Использование Wildcards
{
"live_stage": {
"title": "Этап жизненного цикла",
"enum": [
"Эскиз",
"В разработке / приобретение",
"Внедрение / Не введена в эксплуатацию",
"Опытная эксплуатация",
"Промышленная эксплуатация",
"Архивная"
]
}
}
live_stage.* /* получить значения всех полей объекта live_stage */
Вывод:
[
"Этап жизненного цикла",
"Эскиз",
"В разработке / приобретение",
"Внедрение / Не введена в эксплуатацию",
"Опытная эксплуатация",
"Промышленная эксплуатация",
"Архивная"
]
Получение значений всех дочерних объектов
*.components
Для получения значений объекта на произвольной глубине вложенности можно использовать Multi-level wildcard!
**.enum
Вывод:
[
"component",
"service",
"Централизованная",
"Нецентрализованная",
"Отсутствует",
"Mission Critical",
"Business Critical",
"Business Operational",
"Office Productivity",
"Эскиз",
"В разработке / приобретение",
"Внедрение / Не введена в эксплуатацию",
"Опытная эксплуатация",
"Промышленная эксплуатация",
"Архивная"
]
Литерал - это то что мы можем выразить односложно, например: число, строка.
Строковые литералы в jsonata создаются следующим образом: "тестовая строка" или 'тестовая строка'.
entities.components.title & ' AC'
Вывод
"Компонент прикладной архитектуры AC"
entities.components.schema.(type & ' ' & additionalProperties)
Вывод
"object false"
Язык jsonata поддерживает работу с числами как и во многих других языках.
+ сложение
- вычитание
* умножение
/ деление
% деление по модулю
Операции сравнения:
= equals
!= not equals
< less than
<= less than or equal
> greater than
>= greater than or equal
in value is contained in an array
Логические выражения:
and
or
На языке jsonata можно формировать выходные объекты различного типа, например:
- string - 'Привет Мир!'
- number - 23.5
- Boolean - true, false
- null - null
- object - {"key1": "value1"}
- array - ["value1", value2]
Рассмотрим 2 варианта, часто встречающихся на практике:
В качестве примера возьмем простой объект
{
"Email": [
{
"type": "work",
"address": [
"[email protected]",
"[email protected]"
]
},
{
"type": "home",
"address": [
"[email protected]",
"[email protected]"
]
}
]
}
1) Создание массивов
Email.address
Вывод:
[
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
]
Видим, что 2 массива объединяются в один Если мы хотим, чтобы каждный элемент находился в отдельном массиве, то нужно сделать следующее
Email.[address]
Вывод:
[
[ "[email protected]", "[email protected]" ],
[ "[email protected]", "[email protected]" ]
]
Можем помещать различные элементы нашего объекта помещать в массив, например:
[property1, property2].Object
1) Создание объектов
Phone.{type: number}
Вывод:
[
{ "home": "0203 544 1234" },
{ "office": "01962 001234" },
{ "office": "01962 001235" },
{ "mobile": "077 7700 1234" }
]
Комбинирование пар ключ/значение в одно объекте
Phone{type: number}
Вывод:
{
"home": "0203 544 1234",
"office": [
"01962 001234",
"01962 001235"
],
"mobile": "077 7700 1234"
}
Комбинирование пар ключ/значение в одно объекте и группирование всех чисел в массиве
Phone{type: number[]}
Вывод:
{
"home": [
"0203 544 1234"
],
"office": [
"01962 001234",
"01962 001235"
],
"mobile": [
"077 7700 1234"
]
}
В JSONata все является выражением.
Выражение состоит из значений, функций и операторов, которые при вычислении дают результирующее значение.
Функции и операторы применяются к значениям, которые сами могут быть результатами вычисления подвыражений.
При арифметических опрерациях возможно задавать приоритет с помощью круглых скобок, например:
(5 + 7) * 3
Для более сложных выражений можно использовать следующий подход:
Product.(Price * Quantity) - Price b Quantity являются свойствами объека Product
Можно выделять в блок набор выражений разделенных точкой с запятой, например:
(expr1; expr2; expr3) - каждое выражение в блоке вычисляется последовательно, результат последнего выражения возвращается как результат блока.
Язык предоставляет несколько способов для сортировки массивов.
1) Использование функции $sort()
2) order-by operator
Группировка данных
Account.Order.Product{`Product Name`: Price}
Result
{
"Bowler Hat": [ 34.45, 34.45 ],
"Trilby hat": 21.67,
"Cloak": 107.99
}
Account.Order.Product {
`Product Name`: {"Price": Price, "Qty": Quantity}
}
Result
{
"Bowler Hat": {
"Price": [ 34.45, 34.45 ],
"Qty": [ 2, 4 ]
},
"Trilby hat": { "Price": 21.67, "Qty": 1 },
"Cloak": { "Price": 107.99, "Qty": 1 }
}
String Functions Numeric Functions Aggregation Functions Boolean Functions Array Functions Object Functions Date/Time Functions Higher Order Functions
Jsonata позволяет создавать функции
Для создании функии нужно придерживаться простых правил
- указывать область видимисти с помощью ().
(
$localizeTemperature := function($degrees_celsius, $country) {(
$conversion_ratio := (9 / 5);
($country = "US") ? (($degrees_celsius * $conversion_ratio) + 32)
: $degrees_celsius;
)};
{
"temp": $localizeTemperature(body.temp, best_country)
}
)
Инструкция - это синтаксическая единица языка, выражающее действие.
Path Operators Numeric Operators Comparison Operators Boolean Operators Other Operators
<style type="text/css"> .tg {border-collapse:collapse;border-spacing:0;} .tg td{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px; overflow:hidden;padding:10px 5px;word-break:normal;} .tg th{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px; font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal;} .tg .tg-wzsp{font-size:14px;font-style:italic;font-weight:bold;text-align:left;vertical-align:top} .tg .tg-62xo{font-size:14px;font-weight:bold;text-align:center;vertical-align:top} .tg .tg-13pz{font-size:18px;text-align:center;vertical-align:top} .tg .tg-6nwz{font-size:14px;text-align:center;vertical-align:top} .tg .tg-ltad{font-size:14px;text-align:left;vertical-align:top} .tg .tg-6t3r{font-style:italic;font-weight:bold;text-align:left;vertical-align:top} .tg .tg-0lax{text-align:left;vertical-align:top} </style>Функция | Описание | Пример |
---|---|---|
Функции работы со строками | ||
$string() | ||
$length() | ||
$substring() | ||
$substringBefore() | ||
$substringAfter() | ||
$uppercase() | ||
$lowercase() | ||
$trim() | ||
$pad() | ||
$contains() | ||
$split() | ||
$join() | ||
$match() | ||
$replace() | ||
$eval() | ||
$base64encode() | ||
$base64decode() | ||
$encodeUrlComponent() | ||
$encodeUrl() | ||
$decodeUrlComponent() | ||
$decodeUrl() | ||
Функции работы с числами | ||
$number() | ||
$abs() | ||
$floor() | ||
$ceil() | ||
$round() | ||
$power() | ||
$sqrt() | ||
$random() | ||
$formatNumber() | ||
$formatBase() | ||
$formatInteger() | ||
$parseInteger() | ||
Агрегатные функции | ||
$sum() | ||
$max() | ||
$min() | ||
$average() | ||
Логические функции | ||
$boolean() | ||
$not() | ||
$exists() | ||
Функции работы с массивами | ||
$count() | ||
$append() | ||
$sort() | ||
$reverse() | ||
$shuffle() | ||
$distinct() | ||
$zip() | ||
Функции работы с объектами | ||
$keys() | ||
$lookup() | ||
$spread() | ||
$merge() | ||
$sift() | ||
$each() | ||
$error() | ||
$assert() | ||
$type() | ||
Date/Time функции | ||
$now() | ||
$millis() | ||
$fromMillis() | ||
$toMillis() | ||
Функции высшего порядка | ||
$map() | ||
$filter() | ||
$single() | ||
$reduce() | ||
$sift() |
(
/* Определяем шаблон для идентификаторов */
$isl2 := /^([^\.]*)(\.([^\.]*))?$/;
/* Генерируем список компонентов L1 ровня */
$result := $mergedeep($.components.$spread().(
$id := $keys()[0];
$node := $.*;
$levels := $isl2($id).groups;
/* Формируем ноду */
$node :=
/* Если это компонент уровня L2 */
$levels[2]
/* Создаем виртуальный компонент L1 с информацией о связях */
? $merge([
{
$levels[0]: {
"links":
$distinct($node.links.{
/* Преобразуем идентификатор связи к L1 */
"id": $split(id, ".")[0],
"title": title,
"direction": "--"
})[id != $levels[0]]
}
}
])
/* Иначе, преобразуем связи компонента к уровню L1 */
: $merge([
{
$id: $merge([$.*, {
"links":
$distinct($node.links.{
/* Преобразуем идентификатор связи к L1 */
"id": $split(id, ".")[0],
"title": title,
"direction": "--"
})[id != $levels[0]]
}])
}
])
));
/* Перезаписываем данные манифеста новыми компонентами */
$merge([$,
{ "components": $result}
]);
)
(
$business_entities := $.business_entities;
$makeLocation := function($id) {(
$arrleft := function($arr ,$count) {
$map($arr, function($v, $i) {
$i <= $count ? $v
})
};
$domains := $split($id, ".");
"Документы/Моё болото/Бизнес-сущности/" & $join($map($domains, function($domain, $index) {(
$lookup(business_entities, $join($arrleft($domains, $index), ".")).title
)}), "/");
)};
$append(
[{
"icon": *.icon, /* Получаем иконку */
"link": "entities/business_entities/business_entities_list",
"location": "Документы/Моё болото/1. Бизнес-сущности"
}],
[{
"icon": *.icon, /* Получаем иконку */
"link": "entities/business_entities/business_entities_in_systems",
"location": "Документы/Моё болото/2. Список бизнес-сущностей в системах"
}]
);
$entities := [$.business_entities.$spread().$merge([$.*, {"id": $keys($)}])];
$entities [id=$params.id];
$[system_id=$params.system_id]
)
(
[{
/* Размещение в пользовательском меню */
"location": "C4 Model",
/* URI представления context см. ниже.*/
"link": "entities/c4model/context"
}];
/* В переменной $parent содержатся переданные параметры из URI */
$parent_id := $params.parent;
/* Определяем уровень диаграммы */
$levels := $split($parent_id, ".");
$level := $count($levels);
/* Сохраняем корень данных */
$c4model := c4model;
/* Получаем компонент, который ходим раскрыть. Для L1 = undefined */
$parent := $lookup($c4model, $parent_id);
/* Генерируем шаблон для выбора компонентов диаграммы */
$pattern := $eval("/^" & ($parent_id != "" ? $parent_id & "." : "") & "[a-zA-Z0-9\\_]*$/");
/* Обходим все компоненты сущности C4Model */
$nodes := $c4model.$spread().(
$id := $keys()[0];
/* Обрабатываем компонент, если идентификатор компонента соответствует искомому */
$match($id, $pattern) ? $merge([$.*, {
/* Сохраняем идентификатор компонента в массиве компонентов диаграммы */
"$id": $id
}])
);
/* Находим связь выбранных копонентов с внешними */
$extraNodes := $distinct([$nodes.links.(
$not($exists($match(id, $pattern))) ? $merge([
$lookup($c4model, id),
{
"$id": id,
"links": [],
"boundary": "*"
}
])
)]);
/* Генерируем код Mermaid компонентов */
$node2code := function($nodes, $bid) {(
$join([$nodes[$bid ? boundary = $bid : $not($exists(boundary))].(
/* Формируем заголовок компонента */
$title := $level < 2
/* Если уровень выше L2, даем возможность "провалиться" глубже */
? "![" & title & "](/entities/c4model/context?parent=" & $."$id" & ")"
/* иначе нет */
: title;
entity & "(" & $."$id" & ", \"" & $title & "\", \"" & description & "\")"
)], "\n") & "\n"
)};
/* Генерируем код Mermaid границ и их наполнения */
$components := function($parent) {(
$pattern := $eval("/^" & $parent & "[a-zA-Z0-9\\_]*$/");
$boundaries := $distinct($nodes.boundary)^(boundary);
$join($boundaries[$match($, $pattern)].(
"Boundary(" & $ & ", \"" & $ & "\") {\n"
& $node2code($nodes, $)
& $join($components($ & "."), "\n")
& "\n}"
), "\n") & "\n"
)};
/* Парсим связи компонентов */
$relations := function() {(
$join([$nodes.(
$from := $."$id";
links.{
"from": $from,
"to": id,
"direction": direction,
"title": title,
"description": description
}
)].(
(direction = "<->" ? "BiRel" : "Rel")
& "("
& from
& ", " & to
& ", \"" & title & "\""
& ", \"" & description & "\""
& ")"
)
, "\n") & "\n"
)};
/* Генерируем код диаграммы Mermaid */
$code := function () {(
/* Сначала выводим вгешние компонент обнаруженные в связях */
$node2code($extraNodes, "*")
/* Если уровень глубже L1, создаем контейнер */
& ($level > 0 ? "Container_Boundary(" & $parent_id & ", \"\") {\n" : "")
/* Выводим обнаруженные без областей */
& $node2code($nodes)
/* Выводим обнаруженные компоненты разложенные в области */
& $components("")
/* Если нужно, завершаем контейнер */
& ($level > 0 ? "}\n" : "")
/* Генерируем связи компонентов на диаграмме */
& $relations()
)};
/* Возвращаем результирующие данные */
{
/* Определяем уровень представления */
"notation": $lookup({
"0": "C4Context",
"1": "C4Container",
"2": "C4Component"
}, $string($level)),
/* Генерируем код */
"code":
/* Если количество компонентов на диаграмме есть */
$count($nodes) > 0
/* выводом */
? $code()
/* иначе сообщаем, что внутри пусто */
: "\nBoundary(bempty, \"\") {\nSystem(sempty, \"Здесь пусто\")\n}"
,
/* Генерируем заголовок диаграммы */
"title":
/* Если есть компонент верхнего уровня */
$parent
/* Возвращаем его название */
? $parent.title
/* Иначе идентификатор, а если он пустой, то считаем диаграмму L1 */
: ($parent_id ? $parent_id : "Context")
}
)
- "Jsonata: Query and Transformation Language for JSON Data".
- "Mastering JSONata" by Bikram Kundu.
- "JSON At Work" by Tom Marrs.
- "Querying JSON with JSONata".
- "Introducing JSONata – A Query and Transformation Language for JSON".
- Николай Темняков
- Илья Караваев