diff --git a/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/JsonValue.kt b/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/JsonValue.kt index ccfc26c..d6b3b7d 100644 --- a/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/JsonValue.kt +++ b/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/JsonValue.kt @@ -9,139 +9,43 @@ sealed interface JsonValue /** * JSON object: {"key": value, ...} */ -class JsonObject private constructor( - private val tape: Tape?, - private val tapeIdx: Int, - private val stringBuffer: ByteArray?, - private val _entries: List>? +class JsonObject internal constructor( + private val entries: List> ) : JsonValue, Iterable> { - internal constructor(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray) - : this(tape, tapeIdx, stringBuffer, null) - - internal constructor(entries: List>) - : this(null, 0, null, entries) - /** Number of key-value pairs. */ val size: Int - get() = _entries?.size ?: tape!!.getScopeCount(tapeIdx) + get() = entries.size /** Get value by key, or null if not found. */ - operator fun get(key: String): JsonValue? { - if (_entries != null) { - return _entries.firstOrNull { it.first == key }?.second - } - val keyBytes = key.encodeToByteArray() - var idx = tapeIdx + 1 - val endIdx = tape!!.getMatchingBraceIndex(tapeIdx) - 1 - while (idx < endIdx) { - val sbIdx = tape.getValue(idx).toInt() - val len = IntegerUtils.toInt(stringBuffer!!, sbIdx) - val valIdx = tape.computeNextIndex(idx) - val nextKeyIdx = tape.computeNextIndex(valIdx) - if (len == keyBytes.size) { - val from = sbIdx + Int.SIZE_BYTES - if (regionEquals(keyBytes, 0, stringBuffer, from, len)) { - return materializeTapeValue(tape, valIdx, stringBuffer) - } - } - idx = nextKeyIdx - } - return null - } + operator fun get(key: String): JsonValue? = entries.firstOrNull { it.first == key }?.second /** All keys in insertion order. */ - fun keys(): Set { - if (_entries != null) { - return _entries.mapTo(LinkedHashSet()) { it.first } - } - val result = LinkedHashSet() - var idx = tapeIdx + 1 - val endIdx = tape!!.getMatchingBraceIndex(tapeIdx) - 1 - while (idx < endIdx) { - result.add(readStringFromTape(tape, idx, stringBuffer!!)) - val valIdx = tape.computeNextIndex(idx) - idx = tape.computeNextIndex(valIdx) - } - return result - } + fun keys(): Set = entries.mapTo(LinkedHashSet()) { it.first } /** Iterate over key-value pairs. */ - override fun iterator(): Iterator> { - if (_entries != null) return _entries.iterator() - return TapeObjectIterator() - } + override fun iterator(): Iterator> = entries.iterator() /** Check if key exists. */ operator fun contains(key: String): Boolean = get(key) != null - - private inner class TapeObjectIterator : Iterator> { - private var idx = tapeIdx + 1 - private val endIdx = tape!!.getMatchingBraceIndex(tapeIdx) - 1 - - override fun hasNext(): Boolean = idx < endIdx - - override fun next(): Pair { - if (!hasNext()) throw NoSuchElementException("No more fields") - val key = readStringFromTape(tape!!, idx, stringBuffer!!) - idx = tape.computeNextIndex(idx) - val value = materializeTapeValue(tape, idx, stringBuffer) - idx = tape.computeNextIndex(idx) - return key to value - } - } } /** * JSON array: [value, ...] */ -class JsonArray private constructor( - private val tape: Tape?, - private val tapeIdx: Int, - private val stringBuffer: ByteArray?, - private val _elements: List? +class JsonArray internal constructor( + private val elements: List ) : JsonValue, Iterable { - internal constructor(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray) - : this(tape, tapeIdx, stringBuffer, null) - - internal constructor(elements: List) - : this(null, 0, null, elements) - /** Number of elements. */ val size: Int - get() = _elements?.size ?: tape!!.getScopeCount(tapeIdx) + get() = elements.size /** Get element by index. Throws [IndexOutOfBoundsException]. */ - operator fun get(index: Int): JsonValue { - if (_elements != null) { - return _elements[index] - } - if (index < 0 || index >= size) throw IndexOutOfBoundsException("Index $index, size $size") - var idx = tapeIdx + 1 - repeat(index) { idx = tape!!.computeNextIndex(idx) } - return materializeTapeValue(tape!!, idx, stringBuffer!!) - } + operator fun get(index: Int): JsonValue = elements[index] /** Iterate over elements. */ - override fun iterator(): Iterator { - if (_elements != null) return _elements.iterator() - return TapeArrayIterator() - } - - private inner class TapeArrayIterator : Iterator { - private var idx = tapeIdx + 1 - private val endIdx = tape!!.getMatchingBraceIndex(tapeIdx) - 1 - - override fun hasNext(): Boolean = idx < endIdx - - override fun next(): JsonValue { - if (!hasNext()) throw NoSuchElementException("No more elements") - val value = materializeTapeValue(tape!!, idx, stringBuffer!!) - idx = tape.computeNextIndex(idx) - return value - } - } + override fun iterator(): Iterator = elements.iterator() } /** @@ -246,10 +150,3 @@ value class JsonBoolean(val value: Boolean) : JsonValue * JSON null literal. */ data object JsonNull : JsonValue - -private fun regionEquals(a: ByteArray, aOffset: Int, b: ByteArray, bOffset: Int, length: Int): Boolean { - for (i in 0 until length) { - if (a[aOffset + i] != b[bOffset + i]) return false - } - return true -} diff --git a/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/SimdJsonParser.kt b/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/SimdJsonParser.kt index 39fbaa8..f49773d 100644 --- a/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/SimdJsonParser.kt +++ b/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/SimdJsonParser.kt @@ -22,6 +22,9 @@ expect class SimdJsonParser( /** * Parse JSON bytes into a full DOM tree. * + * The returned tree is fully materialized and independent of this parser: + * it remains valid after subsequent [parse]/[iterate] calls and after [close]. + * * @param data JSON input as a byte array (UTF-8 encoded) * @param length number of bytes to parse (defaults to full array) * @return parsed [JsonValue] representing the root element @@ -33,6 +36,9 @@ expect class SimdJsonParser( /** * Parse a JSON string into a full DOM tree. * + * The returned tree is fully materialized and independent of this parser: + * it remains valid after subsequent [parse]/[iterate] calls and after [close]. + * * @param json JSON input as a string * @return parsed [JsonValue] representing the root element * @throws JsonParsingException if the input is not valid JSON diff --git a/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/TapeValueMaterializer.kt b/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/TapeValueMaterializer.kt deleted file mode 100644 index 082dc54..0000000 --- a/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/TapeValueMaterializer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.github.devcrocod.simdjson - -internal fun materializeTapeValue(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray): JsonValue { - return when (tape.getType(tapeIdx)) { - Tape.START_OBJECT -> JsonObject(tape, tapeIdx, stringBuffer) - Tape.START_ARRAY -> JsonArray(tape, tapeIdx, stringBuffer) - Tape.STRING -> JsonString(readStringFromTape(tape, tapeIdx, stringBuffer)) - Tape.INT64 -> JsonNumber(longValue = tape.getInt64Value(tapeIdx)) - Tape.UINT64 -> JsonNumber.ofULong(tape.getInt64Value(tapeIdx).toULong()) - Tape.DOUBLE -> JsonNumber(doubleValue = tape.getDouble(tapeIdx)) - Tape.TRUE_VALUE -> JsonBoolean(true) - Tape.FALSE_VALUE -> JsonBoolean(false) - Tape.NULL_VALUE -> JsonNull - else -> throw JsonParsingException("Unknown tape type '${tape.getType(tapeIdx)}' at index $tapeIdx") - } -} - -internal fun readStringFromTape(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray): String { - val stringBufferIdx = tape.getValue(tapeIdx).toInt() - val len = IntegerUtils.toInt(stringBuffer, stringBufferIdx) - return stringBuffer.decodeToString(stringBufferIdx + Int.SIZE_BYTES, stringBufferIdx + Int.SIZE_BYTES + len) -} diff --git a/simdjson-kotlin/src/commonTest/kotlin/io/github/devcrocod/simdjson/DomParsingTest.kt b/simdjson-kotlin/src/commonTest/kotlin/io/github/devcrocod/simdjson/DomParsingTest.kt index 1e997de..c9e776f 100644 --- a/simdjson-kotlin/src/commonTest/kotlin/io/github/devcrocod/simdjson/DomParsingTest.kt +++ b/simdjson-kotlin/src/commonTest/kotlin/io/github/devcrocod/simdjson/DomParsingTest.kt @@ -581,11 +581,46 @@ class DomParsingTest { fun `parser reuse`() { val parser = SimdJsonParser() - val r1 = parser.parse("[1, 2]").shouldBeInstanceOf() - r1.size shouldBe 2 + val r1 = parser.parse("""{"name": "first", "items": [1, 2, 3], "flag": true}""").shouldBeInstanceOf() + r1.size shouldBe 3 - val r2 = parser.parse("""{"x": 99}""").shouldBeInstanceOf() - (r2["x"] as JsonNumber).toLong() shouldBe 99L + val r2 = parser.parse("""["other", {"x": 99}, null]""").shouldBeInstanceOf() + + r1.size shouldBe 3 + (r1["name"] as JsonString).value shouldBe "first" + val items = r1["items"].shouldBeInstanceOf() + items.size shouldBe 3 + (items[0] as JsonNumber).toLong() shouldBe 1L + (items[2] as JsonNumber).toLong() shouldBe 3L + (r1["flag"] as JsonBoolean).value shouldBe true + + r2.size shouldBe 3 + (r2[0] as JsonString).value shouldBe "other" + ((r2[1] as JsonObject)["x"] as JsonNumber).toLong() shouldBe 99L + r2[2] shouldBe JsonNull + } + + @Test + fun `dom result survives iterate on same parser`() { + val parser = SimdJsonParser() + val r1 = parser.parse("""{"a": [1, {"b": "text"}]}""").shouldBeInstanceOf() + + parser.iterate("\"zz\"").use { it.getString() shouldBe "zz" } + + val arr = r1["a"].shouldBeInstanceOf() + arr.size shouldBe 2 + ((arr[1] as JsonObject)["b"] as JsonString).value shouldBe "text" + } + + @Test + fun `dom result survives parser close`() { + val parser = SimdJsonParser() + val r = parser.parse("""{"k": ["v", 42]}""").shouldBeInstanceOf() + parser.close() + + val arr = r["k"].shouldBeInstanceOf() + (arr[0] as JsonString).value shouldBe "v" + (arr[1] as JsonNumber).toLong() shouldBe 42L } // --- ByteArray API --- diff --git a/simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/Tape.kt b/simdjson-kotlin/src/jvmAndAndroidMain/kotlin/io/github/devcrocod/simdjson/Tape.kt similarity index 100% rename from simdjson-kotlin/src/commonMain/kotlin/io/github/devcrocod/simdjson/Tape.kt rename to simdjson-kotlin/src/jvmAndAndroidMain/kotlin/io/github/devcrocod/simdjson/Tape.kt diff --git a/simdjson-kotlin/src/jvmMain/kotlin/io/github/devcrocod/simdjson/TapeValueMaterializer.kt b/simdjson-kotlin/src/jvmMain/kotlin/io/github/devcrocod/simdjson/TapeValueMaterializer.kt new file mode 100644 index 0000000..53d07f6 --- /dev/null +++ b/simdjson-kotlin/src/jvmMain/kotlin/io/github/devcrocod/simdjson/TapeValueMaterializer.kt @@ -0,0 +1,46 @@ +package io.github.devcrocod.simdjson + +internal fun materializeTapeValue(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray): JsonValue { + return when (tape.getType(tapeIdx)) { + Tape.START_OBJECT -> materializeObject(tape, tapeIdx, stringBuffer) + Tape.START_ARRAY -> materializeArray(tape, tapeIdx, stringBuffer) + Tape.STRING -> JsonString(readStringFromTape(tape, tapeIdx, stringBuffer)) + Tape.INT64 -> JsonNumber(longValue = tape.getInt64Value(tapeIdx)) + Tape.UINT64 -> JsonNumber.ofULong(tape.getInt64Value(tapeIdx).toULong()) + Tape.DOUBLE -> JsonNumber(doubleValue = tape.getDouble(tapeIdx)) + Tape.TRUE_VALUE -> JsonBoolean(true) + Tape.FALSE_VALUE -> JsonBoolean(false) + Tape.NULL_VALUE -> JsonNull + else -> throw JsonParsingException("Unknown tape type '${tape.getType(tapeIdx)}' at index $tapeIdx") + } +} + +private fun materializeObject(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray): JsonObject { + val entries = ArrayList>(tape.getScopeCount(tapeIdx)) + var idx = tapeIdx + 1 + val endIdx = tape.getMatchingBraceIndex(tapeIdx) - 1 + while (idx < endIdx) { + val key = readStringFromTape(tape, idx, stringBuffer) + idx = tape.computeNextIndex(idx) + entries.add(key to materializeTapeValue(tape, idx, stringBuffer)) + idx = tape.computeNextIndex(idx) + } + return JsonObject(entries) +} + +private fun materializeArray(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray): JsonArray { + val elements = ArrayList(tape.getScopeCount(tapeIdx)) + var idx = tapeIdx + 1 + val endIdx = tape.getMatchingBraceIndex(tapeIdx) - 1 + while (idx < endIdx) { + elements.add(materializeTapeValue(tape, idx, stringBuffer)) + idx = tape.computeNextIndex(idx) + } + return JsonArray(elements) +} + +private fun readStringFromTape(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray): String { + val stringBufferIdx = tape.getValue(tapeIdx).toInt() + val len = IntegerUtils.toInt(stringBuffer, stringBufferIdx) + return stringBuffer.decodeToString(stringBufferIdx + Int.SIZE_BYTES, stringBufferIdx + Int.SIZE_BYTES + len) +}