Зачем писать собственный DSL?
Привет народ. Хочу поделиться с вами своим первым опытом написания DSL решения в Kotlin. В частности, для построения / генерации объектов JSON без какой-либо конкретной структуры. Таким образом, его можно использовать для создания любого типа JSON в целом.
Поводом для этого была потребность в механизме в коде моего Android-приложения для передачи объекта JSON в API SDK без использования длинных строк (в нескольких строках кода), объявления классов данных с определенной структурой, требуемой API, или построения добавить объект JSON с уже существующими библиотеками JSON, такими как org.json и т. д.
Идея
Поэтому я начал его реализовывать, сначала взглянув на официальную документацию Kotlin по написанию DSL. Цель состояла в том, чтобы использовать функциональные литералы с приемником Kotlin и таким образом создать желаемые объекты JSON путем вызова связанных функций. Это должно было выглядеть примерно так:
val jsonString = json { // build up the JSON by calling the related DSL functions }
В моем путешествии по написанию DSL для JSON я понял, что мне нужно использовать функции с параметрами, чтобы указать ключ и значения для объекта JSON, потому что невозможно объявлять функции во время выполнения и в то же время иметь тип безопасность во время компиляции. Поэтому мне пришлось пойти на компромисс и выбрать такую структуру DSL:
val jsonString = json { objectElement("key1") { stringElement("key2", "value2") } booleanElement("key3", true) numberElement("key4", 1234.5678) arrayElement("key5") { stringElement("arrayElement1") // ... } }
В приведенном выше примере кода создание объекта JSON выполняется путем вызова функций DSL objectElement()
, stringElement()
, numberElement()
, booleanElement()
и arrayElement()
. Сами функции objectElement()
и arrayElement()
могут принимать в свои замыкания соответственно дополнительные пары ключ-значение или отдельные элементы, как предлагает спецификация JSON.
Мысли и шаги по реализации
Прежде всего, нам нужно реализовать корневую функцию для DSL, json()
, которая должна возвращать объект JSON в виде строки (без межстрочного интервала или отступа). Кроме того, функции требуется лямбда в качестве параметра, который должен получать экземпляр JsonObjectElement
- реализацию для элемента объекта JSON, который будет отвечать за хранение дочерних элементов и их рендеринг в виде строк. Вот как выглядит функция json
:
fun json(body: JsonObjectElement.() -> Unit): String { val jsonObjectElement = JsonObjectElement() val stringBuilder = StringBuilder() jsonObjectElement.body() jsonObjectElement.render(stringBuilder) return stringBuilder.toString() }
Обратите внимание, что JsonObjectElement
имеет функцию получения объекта StringBuilder
, который должен использоваться дочерними элементами для построения дерева JSON. Вот первая идея того, как может выглядеть реализация класса JsonObjectElement
и метода render()
:
@DslMarker class JsonObjectElement { private val children = mutableMapOf<String, Any>() fun render(builder: StringBuilder) = builder .append("{") .append(children.entries.fold(StringBuilder(), { acc, entry -> acc.append(/* render the children */) .append("}") .toString() // ... }
Вы, вероятно, также поняли, что существует потребность в общем интерфейсе для всех типов элементов JSON (строковых, логических и т. Д.), Скажем, JsonElement
, который будет включать метод render()
. Карта children
будет содержать JsonElement
в качестве значений, и мы сможем завершить реализацию метода render()
и рендеринг дочерних элементов последовательно. Кстати, JsonObjectElement
теперь может также реализовать интерфейс JsonElement
:
interface JsonElement { fun render(builder: StringBuilder): String } @DslMarker class JsonObjectElement : JsonElement { private val children = mutableMapOf<String, JsonElement>() override fun render(builder: StringBuilder) = builder .append("{") .append(children.entries.foldIndexed(StringBuilder(), { index, acc, entry -> acc.apply { append("\"${entry.key}\":${entry.value.render(StringBuilder())}") if (index < children.size - 1) { // add commas between the elements append(",") } } }).toString()) .append("}") .toString() // ... }
Теперь мы можем сделать шаг вперед и реализовать методы для добавления дочерних элементов в дерево JSON:
@DslMarker class JsonObjectElement : JsonElement { // ... fun objectElement(name: String, body: JsonObjectElement.() -> Unit) { val jsonObjectElement = JsonObjectElement() jsonObjectElement.body() children[name] = jsonObjectElement } fun arrayElement(name: String, body: JsonArrayElement.() -> Unit) { val jsonArrayElement = JsonArrayElement() jsonArrayElement.body() children[name] = jsonArrayElement } fun stringElement(name: String, value: String) { children[name] = JsonStringElement(value) } fun booleanElement(name: String, value: Boolean) { children[name] = JsonBooleanElement(value) } fun numberElement(name: String, value: Number) { children[name] = JsonNumberElement(value) } fun nullElement(name: String) { children[name] = JsonNullElement }
Вы, наверное, обратили внимание на классы JsonArrayElement
, JsonStringElement
, JsonBooleanElement
, JsonNumberElement
и JsonNullElement
и догадались, что они также являются подклассами JsonElement
. Важно отметить, что методы objectElement()
и arrayElement()
имеют в качестве параметров лямбда-выражения, аналогично методу json()
, поскольку с их помощью мы хотим включить больше элементов в дерево. Другие методы - это «завершение», добавление только значений определенных типов и завершение дерева JSON в этой точке. Вот реализация «завершающих» классов значений:
@DslMarker class JsonStringElement(private val value: String) : JsonElement { override fun render(builder: StringBuilder) = builder.append("\"$value\"").toString() } @DslMarker class JsonBooleanElement(private val value: Boolean) : JsonElement { override fun render(builder: StringBuilder) = builder.append(value).toString() } @DslMarker class JsonNumberElement(private val value: Number) : JsonElement { override fun render(builder: StringBuilder) = builder.append(value).toString() } @DslMarker object JsonNullElement : JsonElement { override fun render(builder: StringBuilder) = builder.append("null").toString() }
Пока все довольно просто. Немного интереснее реализация реализации JsonArrayElement
, которая также очень похожа на JsonObjectElement
. Единственная разница практически состоит в том, что нам не нужны имена в качестве ключей к содержащим дочерние значения. Поэтому мы используем MutableList
для их хранения и не нуждаемся в параметрах для имен в методах добавления потомков:
@DslMarker class JsonArrayElement : JsonElement { private val children = mutableListOf<JsonElement>() override fun render(builder: StringBuilder) = builder .append("[") .append(children.foldIndexed(StringBuilder(), { index, acc, jsonElement -> acc.apply { append(jsonElement.render(StringBuilder())) if (index < children.size - 1) { append(",") } } })) .append("]") .toString() fun objectElement(body: JsonObjectElement.() -> Unit) { val jsonObjectElement = JsonObjectElement() jsonObjectElement.body() children.add(jsonObjectElement) } fun arrayElement(body: JsonArrayElement.() -> Unit) { val jsonArrayElement = JsonArrayElement() jsonArrayElement.body() children.add(jsonArrayElement) } fun stringElement(value: String) { children.add(JsonStringElement(value)) } fun booleanElement(value: Boolean) { children.add(JsonBooleanElement(value)) } fun numberElement(value: Number) { children.add(JsonNumberElement(value)) } fun nullElement() { children.add(JsonNullElement) } }
Кроме того, мы можем иметь специальный тип DslMarker
аннотации для нашего JSON DSL, который будет гарантировать контроль области видимости, и использовать его во всех уже реализованных объявлениях подкласса JsonElement
:
@DslMarker annotation class JsonElementMarker
Мы завершили базовую реализацию нашего Kotlin JSON DSL и готовы к использованию, как определено в начале статьи:
val jsonString = json { objectElement("key1") { stringElement("key2", "value2") } booleanElement("key3", true) numberElement("key4", 1234.5678) arrayElement("key5") { stringElement("arrayElement1") } } System.out.print(jsonString)
Приведенный выше код распечатает следующую строку JSON:
{"key1":{"key2:"value2"},"key3":true,"key4":1234.5678,"key5":["arrayElement1"]}
Полная реализация
Вот полная реализация:
import java.lang.StringBuilder interface JsonElement { fun render(builder: StringBuilder): String } @DslMarker annotation class JsonElementMarker @JsonElementMarker class JsonObjectElement : JsonElement { private val children = mutableMapOf<String, JsonElement>() override fun render(builder: StringBuilder) = builder .append("{") .append(children.entries.foldIndexed(StringBuilder(), { index, acc, entry -> acc.apply { val renderedChild = "\"${entry.key}\":${entry.value.render(StringBuilder())}" append(renderedChild) if (index < children.size - 1) { append(",") } } }).toString()) .append("}") .toString() fun objectElement(name: String, body: JsonObjectElement.() -> Unit) { val jsonObjectElement = JsonObjectElement() jsonObjectElement.body() children[name] = jsonObjectElement } fun arrayElement(name: String, body: JsonArrayElement.() -> Unit) { val jsonArrayElement = JsonArrayElement() jsonArrayElement.body() children[name] = jsonArrayElement } fun stringElement(name: String, value: String) { children[name] = JsonStringElement(value) } fun booleanElement(name: String, value: Boolean) { children[name] = JsonBooleanElement(value) } fun numberElement(name: String, value: Number) { children[name] = JsonNumberElement(value) } fun nullElement(name: String) { children[name] = JsonNullElement } } @JsonElementMarker class JsonArrayElement : JsonElement { private val children = mutableListOf<JsonElement>() override fun render(builder: StringBuilder) = builder .append("[") .append(children.foldIndexed(StringBuilder(), { index, acc, jsonElement -> acc.apply { append(jsonElement.render(StringBuilder())) if (index < children.size - 1) { append(",") } } })) .append("]") .toString() fun objectElement(body: JsonObjectElement.() -> Unit) { val jsonObjectElement = JsonObjectElement() jsonObjectElement.body() children.add(jsonObjectElement) } fun arrayElement(body: JsonArrayElement.() -> Unit) { val jsonArrayElement = JsonArrayElement() jsonArrayElement.body() children.add(jsonArrayElement) } fun stringElement(value: String) { children.add(JsonStringElement(value)) } fun booleanElement(value: Boolean) { children.add(JsonBooleanElement(value)) } fun numberElement(value: Number) { children.add(JsonNumberElement(value)) } fun nullElement() { children.add(JsonNullElement) } } @JsonElementMarker class JsonStringElement(private val value: String) : JsonElement { override fun render(builder: StringBuilder) = builder.append("\"$value\"").toString() } @JsonElementMarker class JsonBooleanElement(private val value: Boolean) : JsonElement { override fun render(builder: StringBuilder) = builder.append(value).toString() } @JsonElementMarker class JsonNumberElement(private val value: Number) : JsonElement { override fun render(builder: StringBuilder) = builder.append(value).toString() } @JsonElementMarker object JsonNullElement : JsonElement { override fun render(builder: StringBuilder) = builder.append("null").toString() } fun json(body: JsonObjectElement.() -> Unit): String { val jsonObjectElement = JsonObjectElement() val stringBuilder = StringBuilder() jsonObjectElement.body() jsonObjectElement.render(stringBuilder) return stringBuilder.toString() }
Резюме
Хотя вариант использования реализации DSL для JSON, вероятно, очень ограничен и существует множество альтернатив для работы с объектами JSON для Java и Kotlin, мне все же было интересно реализовать его как проект выходного дня, по крайней мере, в учебных целях. Надеюсь, вам понравилась статья, и даже вы сочли ее полезной и использованной в своем коде. Спасибо за чтение!