Pull to refresh

Comments 27

Выглядит симпатично.

Один парсер, один источник правды, и тест-сьют, который доказывает, что все биндинги согласны.

А что, если, вместо биндингов через FFI сделать транслятор в JSON. А там пусть родные библиотеки парсят. Тестами покрыть все краевые случаи, что бы убедиться, что транслятор правильно работает. В итоге получаем не прям новый 15ый стандарт, а просто упрощённый способ описания json.

Спасибо, приятно слышать!

По сути это уже встроено - только как метод тестирования, не как продукт. Тест-спека - это пары “вход на Ktav → ожидаемый JSON”: https://github.com/ktav-lang/spec/tree/main/versions/0.6/tests/valid Берёте любой .ktav - рядом его .json-эквивалент. Транслятор, покрытый тестами на все краевые случаи, - это и есть наш эталонный Rust-парсер.

Почему всё же выбрал FFI в 7 языков, а не “отдай JSON нативному парсеру” - довод один, про скорость. Цель была - парсинг вровень с serde_json; по факту медленнее раза в два, но это наносекунды, я считаю результат хорошим. А вот лишний проход через JSON-текст (сериализовать → распарсить второй раз) - уже отдельная стоимость поверх, которая на горячем пути (частые перечитывания, много мелких конфигов) будет заметна. На конфиге, который читается раз при старте, - без разницы.

И обычно: “транслятор + родной парсер” - ровно то, что делают на старте нового формата, и это хорошая стратегия при ограниченности ресурсов. Я сделал наоборот - вложился в 7 FFI-биндингов за полтора месяца, до первого внешнего пользователя. Риск, чего уж там.

Лёгкий транслятор в чистый JSON отдельно от биндингов - хорошая идея, завёл issue: https://github.com/ktav-lang/spec/issues/2. В следующих версиях рассмотрю подробнее как с сней быть.

И да: без всего, что Ktav даёт сверх модели JSON, формулировка была бы именно такой - не 15-й стандарт, а просто приятный способ печатать JSON руками)

Вообще непонятно в чем отличие от JSON, точнее JSONC. Было бы неплохо увидеть таблицу сравнения хотя бы с JSON и проблем разного рода. Например надо вставить в конфиг дефолтный приватный ключ для тестового окружения, в JSON он выглядит так, в ktav он выглядит так.
Или например, ktav для решения этой проблемы использует include с файлом с приватным ключом. Или проблема с установкой log_level при ручном запуске, и что-то типа такого в конфиге ENV('LOG_LEVEL', 'info'), тоесть при ручном запуске просто устанавливаем LOG_LEVEL в переменных окружения, если ничего не выставлено то дефолт info.

Спасибо, что заглянули! Видимо в статье разница с JSONC не бросилась в глаза. Есть сравнительаня таблица на сайте - ktav-lang.github.io (в комментарий пока не разобрался как вставлять таблицы), опишу словами:

JSONC - это JSON плюс комментарии, и всё, остальное как в обычном JSON: кавычки на ключах и строках обязательны, запятые обязательны.

В Ktav иначе: кавычки на ключах и строках не нужны вообще (кроме ::, когда явно хочешь застолбить строку), запятые нужны только если пишешь массив или объект в одну строку, комментарии свои - ##, а для многострочного текста есть отдельный блок в скобках вместо \n внутри одной строки в кавычках.

На примере с приватным ключом. В JSON/JSONC он склеен в одну строку через \n:

{
      // тестовый ключ, не для прода
      "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKC...\n-----END RSA PRIVATE KEY-----"
}

В Ktav - отдельный блок, построчно как есть:

## тестовый ключ, не для прода
private_key: (
   -----BEGIN RSA PRIVATE KEY-----
   MIIEowIBAAKC...
   -----END RSA PRIVATE KEY-----
)

Include с файлом секрета и ENV(‘LOG_LEVEL’, ‘info’) внутри конфига - сознательно не входит в формат. Ktav - только слой данных, без include/expressions/interpolation, как и JSON с TOML: добавишь интерполяцию - формат превращается в мини-язык со своей семантикой выполнения.

Стремился к минимализму - “Ktav простой и хороший, он не совершенный, но лучший для конфигов”. Получилось сделать его таким или нет - покажет время.

Возможно параллельно будет какой-нибудь ktav_plus - со ссылками, переменными и прочим, но основной формат останется в минимализме, как json

Попробуйте как-то создать конфигурацию эдак на 3-4 тыс ключей (а лучше больше), и для 3-х окружений: dev, prod, test, причем для dev/test должны быть какие-то дефолтные креды для внешних сервисов и настройки (dev и test должны тоже отличаться, dev это локальная разработка, test это какой-то поднятый сервер для qa), а для prod такие креды которые не должны попасть в git. И сразу будет понятно все недостатки и ограничения Вашего решения. Пока из приятного только многострочные значения, кавычки и запятые вкусовщин. Да можно сказать что этого всего нету и в JSON/YAML/TOML и include/expressions/interpolation и логику merge надо будет городить руками, но при этом всем среди простых форматов у JSON явное преимущество из-за наличия схем.

Спасибо за подробный контр-аргумент - это полезнее общей критики, потому что чётко очерчивает границу применимости.

И да, вы правы: на 3-4 тысячах ключей с тремя окружениями и разделением секретов Ktav вам ничего не даст сверх того, что дают JSON/YAML/TOML. Формат этого не решает и не пытается. Он про другое - про конфиг, который человек пишет и читает руками: сервис, CLI, небольшое приложение. Там, где конфигурация становится системой со своим наследованием, оверлеями и секрет-менеджментом, нужен не формат, а инструмент: Helm/Kustomize, CUE, Consul, Vault. Выбор формата на таком масштабе - вопрос десятый.

Про include / interpolation / merge - это сознательно вне формата, а не недоделка. Как только внутрь синтаксиса заходит include или подстановка переменных, файл перестаёт читаться сам по себе, и появляется порядок вычисления, который надо описывать и реализовывать одинаково во всех семи биндингах. Мы решили, что этот слой живёт в приложении: конфиг из файла, env поверх, секреты инжектятся отдельно. Скучно, зато предсказуемо.

Про схемы вы попали в реальный пробел - своего языка описания схем у Ktav нет, и это честное ограничение.

Но одно уточнение: модель данных у Ktav ровно JSON-овская - те же скаляры, массивы, объекты, null, bool, без тегов и якорей. Поэтому JSON Schema применима к результату разбора: распарсили .ktav в структуру и валидируйте тем же валидатором, что и для JSON. Отдельная схема под формат не нужна - преимущество JSON здесь наследуется, а не теряется.

Про вкусовщину спорить не буду. Кавычки и запятые действительно вопрос привычки, и если они вам не мешают - формат вам не нужен, это нормальный исход. Он вырос из личного раздражения, а не из уверенности, что так лучше всем, и 20 лет за клавитурой накопили разражение в бесконечных танцах пальцев с шифтами)

Опишу свою позицию в плане конфигурации. В конфигурации есть две крайности, или переменные окружения (.env, .env.production) или инструменты.
А между этими крайностями есть много форматов конфигурации и каждый имеет свои недостатки.

Как для меня выглядит нормальная конфигурация.

  • Нативная поддержка схемы данных и валидации, конвертировать yaml/toml/etc в json и потом валидировать не вариант, не видно ошибок при редактировании конфига;

  • Поддержка окружения, например dev/prod/test/e2e.

  • Вложение конфигов, например dev.xxx содержит в себе и сразу понятно что и в каком порядке загружаем

    include language.xxx
    include country.xxx
    include currency.xxx
  • Перекрытие значений или merge для окружений, например добавляем новый язык в общие настройки, и потом для prod окружения его выключаем, подменяя только нужное поле, что-то типа этого

    include language.xxx
    include country.xxx
    include currency.xxx
    
    language:
      ita:
        enable: false
    
  • Разделение конфигураций на открытую и закрытую части, открытая то что можно положить в git, для всех окружений, закрытая то что надо взять откуда-то (из определенного файла, из переменной окружения).

Очень ценные запросы, спасибо! И почти всё здесь я готов подписать.

Про валидацию соглашусь. В другом комментарии я предлагал ровно то, что вы отвергаете - взять JSON Schema и проверять ей уже разобранный конфиг. Но тогда проверка идёт по готовым данным, в которых уже не осталось привязки к строкам файла. Подсветить ошибку в редакторе нечем.

Про остальные четыре пункта скажу прямо: этого нет. Почему - мы этим просто не занимались и не планировали заниматься. Формат отвечает на вопрос, как выглядит файл, а окружения, include и merge - это уже про то, как конфигурация собирается из нескольких файлов. Другой слой.

JSON и TOML останавливаются там же. Вы сами это отмечаете - и ваш тезис именно в том, что между форматом и полноценным инструментом остаётся дыра. С этим спорить нечего, дыра есть.

Мне кажется правильным закрывать её не изменением формата, а отдельной необязательной спекой поверх него: include как соглашение над обычными Ktav-документами, семантика глубокого merge (включая удаление ключа, а не только замену), выбор окружения, соглашение о ссылках на внешние секреты. Тогда файл остаётся обычным Ktav для тех, кому обвязка не нужна.

Завёл под это issue, расписал ваши пять пунктов как отправную точку: https://github.com/ktav-lang/spec/issues/5

Там же вынес открытые вопросы: своя схема или привязка к JSON Schema, отдельный документ или репозиторий, и стоит ли браться до 1.0 самого формата.

В YAML есть что-то типа merge, через якоря, коряво, косо, но есть.

Не соглашусь с тем что все кроме формата данных надо кидать в обвязку.
Тем же merge хотелось бы управлять на уровне файла, что-то типа

include language.xxx
include country.xxx, merge_left: true
include currency.xxx, merge_right: true

Но с include проблема в другом, в редакторе не видно финального конфига, и собственно он не может быть провалидирован через схему. Тут только надежда на то что конфиг из валидных кусков будет валидным(и это работает с простыми типами и вариацией только типов). Но опять таки это все ломается, если у схемы и валидации есть условия, например одно поле зависит от другого (тут только валидация при старте поможет).

Поэтому и валидацию идеально было бы разделить на две части. Валидация через схему, только типов и чтобы было в редакторе. И валидация при старте с условиями и возможно кастомными валидаторами. Выдуманный пример, есть у нас в конфиге допустим адрес какого-то биткоин кошелька, с точки зрения данных это строка и ее вариация ничего не даст, но вот для prod/test кошельки должны быть разные и валидные, и для этого надо вызвать кастомный валидатор.

Это будет масштабной работой. Не уверен, что возмусь за это, но мне нравятся ваши слова. В любом случае, это будет либо ktavPlus - вторым форматом (как TS над JS), либо обвязкой.

Оригинальный ktav очень хочу сохранить в простом виде - мне он тоже нравится именно своей простотой.

Если вы готовы быть поставщиком идей (в случае, если возьмусь) - создайте issue (https://github.com/ktav-lang/spec/issues). Если в будущем возьмусь, то я отпишусь под ним.

Крутой мини продукт. Выглядит очень приятно. И тут, наверное, главный плюс. Вы не пытаетесь сказать, что это удобно и полезно везде и всегда.

Конкретная боль - кросс-языковые команды. В этом случае все рассуждения о том, что для того же Go решение наверное не самое идиоматичное, надо доставлять .so рядом, думать про musl/alpine в докере, про комбинации OS/arch и.т.д. это осознанная цена за консистентность.

Единственное, чего мне не хватило бы, доп. синтаксиса для явного указания типа. У вас уже есть :: для форс-строки, и напрашивается то же самое для остальных типов, условные ::int и ::float. Защита от дурака для критичных переменных, где типизация по форме может поехать, плюс ревью конфигов становится проще: тип виден глазами, а не выводится в голове.

Пробежался по репо и заметил одну деталь, тихую канонизацию числовых скаляров при выводе типов (1.10 -> 1.1, 01234 -> 1234). Я с подобным сталкивался в работе, поэтому решил помочь и подготовил issue и PR. Ознакомьтесь, может будет полезным.
Issue: https://github.com/ktav-lang/rust/issues/1
PR: https://github.com/ktav-lang/rust/pull/2

Спасибо - и отдельное спасибо за ПР, это редкий жанр комментария.

Про кросс-языковые команды верно, цена именно такая, как вы описали: .so рядом и прочее. Платим её сознательно, ради одинакового поведения везде и производительности Rust.

PR влит, релиз для Rust опубликован - 0.6.2: https://crates.io/crates/ktav

Теперь про ::int / ::float. Идея понятна и ложится на существующий синтаксис, но в 0.5.0 мы убрали ровно такие маркеры - были :i и :f.

Главная причина - типизированные языки всё равно сами решают, какого типа получать значение. Целевой тип задаёт структура, в которую десериализуют, а не документ: serde в Rust, struct в Go, класс в C# приведут port к u16 независимо от того, как парсер классифицировал скаляр. Маркер в конфиге дублировал то, что уже объявлено в коде, и добавлял вопрос, что делать, когда он противоречит форме.

Плюс ощущение, что для конфигов небольшого ПО текущих возможностей хватает - формат задумывался именно для удобных конфигураций, а не для передачи данных.

Отчасти ту же боль закрывает как раз ваш parse_strict, только с другой стороны: он не даёт форме молча поехать.

Благодраю за участие! Очень рад первому ПР.

Оказывается еще и баг в темной теме со списоком. Исправил. Спасибо!

По INI - не каждый конфиг может сконвертироваться в INI. Это не баг. Ниже на скрине показывается корректная ошибка об этом.

Посмотрите plist из Common Lisp или Clojure, где ключи - keywords.

Не совсем то, что у вас, но семантически близко.

Спасибо, мысль хорошая - keywords действительно про то же самое: ключ самоозначающий, кавычки ему не нужны.

Если продолжить её и поставить форматы рядом, то ближайшим соседом по внешнему виду окажется, наверное, всё-таки JSON5 - там тот же порядок “ключ двоеточие значение” и ключи тоже без кавычек:

{ host: 'a.example', port: 1080 }

В plist и EDN двоеточие переезжает влево и превращает сам ключ в keyword, а пары разделяются пробелами внутри s-выражения:

(:host "a.example" :port 1080)

Но любопытно, что plist и JSON5 при этом роднит ровно то же самое, что вы заметили: ключ не нуждается в кавычках. Похоже, до этой мысли рано или поздно доходит каждый, кто делает формат для чтения человеком, - просто доводят её по-разному.

Ktav дотянул её на шаг дальше и снял кавычки и со строковых значений тоже, а структуру оставил построчной, без объемлющих скобок:

host: a.example
port: 1080

Так что, да - “не совсем то, но семантически близко”)

Это корректная форма ktav, спарсится в объект, который эквивалентен JSON:

{
  "host": "https://example.com",
  "port": "*:1080"
}

Это можно в живую проверить на сайте ktav-lang.github.io

(на сложные случаи есть экранирование, но в малом ПО для конфигов это скорее всего не понадобится)

Заинтересовало и возник вопрос

У вас на сайте есть демо пример. Я немного изменил его

{
  "log_level": "info",
  "log_level1": "true",
  "log_level2": true,
  "debug": true
}

на выходе

log_level: info
log_level1:: true
log_level2: true
debug: true

log_level2 и debug оба boolean, но записались по разному

PS Все понял :: это наоброт переводить в строку

Почти) Только запятые были довеском - по-настоящему меня довели кавычки.

В JSON5 голым можно написать ключ, а значение всё равно в кавычках:

{ host: 'a.example', port: 1080 }

В Ktav кавычек нет ни в ключе, ни в значении, и корень не обёрнут в скобки - файл читается построчно:

host: a.example
port: 1080

Запятые ушли уже за компанию: раз пары и так разделены переносом строки, разделять их ещё и запятой стало не нужно.

Понравилось. Есть небольшое замечание: По песочнице ktav-lang.github.io бросается в глаза недоведенность по начальным и конечным пробелам строковых значений. ktav -> json обрабатывает невидимые пробелы как мне и хотелось: их игнорирует, а в ((…)) - сохраняет. Хотелось бы, чтобы json -> ktav начальные и конечные пробелы строковых значений превращал в соответствующую 3-строчную конструкцию ((…)), ну или придумайте для этого, хоть и редкого, но значимого случая специальные кавычки.

Спасибо - точное и ценное замечание, круто, что вы сразу нашли реальуню вещь, которая стоит доработки!

Это не пробел в спеке - она уже требует именно того, чего вам хочется. § 5.9.5(b) и § 5.9.7 прямо запрещают однострочную форму для строки с краевыми пробелами и обязывают многострочный блок ((…)) вместо неё. Отдельные кавычки не нужны - механизм для этого случая в формате уже есть, просто writer его не соблюдает.

Направление вы нащупали точно: ktav -> json у вас работает правильно, потому что там парсер, а он спеке следует. json -> ktav - это запись, и вот там баг: строка с пробелом по краям сейчас молча теряет байты вместо того, чтобы уйти в блок.

Этот же баг на этой неделе нашёл и прислал с PR @chappihappymeal - независимо от вас, через код.

Issue: https://github.com/ktav-lang/rust/issues/3

PR: https://github.com/ktav-lang/rust/pull/4

И раз тема всплыла - параллельно копится пачка для отдельного релиза 0.7. Там уже прилично набралось: и спеку, и все семь парсеров гонять волной ради одной мелкой правки слишком дорого, поэтому такие изменения собираем до критической массы

Спасибо!

Это не пробел в спеке - она уже требует именно того, чего вам хочется. § 5.9.5(b) и § 5.9.7 прямо запрещают однострочную форму для строки с краевыми пробелами и обязывают многострочный блок ((…)) вместо неё.

Направление вы нащупали точно: ktav -> json у вас работает правильно, потому что там парсер, а он спеке следует. json -> ktav - это запись, и вот там баг: строка с пробелом по краям сейчас молча теряет байты вместо того, чтобы уйти в блок. Проверил на самом остром случае - строка “\ttrue”: вместо блока сейчас пишется однострочное field1:: true, а при обратном чтении ведущий таб пропадает вообще, значение схлопывается в чистое “true”. То есть это не просто некрасивая форма записи, а реальная потеря байта.

Этот же класс бага @chappihappymeal нашёл и прислал с PR, независимо от вас:

Issue: https://github.com/ktav-lang/rust/issues/3

PR: https://github.com/ktav-lang/rust/pull/4

Я прогнал ваш случай на ветке этого PR отдельно - чинит и его: та же строка с ведущим табом уходит в ((…)) и возвращается обратно байт в байт. Фикс идёт в ближайший патч-релиз 0.6.3, ничего не ломает - плейграунд подтянет его следующим обновлением вендоренного WASM.

И раз тема всплыла - параллельно копится пачка для отдельного релиза 0.7. Там уже прилично набралось: и спеку, и все семь парсеров гонять волной ради одной мелкой правки слишком дорого, поэтому такие изменения собираем до критической массы, а не выкатываем по одному.

Почему в первом случае одинарные скобочки? В целом, понятно, что можно в обеих формах записывать любой текст, просто интересно, как конвертер выбирает способ.

Одинарные скобки делают красивый триминг отступов когда можно сохранить целостность данных (в обратном конвертировании они значат, что отступы не нужны). Двойные - сохраняют строку как есть. Такой была цель.

Sign up to leave a comment.

Articles