Как написать свой пакет данных
Самый простой пакет данных — это обычный файл, по одному значению на строку, доступный по своему адресу (см. Обзор). Дальше шапка открывает взвешенные списки, внешние файлы и небольшие генераторы — и всё это без единой строчки кода в движке, и всё это безопасно передавать другим, потому что пакет — это только данные или разобранный DSL в песочнице.
Выводы в примерах ниже — иллюстративные: точные значения зависят от сида и могут поменяться между версиями ядра. Что гарантировано — детерминизм при одном сиде и точные доли — отдельно отмечено там, где это важно.
Шапка
Поля ставятся между двумя строками --- в начале файла. Все необязательные:
| Поле | Значение |
|---|---|
description | Человеческое описание — «что это» |
address | Явный адрес, переопределяет вычисленный из пути |
locale | Язык (en, es, ru…) — и сегмент локали, которого нет у плоского пути |
file | Ссылка на внешний файл с данными вместо тела |
column | С file: взять именованную/номерную колонку из чужого CSV |
delimiter | С file CSV или взвешенным телом: разделитель (по умолчанию ,) |
weight | С file: колонка с частотой, делающая пакет взвешенным |
weighted | true — тело в форме строк значение,вес |
generator | tdc — тело это <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%, что он занимает среди тех тысячи:
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
Разрез идёт по последнему разделителю в строке, так что запятые внутри значения остаются целыми:
Ваш заказ, готовый к выдаче, отправлен Новое сообщение, помеченное срочным Ваш заказ, готовый к выдаче, отправлен
Тот же 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"/>
Т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>
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>
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>
Пётр и Дмитрий Владимир и Андрей Михаил и Олег
Пётр и Дмитрий — нормально; Пётр и Пётр не появится никогда. При совпадении движок
перетягивает одно из значений, и детерминизм при одном сиде сохраняется. Работает и
внутри <sequence> (оборачивая <gen>), и внутри
<env> (оборачивая целые последовательности).
Не путайте с uniq: uniq не даёт всей строке
целиком повторяться по всему датасету (по вертикали); <distinct> не даёт полям
внутри одной строки совпасть (по горизонтали). Оба реализованы и независимы.
Где лежат свои пакеты данных
- Встроенный набор поставляется в репозитории в
data/packs/и сканируется автоматически при запуске. - Свои папки добавляются флагом CLI
--data-path <папка>(повторяемым) или параметром библиотекиdataPaths— см. Установка пакетов данных.
Ошибки и игнорируемые файлы
- Два файла претендуют на один адрес →
TDC170с указанием обоих файлов. Переименуйте или переместите один. - Файл, не получивший адреса — есть шапка, но нет ни
address:, ниlocale:, и первый сегмент пути — не локаль, не страна и неcommon→TDC171, предупреждение с именем файла. Файл пропускается, поэтомуvalue=на него позже упадёт сTDC071. - Опечатка в адресе в конфиге (
value="person.lastNam") →TDC071"unknown template path", ещё до генерации. - Скрытые файлы (начинающиеся с
.), а такжеREADME/LICENSE/CHANGELOGсканером игнорируются.
Что пока не сделано
- Автодополнение адресов в редакторе (по описаниям из шапок) — следующий шаг.
- Манифест пакета на всю папку (лицензия, автор, версия) — в планах.
Смотрите также
- Обзор — адреса и использование пакетов данных.
- Установка пакетов данных —
tdcv2 initиtdcv2 pack. - Связные и реляционные данные — родитель → ребёнок по имени.
- Уникальные значения —
<distinct>иuniqподробно.