Хотели бы вы анализировать ndjson в терминале вот таким образом?
where { bool("muted") } max { long("size") } top 3
{"id":4880123,"size":245,"muted":true}
{"id":2392636,"size":107,"muted":true}
{"id":15843320,"size":59,"muted":true}
В работе мы нередко используем выгрузки в формате ndjson. Однако их анализ затруднен из-за того что стандартным инструментом для этого является jq с его чудовищным синтаксисом порождающим такое:
cat file.ndjson | jq '.tickets | map(select(.assigned_displayname=="MyNameOnApi"))' | jq '.[] | "\(.id) \(.title) \t \(.description)\n"' | sed #some after jq prettifying
Так как мне это не нравится, решил сделать современный json-процессор с удобочитаемым DSL. Что уже умеет этот инструмент?
Прежде всего, в наличии есть пять sql-подобных управляющих конструкций для обработки:
where { /* условие фильтрации */ }
order { /* выражение для задания порядка */ }
min { /* выражение для поиска минимальных значений */ }
max { /* выражение для поиска максимальных значений */ }
top ( /* число, лимит для выборки */ )
Все они могут быть использованы как по одиночке, так и скомбинированы друг с другом в любом порядке и с любым количеством повторов. Внутри используется обычный Kotlin, поэтому в выражениях вы также ничем не ограничены. Управляющие конструкции скомбинированные вкупе с выражениями образуют запрос который немедленно вернет вам набор json'ок. Сами запросы также можно комбинировать друг с другом, тогда они будут обработаны за один проход и вы получите несколько выборок в результате.
Примеры запросов
Постарался подчеркнуть читаемость:
max { long("size") } top 3 where { bool("active") }
top(5) where { !bool("active") } min { int("some") }
top(5) min { time("first") to time("last") } // 5 минимальных промежутков
where { get("arr")int(0) > 5 }
where { !get("broken") } top 3 min { get(4)get("nested")bool("flag") }
Если вы присмотрелись к этому примеру внимательнее, то заметили что в самих выражениях тоже используется лаконичный синтаксис доступа к json полям. Это стало возможно благодаря определенным хелперам выражений, которые являются ничем иным как расширениями для jackson:
// уже знакомые по jackson методы, возвращают ноду:
get(name: String)
get(idx: Int)
// о назначении дополнительных расширений комментариев не требуется:
bool(name: String)
bool(idx: Int)
int(name: String)
int(idx: Int)
double(name: String)
double(idx: Int)
string(name: String)
string(idx: Int)
time(name: String)
time(idx: Int)
Где скачать?
Из зависимостей вам потребуется лишь Docker. Установка не требуется. Просто запустите контейтер с утилитой, указав вашу директорию и файл в ней. Например, для директории ~/Desktop
и файла example.ndjson
, запуск выглядит следующим образом:
docker run -v ~/Desktop:/opt -it demidko/analyze example.ndjson
Вот и все! Мы попали в шелл для json и можем вводить любые запросы на ваш вкус.
Пара слов о внутренней реализации
Глядя на этот синтаксис можно было подумать что внутри творится ад dsl-строения, но на самом деле все гораздо проще. Запросы являются валидным подмножеством Kotlin и используют определенные прямо в исходном коде Kotlin функции. Запускается эта радость через известный jsr223 на котором подробно останавливаться нет смысла.
А вот про реализацию самих запросов стоит рассказать, остановлюсь на примере из кода:
typealias Query = (MutableList<JsonNode>, JsonNode) -> Unit
class Action(val action: MutableList<JsonNode>.(JsonNode) -> Unit) :
Query by { list, el ->
list.action(el)
}
infix fun Action.top(limit: Int) = Action {
action(it)
while (size > limit) {
removeLast()
}
}
fun order(comparator: Comparator<JsonNode>) = Action {
add(it)
sortWith(comparator)
}
infix fun Action.where(filter: JsonNode.() -> Boolean) = Action {
if (it.filter()) {
action(it)
}
}
Возможно вы удивитесь, но эти несколько функций и образуют ядро синтаксиса благодаря инфиксной нотации, а все остальное по сути синтаксические дополнения.
Всем удачного дня! Буду рад услышать критику и предложения.
MentalBlood
Вот тут
не совсем понятно, что значит int (0) > 5. Нашел только
а здесь, похоже, число. Кстати было бы совсем хорошо, если бы язык был описан (пусть только в рамках статьи) через формальную грамматику, с правилами вывода (тем более, что язык не очень сложный). Тогда бы и вопросов таких не возникало
Reformat Автор
Этот код возвращает вложенный объект «arr»:
Так как это тоже нода, для нее применяются все те же хелперы, например, получили нулевой элемент-число объекта «arr» и проверили что он больше пяти:
Однако Kotlin позволяет опционально опускать точку и получить ровно тоже самое:
Насчет того что в документации idx должно быть числом, да, вы правы, уже исправил.
Формальная грамматика Kotlin доступна здесь: kotlinlang.org/docs/reference/grammar.html