Kotlin DSL для создания объектов json (без создания мусора)

Я пытаюсь создать DSL для создания JSONObjects. Вот класс строителя и пример использования:

import org.json.JSONObject fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { val builder = JsonObjectBuilder() builder.build() return builder.json } class JsonObjectBuilder { val json = JSONObject() infix fun <T> String.To(value: T) { json.put(this, value) } } fun main(args: Array<String>) { val jsonObject = json { "name" To "ilkin" "age" To 37 "male" To true "contact" To json { "city" To "istanbul" "email" To "xxx@yyy.com" } } println(jsonObject) } 

Вывод вышеуказанного кода:

 {"contact":{"city":"istanbul","email":"xxx@yyy.com"},"name":"ilkin","age":37,"male":true} 

Он работает так, как ожидалось. Но он создает дополнительный экземпляр JsonObjectBuilder каждый раз, когда он создает объект json. Можно ли написать DSL для создания объектов json без лишнего мусора?

Вы можете использовать Deque как стек для отслеживания текущего контекста JSONObject с помощью одного JsonObjectBuilder :

 fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { return JsonObjectBuilder().json(build) } class JsonObjectBuilder { private val deque: Deque<JSONObject> = ArrayDeque() fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { deque.push(JSONObject()) this.build() return deque.pop() } infix fun <T> String.To(value: T) { deque.peek().put(this, value) } } fun main(args: Array<String>) { val jsonObject = json { "name" To "ilkin" "age" To 37 "male" To true "contact" To json { "city" To "istanbul" "email" To "xxx@yyy.com" } } println(jsonObject) } 

Пример вывода:

 {"contact":{"city":"istanbul","email":"xxx@yyy.com"},"name":"ilkin","age":37,"male":true} 

Вызов json и build нескольких потоков на одном JsonObjectBuilder будет проблематичной, но это не должно быть проблемой для вашего JsonObjectBuilder использования.

Вам нужен DSL? Вы теряете возможность принудительно использовать клавиши String , но ваниль Kotlin не так уж плоха 🙂

 JSONObject(mapOf( "name" to "ilkin", "age" to 37, "male" to true, "contact" to mapOf( "city" to "istanbul", "email" to "xxx@yyy.com" ) )) 

Да, это возможно, если вам не требуется промежуточное представление узлов, и если контекст всегда один и тот же (рекурсивные вызовы не отличаются друг от друга). Это можно сделать, написав вывод сразу.

Однако это сильно увеличивает сложность кода, потому что вам нужно немедленно обрабатывать вызовы DSL, не сохраняя их в любом месте (опять же, чтобы избежать избыточных объектов).

Пример (см. Его демо здесь ):

 class JsonContext internal constructor() { internal val output = StringBuilder() private var indentation = 4 private fun StringBuilder.indent() = apply { for (i in 1..indentation) append(' ') } private var needsSeparator = false private fun StringBuilder.separator() = apply { if (needsSeparator) append(",\n") } infix fun String.to(value: Any) { output.separator().indent().append("\"$this\": \"$value\"") needsSeparator = true } infix fun String.toJson(block: JsonContext.() -> Unit) { output.separator().indent().append("\"$this\": {\n") indentation += 4 needsSeparator = false block(this@JsonContext) needsSeparator = true indentation -= 4 output.append("\n").indent().append("}") } } 

 fun json(block: JsonContext.() -> Unit) = JsonContext().run { block() "{\n" + output.toString() + "\n}" } 

 val j = json { "a" to 1 "b" to "abc" "c" toJson { "d" to 123 "e" toJson { "f" to "g" } } } 

Если вам не нужен отступ, но только действительный JSON, это может быть легко упрощено.

Вы можете сделать функции json { } и .toJson { } inline чтобы избавиться даже от лямбда-классов и, таким образом, вы достигли почти нулевого накладных расходов (один JsonContext и JsonContext с его буферами по-прежнему выделены), но это потребует от вас измените модификаторы видимости членов, @PublishedApi internal пользуются эти функции: общедоступные встроенные функции могут обращаться только к public или @PublishedApi internal членам.

Я не уверен, правильно ли задал вопрос. Вам не нужен строитель?

 class Json() { val json = JSONObject() constructor(init: Json.() -> Unit) : this() { this.init() } infix fun <T> String.To(value: T) { json.put(this, value) } override fun toString(): String { return json.toString() } } 

Вы можете просто сделать это:

 val json = Json { "name" To "Roy" "body" To Json { "height" To 173 "weight" To 80 } } println(json) 

{"name":"Roy","body":"{\"weight\":80,\"height\":173}"}