Перейти к основному содержимому

Как написать свой пакет данных

Самый простой пакет данных — это обычный файл, по одному значению на строку, доступный по своему адресу (см. Обзор). Дальше шапка открывает взвешенные списки, внешние файлы и небольшие генераторы — и всё это без единой строчки кода в движке, и всё это безопасно передавать другим, потому что пакет — это только данные или разобранный DSL в песочнице.

Выводы в примерах ниже — иллюстративные: точные значения зависят от сида и могут поменяться между версиями ядра. Что гарантировано — детерминизм при одном сиде и точные доли — отдельно отмечено там, где это важно.

Шапка

Поля ставятся между двумя строками --- в начале файла. Все необязательные:

ПолеЗначение
descriptionЧеловеческое описание — «что это»
addressЯвный адрес, переопределяет вычисленный из пути
localeЯзык (en, es, ru…) — и сегмент локали, которого нет у плоского пути
fileСсылка на внешний файл с данными вместо тела
columnС file: взять именованную/номерную колонку из чужого CSV
delimiterС file CSV или взвешенным телом: разделитель (по умолчанию ,)
weightС file: колонка с частотой, делающая пакет взвешенным
weightedtrue — тело в форме строк значение,вес
generatortdc — тело это <gen>, а не список
injectСвой маркер интерполяции для генератора

Каждое из полей разобрано ниже с конфигом и его выводом.

Взвешенные пакеты — частота из данных

Обычный пакет берётся равномерно: Smith выпадает так же часто, как Zabrowski. В жизни не так — по переписи фамилию Smith носят больше 2,4 миллиона американцев. Взвешенный пакет это учитывает, и делает это точно, раскладывая частоты методом Гамильтона (наибольшего остатка) — та же гарантия, что у percent. Веса можно задать двумя способами.

Тело прямо в пакете — weighted: true

Поставьте weighted: true и пишите каждую строку в форме значение,вес:

---
description: US surnames, weighted (2010 Census)
weighted: true
---
Smith,2442977
Johnson,1932812
Williams,1625252

Обращаются к нему как к любому пакету — в конфиге ничего не меняется:

<sequence name="Last">
<gen type="template" value="person.lastName"/>
</sequence>

На 100 000 строк каждая фамилия встретится пропорционально своему счётчику — Smith около 2000 раз, те самые 2,02%, что он занимает среди тех тысячи:

./run surnames.tdc (100 000 строк)
Smith      2022
Johnson    1599
Williams   1345

Эта форма удобна, когда список короткий и веса хочется держать прямо рядом со значениями.

Внешний CSV — file: + weight:

Когда список большой и лежит отдельно, сошлитесь на него через file: и назовите колонку с частотой в weight: (а колонку со значениями в column:):

---
description: US surnames, weighted (2010 Census)
file: ../../../sources/us/person/lastName.csv
column: name
weight: count
---

Вызов в конфиге тот же самый — изменение шапки для конфига невидимо:

<gen type="template" value="person.lastName"/>

Эта форма удобна, чтобы сослаться на большой CSV переписи или каталога, не копируя его в сам пакет.

Доли точны в обоих движках — и в потоковом (по умолчанию), и в mode="memory". Вес — это целое неотрицательное число (сырой счётчик, а не процент); пустая ячейка веса (Smith,) — это ошибка, а не тихий ноль; осознанный 0 исключает значение.

Значения с запятыми внутри — delimiter:

Если ваши значения — это фразы, в которых сами есть запятые (уведомления, предложения), запятая-разделитель их порвёт. Задайте delimiter: — любой символ или алиас (tab, semicolon, pipe):

---
weighted: true
delimiter: @
---
Ваш заказ, готовый к выдаче, отправлен@100
Новое сообщение, помеченное срочным@50

Разрез идёт по последнему разделителю в строке, так что запятые внутри значения остаются целыми:

./run notices.tdc (30 строк)
Ваш заказ, готовый к выдаче, отправлен
Новое сообщение, помеченное срочным
Ваш заказ, готовый к выдаче, отправлен

Тот же delimiter: задаёт и разделитель колонок для внешнего file: CSV.

Генераторы в пакете — generator: tdc

Пакет может вернуть генератор вместо списка — тогда адрес выдаёт вычисленное значение. Пишется он на собственном DSL TDC, так что учить ничего нового не надо. Поставьте в шапке generator: tdc; тело — это <gen>. Вот российский автомобильный номер — буква, три цифры, две буквы:

---
description: Российский автомобильный номер
address: russia.vehicle.plate
generator: tdc
---
<gen type="regex" value="[АВЕКМНОРСТУХ][0-9]{3}[АВЕКМНОРСТУХ]{2}"/>

Обращайтесь к нему ровно как к шаблону из списка:

<gen type="template" value="russia.vehicle.plate"/>
./run plates.tdc
Т027ВМ
М829СА
К415РО

Генератор исполняется тем же движком, что и ваш конфиг, поэтому все гарантии (детерминизм, переносимость на будущие рантаймы Python/Java) сохраняются, и он безопасен даже если скачан от кого-то — это разобранный ограниченный DSL без доступа к системе. Файлы-генераторы принято называть по расширению .tdc (данные остаются .txt), чтобы отличать их с одного взгляда; имя файла — это последний сегмент адреса (plate.tdc…plate).

Генератор, собирающий из данных

Генератор может дёргать соседние списки данных по адресу и собирать из них значение. Тело — это составная последовательность (вытяжки из данных, названные через точку) плюс один <data>, который говорит, что вернуть.

Под локалью по умолчанию en имена разрешаются в английские. В этом примере намеренно задана locale: es, чтобы показать соглашение об именовании, которого нет в английском — испанское полное имя из двух личных имён и двух фамилий:

---
description: Испанское полное мужское имя
generator: tdc
locale: es
---
<sequence name="p">
<distinct>
<gen name="f1" type="template" value="es.person.male.firstName"/>
<gen name="f2" type="template" value="es.person.male.firstName"/>
</distinct>
<distinct>
<gen name="l1" type="template" value="es.person.lastName"/>
<gen name="l2" type="template" value="es.person.lastName"/>
</distinct>
</sequence>
<data>${{p.f1}} ${{p.f2}} ${{p.l1}} ${{p.l2}}</data>
./run es-fullname.tdc
Antonio Javier García Fernández
Miguel Rodrigo López Romero
Carlos Alejandro Martín Ruiz

Данные (firstName, lastName) живут в своих файлах; генератор их только собирает. Тег <distinct> говорит, что две вытяжки из одного списка должны различаться в пределах строки — иначе две независимые вытяжки могли бы столкнуться в Juan Juan.

Точные проценты внутри генератора — <mix> + percent

<mix> с percent работает внутри генератора, и разбивка точна по числу строк. Например, у 60% людей — две фамилии, у 40% — одна:

---
description: Испанская фамилия — 60% двойных, 40% одинарных
address: es.person.surname
generator: tdc
---
<mix name="s" percent="60,40">
<case>
<gen type="template" value="es.person.lastName"/>
<data> </data>
<gen type="template" value="es.person.lastName"/>
</case>
<case>
<gen type="template" value="es.person.lastName"/>
</case>
</mix>
<data>${{s}}</data>
./run es-surname.tdc (100 строк)
García Fernández
López
Martín Romero
Ruiz

На 100 строк ровно 60 строк несут две фамилии и 40 — одну: доля раскладывается методом Гамильтона по всему count, а не случайно.

Про движок. Такая доля — это квота на весь столбец, а её ни один потоковый движок не умеет раздать по одной строке за раз, поэтому конфиг с таким пакетом считает in-memory движок, и память растёт вместе с count. Пакет без percent= не стоит ничего. См. Какой движок считает ваш конфиг.

Внутри <case> собирайте значение из самих тегов (<gen> и <data> для текста между ними), а не через ${{…}} — интерполяция чужих полей внутри кейса пока не поддерживается.

Свой маркер интерполяции — inject:

Если вывод генератора должен содержать буквальный ${{ }} (вы генерируете GitHub Actions, Handlebars или Go-шаблоны), задайте свой маркер через inject: (ровно один % отмечает место для имени), чтобы подстановка TDC не столкнулась с вашим текстом:

---
address: common.ci.deploy_step
generator: tdc
inject: <<%>>
---
<sequence name="s"><gen name="env" type="text" value="prod,staging"/></sequence>
<data> - run: deploy.sh --token ${{ secrets.TOKEN }} --env <<s.env>></data>

Здесь <<s.env>> — это подстановка TDC, а ${{ secrets.TOKEN }} уходит в вывод нетронутым. Получившаяся строка (показана как блок кода, потому что буквально содержит маркер ${{ }}):

- run: deploy.sh --token ${{ secrets.TOKEN }} --env prod
- run: deploy.sh --token ${{ secrets.TOKEN }} --env staging

Маркер изолирован — он не зависит от inject основного конфига, так что один и тот же генератор ведёт себя одинаково, куда бы его ни подключили. Без inject: по умолчанию остаётся ${{%}}.

Генератор, который вызывает другой генератор

Генератор может ссылаться на другой генератор, не только на список — «полное имя» может опираться на генератор «фамилия», который сам решает: одинарная она или двойная. TDC при загрузке проверяет, что цикла нет (A → B → A или ссылка на самого себя), и падает с ошибкой generator reference cycle: … ещё до генерации, а не уходит в бесконечную рекурсию.

Что разрешено внутри генератора

Восемь типов генераторов выдают значение сами по себе и разрешены в теле пакета где угодно: text, number, regex, advanced_regex, symbol, date, increment и decrement. Внутри <sequence> можно ещё использовать template, чтобы подтянуть список-данные или другой генератор по адресу, и распределение <mix> / percent.

Всё остальное отклоняется поимённоfile разрешал бы путь относительно неизвестно чего, а http спрятал бы сетевой вызов за адресом, который выглядит как список слов:

generator uses <gen type="http"> which is not allowed inside a pack generator

uniq= и order= тоже отклоняются, где бы в пакете они ни стояли. Оба описывают всю колонку — какие значения могут повторяться между строками и в каком порядке они выходят, — а у пакета просят одно значение на строку, так что ни счётчика строк, ни соседних строк у него нет. Объявляйте их на последовательности в том конфиге, который берёт из пакета. <distinct> — другое дело и остаётся разрешённым: он разводит поля внутри одной строки, а это пакет решить может.

Сложные корреляции между полями — это дело конфига, а не генератора в пакете.

<distinct> — без повторов в одной строке

Две независимые вытяжки из одного списка иногда совпадают (Пётр Пётр). Оберните поля (или целые последовательности), которые должны различаться в пределах строки, в <distinct>:

<sequence name="pair">
<distinct>
<gen name="a" type="template" value="ru.person.male.firstName"/>
<gen name="b" type="template" value="ru.person.male.firstName"/>
</distinct>
</sequence>
./run pair.tdc
Пётр и Дмитрий
Владимир и Андрей
Михаил и Олег

Пётр и Дмитрий — нормально; Пётр и Пётр не появится никогда. При совпадении движок перетягивает одно из значений, и детерминизм при одном сиде сохраняется. Работает и внутри <sequence> (оборачивая <gen>), и внутри <env> (оборачивая целые последовательности).

Не путайте с uniq: uniq не даёт всей строке целиком повторяться по всему датасету (по вертикали); <distinct> не даёт полям внутри одной строки совпасть (по горизонтали). Оба реализованы и независимы.

Где лежат свои пакеты данных

  • Встроенный набор поставляется в репозитории в data/packs/ и сканируется автоматически при запуске.
  • Свои папки добавляются флагом CLI --data-path <папка> (повторяемым) или параметром библиотеки dataPaths — см. Установка пакетов данных.

Ошибки и игнорируемые файлы

  • Два файла претендуют на один адресTDC170 с указанием обоих файлов. Переименуйте или переместите один.
  • Файл, не получивший адреса — есть шапка, но нет ни address:, ни locale:, и первый сегмент пути — не локаль, не страна и не commonTDC171, предупреждение с именем файла. Файл пропускается, поэтому value= на него позже упадёт с TDC071.
  • Опечатка в адресе в конфиге (value="person.lastNam") → TDC071 "unknown template path", ещё до генерации.
  • Скрытые файлы (начинающиеся с .), а также README / LICENSE / CHANGELOG сканером игнорируются.

Что пока не сделано

  • Автодополнение адресов в редакторе (по описаниям из шапок) — следующий шаг.
  • Манифест пакета на всю папку (лицензия, автор, версия) — в планах.

Смотрите также