Блог

Синтаксис yaml для js разработчиков

September 08, 2020

Photo by Ferenc Almasi

Синтаксис yaml (файлы yml) — один из самых распространенных синтаксисов используемых для написания конфигов. Например: конфиги CI чуть ли не для всех известных CI/CD провайдеров, openapi (swagger), docker-compose, helm, k8s, немалая часть js инструментов поддерживают конфиги как в формате json/js, так и в формате yml. Даже если вы работаете с фронтендом и особо не используете nodejs, вы так или иначе сталкивались с подобными конфигами. Как минимум, чтобы настроить ci для фронта.

Причина популярности yaml формата, на мой взгляд, в том, что этот формат не требует дополнительных скобочек, кавычек и прочего. Этот формат чем-то похож на markdown в мире форматов данных. Но при этом он очень строг в отношении отступов (количества пробелов), так как отступы используются для определения структуры.

Вторая причина — это наличие дополнительного синтаксиса, который позволяет уменьшить количество копипасты в конфигах и реюзать части конфига в нескольких местах, выполнять мердж объектов, писать многострочные строки (да, multiline strings так себе на русском звучит). А еще в yaml из коробки работают комментарии (#), которые отсутствуют в классическом формате JSON.

Одна только проблема, что этот синтаксис довольно не интуитивный для js разработчиков (на мое субъективное мнение, конечно). Потому в этом посте я хочу немного рассказать про некоторые возможности yaml проведя аналогию с языком программирования JavaScript и привести примеры использования yaml alias в конфигах.

Типы данных и примитивы в yaml

Сперва маленький пример того, как выглядят объекты и массивы в yaml:

# это комментарий
array:
  - 'one'
  - two
  - 3
  - foo: 'foo'
    bar: 'bar'
  # мы так же можем использовать что-то похожее на json
  - { some: 'json' }
  - [foo, bar]

object:
  one: 'one'
  two: two
  three: 3
  truth: true
  empty: null
  another_null:

На выходе получим следующий объект:

const result = {
  array: [
    'one',
    'two',
    3,
    {
      foo: 'foo',
      bar: 'bar',
    },
    {
      some: 'json',
    },
    ['foo', 'bar'],
  ],
  object: {
    one: 'one',
    two: 'two',
    three: 3,
    truth: true,
    empty: null,
    another_null: null,
  },
};

Из примера выше видно, что yaml умеет распознавать примитивы: строки, числа, булево и null. Даже если строку не обвернуть в кавычки, yaml корректно ее распарсит как строку (при условии, если строка не будет похожа на другой примитив).

Объекты задаются в виде key: value, если на месте value будет список элементов в формате - item, мы получим массив. Кроме того, никто не мешает нам вместо item начать писать список в формате key: value, чтобы добавить объект в массив.

На верхнем уровне мы получили объект (значение переменой result) по той причине, что использовали синтаксис key: value на верхнем уровне yaml файла. Если мы захотим получить массив в качестве result, тогда нам нужно начать объявление файла с использования - item синтаксиса:

- one
- 2
- three: 3
const result = [
  'one',
  2,
  {
    three: 3,
  },
];

Предыдущий пример был скорее как бонус, так как обычно конфиги все же описываются как объекты.

Объявление и использование yaml алиасов

Алиасы в yaml дают возможность переиспользовать части конфига в нескольких местах. Алиасы чем-то похожи на переменные (скорее даже константы) из традиционных языков программирования. Для того, чтобы объявить yaml алиас, вам необходимо использовать синтаксис &exampleName, а для того, чтобы в дальнейшем считать значение алиаса мы используем *exampleName. Я так полагаю, что этот синтаксис выполнен по аналогии с указателями в C.

object: &objectValue
  one: &oneValue 'one'

array: &arrayValue
  - *oneValue
  - 'two'

foo:
  - *arrayValue
  - *objectValue

Выше приведенный yaml с алиасами примерно соответствует следующему JavaScript коду, где алиасы заменены на переменные:

const oneValue = 'one';
const objectValue = {
  one: oneValue,
};
const arrayValue = [oneValue, 'two'];

const result = {
  object: objectValue,
  array: arrayValue,
  foo: [arrayValue, objectValue],
};

То есть с помощью & мы просто присваиваем значение переменной, а с помощью * считываем это самое значение.

Выполнение shallow merge объектов

Еще один полезный оператор <<: необходим для выполнения shallow merge объектов:

variables: &variables
  GITHUB_URL: https://github.com
  GITLAB_URL: https://gitlab.com

urls: &defaultUrls
  HOMEPAGE_URL: https://udf.su
  <<: *variables
  GOOGLE_URL: https://google.com

defaultConfig: &defaultConfig
  title: 'Yaml'
  urls: *defaultUrls

newUrls: &newUrls
  urls:
    YAML_URL: https://yaml.org

mergedConfig:
  # вот здесь мы полностью перезапишем поле `urls`
  <<: [*defaultConfig, *newUrls]
  description: 'Yaml merge example'

В JavaScript этот оператор полностью соответствует spread оператору (за исключением того, что в yaml нельзя конкатенировать массивы):

const variables = {
  GITHUB_URL: 'https://github.com',
  GITLAB_URL: 'https://gitlab.com',
};
const defaultUrls = {
  HOMEPAGE_URL: 'https://udf.su',
  ...variables,
  GOOGLE_URL: 'https://google.com',
};
const defaultConfig = {
  title: 'Yaml',
  urls: {
    HOMEPAGE_URL: 'https://udf.su',
    GITHUB_URL: 'https://github.com',
    GITLAB_URL: 'https://gitlab.com',
    GOOGLE_URL: 'https://google.com',
  },
};
const newUrls = {
  urls: { YAML_URL: 'https://yaml.org' },
};

const result = {
  variables,
  urls: defaultUrls,
  defaultConfig,
  newUrls,
  mergedConfig: {
    ...defaultConfig,
    // вот здесь мы полностью перезапишем поле `urls`
    ...newUrls,
    description: 'Yaml merge example',
  },
};

Как я уже говорил, объединять массивы через алиасы в yaml нельзя, однако некоторые приложения/библиотеки могут поддерживать дополнительные спецификаторы !flatten или, в случае с Gitlab CI, он автоматически выпрямляет (flatten) массивы в before_script, script и after_script, то есть там можно просто делать вот так:

cmds: &cmds
  - echo one
  - echo two

before_script:
  - *cmds
  - echo three

В общем, если вам понадобится мерджить массивы — изучайте документации библиотек/сервисов, которыми вы пользуетесь.

Работа со строками 80 уровня

Отдельным пунктом хочу остановиться на строках в yaml. Вообще этот раздел вполне мог бы быть отдельным постом, но все же хочу затронуть эту тему здесь. Если вы уже устали от чтения, то можно полностью пропустить этот раздел, либо посмотреть интерактивный сайтик, ссылка на который размещена в конце раздела. Итак, в yaml есть два стиля написания строк: “Flow scalars” и “Block scalars”.

Flow scalars (полагаю, что это можно перевести как строчные скаляры) — это по сути то, что мы уже использовали в прошлых примерах. Этот стиль немного более ограниченный по сравнению с блочным. Всего у строчных скаляров есть три немного различающихся синтаксиса:

  • Одинарные кавычки ('hello world') — в этом режиме не работает экранирование (escaping), например \n в результате так и останется \n. Чтобы экранировать кавычку, нужно ее продублировать '' конвертируется в '. По аналогии с markdown пустая строка будет конвертирована в перенос строки (\n)
  • Двойные кавычки ("hello world") — в этом стиле работает экранирование: \"", \\n\n, \n[перенос строки], \ в конце строки отключает перенос строки. Чтобы вставить перенос строки, кроме символа \n можно точно так же как и в предыдущем, стиле оставлять пустую строку.
  • Без кавычек — этот стиль действует по тем же правилам, что и использование кавычек за исключением того, что теперь можно использовать любые кавычки в тексте без необходимости их экранировать. Кроме этого, стиль без кавычек немного избирательный к использованию : и # в тексте. Символ : не может идти перед переносом строки или пробелом, а # после переноса строки или пробела. Если нарушить эти правила, то парсер yaml воспримет эти символы как попытку объявить объект или написать комментарий, что приведет к синтаксической ошибке.

Block scalars (блочные скаляры) предполагают использование специальных операторов предназначенных для работы со строками: | (literal style) и > (folded style), которые могут использоваться с модификаторами + и -. Оба оператора предназначены для написания текстов с переносами строки.

При использовании > все переносы строки заменяются на пробелы. Чтобы вставить перенос строки, необходимо оставить пустую строку. Альтернативно новую строку можно создать если поставить дополнительный пробел в начале строки. Этот оператор полезный, если, например, у вас есть длинная однострочная команда и вы хотите разбить ее на несколько строк для удобства чтения.

При использовании | все переносы строк остаются как есть.

С помощью +/- (chomping indicator) можно регулировать как обрабатывается окончание строки. По умолчанию режим clip (когда +/- опущены), который оставляет один перенос строки в конце строки. При использовании + keep, yaml оставит все переносы строк в точности так, как вы написали в коде. При использовании - strip будут обрезаны все переносы строки в конце строки.

Кроме того, при использовании блочных скаляров можно указать сколько пробелов отступа вы используете (например |2, |+2), чтобы явно задать начало “печатаемой” части строки. Если это число не указывать, то отступы определяются по отступу первой строки. Честно говоря, не могу представить в каких случаях пригодится эта настройка. Разве что, если вам понадобится забросить в строку кусок кода со строгими правилами индентации, при чем индентация должна начинаться прямо с первой строки.

В качестве примера предлагаю curl запрос написанный в разных стилях:

flow:
  # а сейчас я отключу prettier, иначе он свернет мой пример в строку :D
  # prettier-ignore
  singleQuote: 'curl ''https://www.google.com/search?q=yaml''
    -H "user-agent: Hello world"
    -H "accept: text/html"
    -H "referer: https://www.google.com/"'
  # prettier-ignore
  doubleQuote: "curl 'https://www.google.com/search?q=yaml'
    -H 'user-agent: Hello world'
    -H 'accept: text/html'
    -H 'referer: https://www.google.com/'"
  # Пришлось убрать пробелы после `:`. Иначе будет синтаксическая ошибка
  # prettier-ignore
  noQuotes: curl 'https://www.google.com/search?q=yaml'
    -H 'user-agent:Hello world'
    -H 'accept:text/html'
    -H 'referer:https://www.google.com/'
block:
  # Использование `-` не критично, bash и так скушает,
  # но почему бы не убрать не нужные переносы строки?
  #
  # Обратите внимание на отступы перед `-H`, если мы не выравняем отступы с `curl`
  # то получим перевод строки согласно правил для folded блоков
  # prettier-ignore
  folded: >-
    curl 'https://www.google.com/search?q=yaml'
    -H 'user-agent: Hello world'
    -H 'accept: text/html'
    -H 'referer: https://www.google.com/'
  # Здесь нам придется экранировать переносы строки
  # Так же как мы бы это делали в командной строке
  block: |-
    curl 'https://www.google.com/search?q=yaml' \
      -H 'user-agent: Hello world' \
      -H 'accept: text/html' \
      -H 'referer: https://www.google.com/'

И вот такой у нас будет результат:

{
  "flow": {
    "singleQuote": "curl 'https://www.google.com/search?q=yaml' -H \"user-agent: Hello world\" -H \"accept: text/html\" -H \"referer: https://www.google.com/\"",
    "doubleQuote": "curl 'https://www.google.com/search?q=yaml' -H 'user-agent: Hello world' -H 'accept: text/html' -H 'referer: https://www.google.com/'",
    "noQuotes": "curl 'https://www.google.com/search?q=yaml' -H 'user-agent:Hello world' -H 'accept:text/html' -H 'referer:https://www.google.com/'"
  },
  "block": {
    "folded": "curl 'https://www.google.com/search?q=yaml' -H 'user-agent: Hello world' -H 'accept: text/html' -H 'referer: https://www.google.com/'",
    "block": "curl 'https://www.google.com/search?q=yaml' \\\n  -H 'user-agent: Hello world' \\\n  -H 'accept: text/html' \\\n  -H 'referer: https://www.google.com/'"
  }
}

Итог: если вам нужно записать что-то в несколько строк, то лучше использовать | и >, так как они дают более контролируемый результат и не создают конфликтов с кавычками и прочими служебными символами.

Дополнительную информацию по части строк можно посмотреть на интерактивном сайте посвященном этой теме.

Онлайн конвертер yaml to json

И напоследок, если у вас возникают сложности с пониманием структуры yaml, можно воспользоваться любым онлайн ресурсом для конвертации yaml в более привычный json. Например вот этим