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 "[email protected]"
                }
            }
    println(jsonObject)
}

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

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true}

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

Ответ 1

Вы можете использовать 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 "[email protected]"
                }
            }
    println(jsonObject)
}

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

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true}

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

Ответ 2

Вам нужен DSL? Вы теряете способность применять клавиши String, но ваниль Котлин не так уж плоха:)

JSONObject(mapOf(
        "name" to "ilkin",
        "age" to 37,
        "male" to true,
        "contact" to mapOf(
                "city" to "istanbul",
                "email" to "[email protected]"
        )
))

Ответ 3

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

Однако это значительно увеличивает сложность кода, потому что вам нужно немедленно обрабатывать вызовы 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([email protected])
        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 и StringBuilder с его буферами все еще выделены), но для этого потребуется изменить модификаторы видимости членов, которые используют эти функции: общедоступные встроенные функции могут обращаться только к членам public или @PublishedApi internal.

Ответ 4

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

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}"}

Ответ 5

Нашел другое решение. Вы можете просто наследовать класс JSONObject без необходимости создавать другие объекты.

class Json() : JSONObject() {

    constructor(init: Json.() -> Unit) : this() {
        this.init()
    }

    infix fun <T> String.To(value: T) {
        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 "[email protected]"
                }
            }
    println(jsonObject)
}

Вывод кода будет таким же.

{"contact":{"city":"istanbul","email":"[email protected]"},"name":"ilkin","age":37,"male":true}