Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pair<String, JsonValue>>?
class JsonObject internal constructor(
private val entries: List<Pair<String, JsonValue>>
) : JsonValue, Iterable<Pair<String, JsonValue>> {

internal constructor(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray)
: this(tape, tapeIdx, stringBuffer, null)

internal constructor(entries: List<Pair<String, JsonValue>>)
: 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<String> {
if (_entries != null) {
return _entries.mapTo(LinkedHashSet()) { it.first }
}
val result = LinkedHashSet<String>()
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<String> = entries.mapTo(LinkedHashSet()) { it.first }

/** Iterate over key-value pairs. */
override fun iterator(): Iterator<Pair<String, JsonValue>> {
if (_entries != null) return _entries.iterator()
return TapeObjectIterator()
}
override fun iterator(): Iterator<Pair<String, JsonValue>> = entries.iterator()

/** Check if key exists. */
operator fun contains(key: String): Boolean = get(key) != null

private inner class TapeObjectIterator : Iterator<Pair<String, JsonValue>> {
private var idx = tapeIdx + 1
private val endIdx = tape!!.getMatchingBraceIndex(tapeIdx) - 1

override fun hasNext(): Boolean = idx < endIdx

override fun next(): Pair<String, JsonValue> {
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<JsonValue>?
class JsonArray internal constructor(
private val elements: List<JsonValue>
) : JsonValue, Iterable<JsonValue> {

internal constructor(tape: Tape, tapeIdx: Int, stringBuffer: ByteArray)
: this(tape, tapeIdx, stringBuffer, null)

internal constructor(elements: List<JsonValue>)
: 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<JsonValue> {
if (_elements != null) return _elements.iterator()
return TapeArrayIterator()
}

private inner class TapeArrayIterator : Iterator<JsonValue> {
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<JsonValue> = elements.iterator()
}
Comment on lines +48 to 49

/**
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -581,11 +581,46 @@ class DomParsingTest {
fun `parser reuse`() {
val parser = SimdJsonParser()

val r1 = parser.parse("[1, 2]").shouldBeInstanceOf<JsonArray>()
r1.size shouldBe 2
val r1 = parser.parse("""{"name": "first", "items": [1, 2, 3], "flag": true}""").shouldBeInstanceOf<JsonObject>()
r1.size shouldBe 3

val r2 = parser.parse("""{"x": 99}""").shouldBeInstanceOf<JsonObject>()
(r2["x"] as JsonNumber).toLong() shouldBe 99L
val r2 = parser.parse("""["other", {"x": 99}, null]""").shouldBeInstanceOf<JsonArray>()

r1.size shouldBe 3
(r1["name"] as JsonString).value shouldBe "first"
val items = r1["items"].shouldBeInstanceOf<JsonArray>()
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<JsonObject>()

parser.iterate("\"zz\"").use { it.getString() shouldBe "zz" }

val arr = r1["a"].shouldBeInstanceOf<JsonArray>()
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<JsonObject>()
parser.close()

val arr = r["k"].shouldBeInstanceOf<JsonArray>()
(arr[0] as JsonString).value shouldBe "v"
(arr[1] as JsonNumber).toLong() shouldBe 42L
}

// --- ByteArray API ---
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Pair<String, JsonValue>>(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<JsonValue>(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)
}