- PVSM.RU - https://www.pvsm.ru -
Формат данных YAML чрезвычайно усложнён. Он задумывался как удобный для человека, но в стремлении к этой цели сложность настолько увеличилась, что, по моему мнению, его разработчики достигли противоположного результата. В YAML есть куча возможностей выстрелить себе в ногу, а его дружелюбие обманчиво. В этом посте я хочу продемонстрировать это на примере.
Данный пост является нытьём и он более субъективен, чем мои обычные статьи.
JSON прост. Вся спецификация JSON [1] состоит из шести синтаксических диаграмм. Это простой формат данных с простым синтаксисом, и больше в нём нет ничего. В отличие от него, YAML сложен. Настолько сложен, что его спецификация [2] состоит из десяти глав с разделами, пронумерованными на четыре уровня в глубину, и отдельным списком ошибок [3].
Спецификация JSON не разделена на версии. В 2005 году в неё внесены два изменения [4] (удаление комментариев и добавление научной записи для чисел), но с тех пор она заморожена (уже почти два десятка лет). Спецификация YAML имеет версии. Последняя версия довольно свежая, 1.2.2 от октября 2021 года. YAML 1.2 существенно отличается от 1.1: один и тот же документ в разных версиях YAML может парситься по-разному. Ниже мы увидим много примеров этого.
JSON настолько очевиден, что Дуглас Крокфорд заявляет, что он был открыт [5], как явление, а не придуман. Я не нашёл никакой информации о том, сколько времени ему понадобилось на создание спецификации, но это были скорее часы, чем недели. Переход YAML с версии 1.2.1 на 1.2.2 потребовал многолетних усилий команды специалистов [6]:
Эта ревизия стала результатом нескольких лет работы новой команды разработки языка YAML. Каждый человек в этой команде обладает глубокими знаниями языка, написал и поддерживает важные опенсорсные фреймворки и инструменты YAML.
Более того, эта команда планирует активно развивать YAML, а не замораживать его.
При работе со столь сложным форматом, как YAML, сложно понимать все фичи и особенности его поведения. Существует целый веб-сайт [7], посвящённый выбору одного из 63 синтаксисов многострочных строк [8]. Это значит, что человеку очень сложно предсказать, как спарсится конкретный документ. Чтобы подчеркнуть это, давайте рассмотрим пример.
Рассмотрим следующий документ.
server_config:
port_mapping:
# Expose only ssh and http to the public internet.
- 22:22
- 80:80
- 443:443
serve:
- /robots.txt
- /favicon.ico
- *.html
- *.png
- !.git # Do not expose our Git repository to the entire world.
geoblock_regions:
# The legal team has not approved distribution in the Nordics yet.
- dk
- fi
- is
- no
- se
flush_cache:
on: [push, memory_pressure]
priority: background
allow_postgres_versions:
- 9.5.25
- 9.6.24
- 10.23
- 12.13
Проанализируем его по частям и посмотрим, как эти данные преобразуются в JSON.
Давайте начнём с того, что можно найти в конфигурации среды исполнения контейнера:
port_mapping:
- 22:22
- 80:80
- 443:443
{"port_mapping": [1342, "80:80", "443:443"]}
Ой, а что здесь произошло? Оказывается, числа от 0 до 59, разделённые двоеточиями — это шестидесятеричные (по основанию 60) числовые литералы [9]. Эта загадочная фича присутствовала в YAML 1.1, но была незаметно удалена из YAML 1.2, поэтому в зависимости от версии, которую использует парсер, элемент списка спарсится как 1342
или как "22:22"
. Формату YAML 1.2 уже больше десяти лет, но если вы предположите, что он широко поддерживается, то ошибётесь: последняя версия libyaml на момент написания (которая, среди прочего, используется и в PyYAML [10]) реализует YAML 1.1 и парсит 22:22
как 1342
.
Следующий фрагмент на самом деле невалиден:
serve:
- /robots.txt
- /favicon.ico
- *.html
- *.png
- !.git
YAML позволяет создать якорь добавив &
и имя перед значением, а позже вы сможете ссылаться на это значение при помощи псевдонима: *
, за которым следует имя. В этом случае якоря не заданы, поэтому псевдонимы недействительны. Давайте пока избавимся от них и посмотрим, что произойдёт.
serve:
- /robots.txt
- /favicon.ico
- !.git
{"serve": ["/robots.txt", "/favicon.ico", ""]}
Теперь интерпретация зависит от используемого парсера. Элемент, начинающийся с !
— это тэг [11]. Эта фича предназначена для того, чтобы позволить парсеру преобразовывать довольно ограниченные типы данных YAML в расширенные типы, которые могут существовать на языке реализации. Тэг, начинающийся с !
, интерпретируется парсером по своему усмотрению, часто вызовом конструктора с соответствующим именем и передачей ему значения, следующего за тэгом. Это значит, что загрузка непроверенного YAML-документа в общем случае небезопасна, потому что может привести к исполнению произвольного кода. (В Python можно избежать этой проблемы, используя yaml.safe_load
вместо yaml.load
.) В приведённом выше случае PyYAML не удаётся загрузить документ, потому что она не знает тэг .git
. YAML-пакет языка Go менее строг, он возвращает пустую строку.
Эта проблема настолько широко известна, что её назвали проблемой Норвегии [12]:
geoblock_regions:
- dk
- fi
- is
- no
- se
{"geoblock_regions": ["dk", "fi", "is", false, "se"]}
Что здесь делает false
? Литералы off
, no
и n
и их различные вариации с заглавным регистром (но не все [13]!) в YAML 1.1 считаются false
, а on
, yes
и y
считаются true. В YAML 1.2 эти альтернативные написания булевых литералов больше не допускаются, но в реальном мире они встречаются столь часто, что соответствующий стандарту парсер будет испытывать трудности с чтением многих документов. Поэтому разработчики YAML-библиотеки языка Go приняли решение [14] реализовать собственный вариант, находящийся примерно посередине между YAML 1.1 и 1.2; в зависимости от контекста парсер ведёт себя по-разному:
YAML-пакет поддерживает бОльшую часть YAML 1.2, однако сохраняет часть поведения 1.1 для обратной совместимости. Булевы литералы YAML 1.1 (yes/no, on/off) поддерживаются, если они декодируются в типизированное значение bool. В противном случае они ведут себя как строка.
Стоит заметить, что библиотека ведёт себя так только с версии 3.0.0, выпущенной в мае 2022 года. Более ранние версии ведут себя иначе [15].
В JSON ключи всегда являются строками, однако в YAML они могут любыми значениями, в том числе и булевыми.
flush_cache:
on: [push, memory_pressure]
priority: background
{
"flush_cache": {
"True": ["push", "memory_pressure"],
"priority": "background"
}
}
В сочетании с ранее описанной особенностью интерпретации on
как булева значения это приводит к тому, что одним из ключей словаря становится true
. Если это происходит, то результат зависит от языка, выполняющего преобразование в JSON. В Python это превращается в строку "True"
. В реальном мире ключ on
встречается часто, потому что используется в GitHub Actions [16]. Мне было очень любопытно узнать, что ищет парсер GitHub Actions внутри, "on"
или true
.
Если оставлять строки без кавычек, они запросто могут превращаться в числа.
allow_postgres_versions:
- 9.5.25
- 9.6.24
- 10.23
- 12.13
{"allow_postgres_versions": ["9.5.25", "9.6.24", 10.23, 12.13]}
Возможно, список — это натянутый пример, но представьте, что нужно обновить файл конфигурации, в котором указано единственное значение 9.6.24, и нужно заменить его на 10.23. Вспомните ли вы, что нужно добавить кавычки? Проблема становится ещё более коварной из-за того, что многие приложения с динамической типизацией при необходимости преобразуют неявным образом число в строку, поэтому бОльшую часть времени документ будет работать нормально, однако в некоторых контекстах может ломаться. Например, следующий шаблон Jinja принимает и version: "0.0"
, и version: 0.0
, но для первого варианта идёт только по ветви true.
{% if version %}
Latest version: {{ version }}
{% else %}
Version not specified
{% endif %}
Это всё, что я смог уместить в один вымышленный пример. Есть и другие загадочные поведения YAML, не вошедшие в него: директивы [17], целочисленные значения, начинающиеся с 0
и являющиеся восьмеричными литералами (но только в YAML 1.1), ~
как альтернативное написание null
и ?
, добавляющий сложный ключ преобразования [18].
Вероятно, вы заметили, что ни в одном из моих примеров не включена подсветка синтаксиса. Возможно, я несправедлив к YAML, потому что подсветка синтаксиса выделяет специальные конструкции, поэтому мы, по крайней мере, сможем видеть значения, а не обычные строки. Однако из-за множества используемых версий YAML и разных уровней изощрённости функций подсветки на это полагаться нельзя. И данном случае я не пытаюсь придираться: Vim, генератор моего блога, GitHub и Codeberg на самом деле подсвечивали пример документа из поста каждый по-своему. Ни один из них не выделил одно и то же подмножество значений как строки!
Надеюсь, теперь понятно, что работа с YAML, по меньшей мере, имеет свои тонкости. Ещё более тонкая тема — конкатенация и изолирование произвольных текстовых фрагментов таким образом, что результат становится валидным YAML-документом, но не тем, который вы ожидаете. Сюда же стоит добавить значимость для YAML пробелов (whitespace). В результате мы получаем формат, о сложностях шаблонизации которого создают мемы [19]. Я искренне не понимаю, почему инструменты, основанные на такой подверженной ошибкам практике [20], получили столь большое внимание, несмотря на то, что существует более безопасная, простая и мощная альтернатива: генерация JSON.
Я думаю, основная причина доминирования YAML, несмотря на его проблемы, заключается в том, что долгое время он был единственным жизнеспособным форматом конфигураций. Часто нам требуются списки и вложенные данные, поэтому «плоские» форматы наподобие ini исключаются. XML шумный и вручную его писать неудобно. Но самое важное, что нам нужны комментарии, а это исключает применение JSON. (Как мы говорили ранее, изначально в JSON существовали комментарии, но их удалили, потому что люди начали помещать в них директивы парсинга. Думаю, это подходящий выбор для формата сериализации, однако из-за этого JSON не подходит как язык конфигураций.) Итак, если на самом деле нам нужна модель данных JSON, только с синтаксисом, допускающим комментарии, то какие у нас есть варианты?
true
и false
вместо yes
и no
, держась подальше от неявных особенностей. Сложность здесь в том, что любая конструкция, не запрещённая явным образом, рано или поздно всё равно попадёт в кодовую базу, а я не знаю ни одного хорошего инструмента, способного принудительно обеспечивать применение безопасного подмножества YAML.Часто выбор формата зависит не от нас и приложение может принимать только YAML. Однако не всё потеряно, ведь YAML — это надмножество JSON, поэтому любой инструмент, способный создавать JSON, можно использовать для генерации YAML-документа.
Иногда изначально приложению требуется только один формат конфигурации, однако со временем может появиться много схожих строк файлов конфигурации и вам захочется передавать между ними части, а также абстрагировать повторения. Такое случается, например, с Kubernetes и GitHub Actions. Когда язык конфигураций не поддерживает абстракций, люди обычно обращаются к шаблонизации, а это плохая идея по описанным выше причинам. Лучше подойдут настоящие языки программирования, возможно, предназначенные для конкретной области. Мои любимые — это Nix и Python:
json.dump
[28]. Автономный файл Python, выводящий JSON на stdout, послужит вам добрую службу!Наконец, в этой категории есть инструменты, которые я недостаточно много использовал, чтобы рекомендовать с уверенностью, но заслуживающие упоминания:
YAML задумывался как более дружественная для человека альтернатива JSON, но из-за всех своих особенностей он стал таким сложным форматом с таким множеством странных и неожиданных поведений, что людям сложно предсказать, как будет парситься конкретный YAML-документ. Если вам нужен формат конфигураций, то TOML — это дружественный формат без выстрелов в ногу, присущих YAML. В случаях, когда вы вынуждены пользоваться YAML, жизнеспособным решением будет генерация JSON из более подходящего языка. Генерация JSON также открывает возможности для абстрагирования и многократного использования в той степени, которую сложно безопасно достичь шаблонизацией YAML.
Автор:
PatientZero
Источник [32]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/382029
Ссылки в тексте:
[1] Вся спецификация JSON: https://www.json.org/json-en.html
[2] его спецификация: https://yaml.org/spec/1.2.2/
[3] списком ошибок: https://yaml.org/spec/1.2/errata.html
[4] два изменения: https://youtu.be/-C-JoyNuQJs?t=965
[5] был открыт: https://www.youtube.com/watch?v=-C-JoyNuQJs
[6] многолетних усилий команды специалистов: https://yaml.com/blog/2021-10/new-yaml-spec/
[7] целый веб-сайт: https://yaml-multiline.info/
[8] 63 синтаксисов многострочных строк: https://stackoverflow.com/a/21699210/135889
[9] шестидесятеричные (по основанию 60) числовые литералы: https://yaml.org/spec/1.1/#id858600
[10] PyYAML: https://pypi.org/project/PyYAML/6.0/
[11] тэг: https://yaml.org/spec/1.2.2/#3212-tags
[12] проблемой Норвегии: https://hitchdev.com/strictyaml/why/implicit-typing-removed/
[13] но не все: https://yaml.org/type/bool.html
[14] приняли решение: https://github.com/go-yaml/yaml/tree/v3.0.1#compatibility
[15] Более ранние версии ведут себя иначе: https://github.com/go-yaml/yaml/commit/b145382a4cda47600eceb779844b8090b5807c4f
[16] используется в GitHub Actions: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
[17] директивы: https://yaml.org/spec/1.2.2/#68-directives
[18] сложный ключ преобразования: https://yaml.org/spec/1.2.2/#example-mapping-between-sequences
[19] мемы: https://twitter.com/memenetes/status/1600898397279502336
[20] инструменты, основанные на такой подверженной ошибкам практике: https://helm.sh/docs/chart_best_practices/templates/
[21] TOML: https://toml.io/en/
[22] JSON с комментариями: https://code.visualstudio.com/docs/languages/json#_json-with-comments
[23] JSON с запятыми и комментариями: https://nigeltao.github.io/blog/2021/json-with-commas-comments.html
[24] Nix: https://nixos.org/manual/nix/stable/language/index.html
[25] менеджером пакетов Nix: https://nixos.org/
[26] экспорт в JSON: https://nixos.org/manual/nix/stable/language/builtins.html#builtins-toJSON
[27] Python: https://www.python.org/
[28] json.dump
: https://docs.python.org/3/library/json.html?highlight=json%20dump#json.dump
[29] Dhall: https://dhall-lang.org/
[30] Cue: https://cuelang.org/
[31] Hashicorp Configuration Language: https://github.com/hashicorp/hcl
[32] Источник: https://habr.com/ru/post/710414/?utm_source=habrahabr&utm_medium=rss&utm_campaign=710414
Нажмите здесь для печати.