[Kotlin][FlexBuffers] JSON support for Flexbuffers (#6417)
authorPaulo Pinheiro <paulovictor.pinheiro@gmail.com>
Mon, 29 Mar 2021 22:57:23 +0000 (00:57 +0200)
committerGitHub <noreply@github.com>
Mon, 29 Mar 2021 22:57:23 +0000 (15:57 -0700)
* [Kotlin][FlexBuffers] Add JSON support for FlexBuffers

* [Kotlin][Flexbuffers] Re-implement JSON parser with a tokenizer.

kotlin/benchmark/build.gradle.kts
kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/FlexBuffersBenchmark.kt
kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt [new file with mode: 0644]
kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Buffers.kt
kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt [new file with mode: 0644]
kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/Utf8.kt
kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/FlexBuffersTest.kt
kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/JSONTest.kt [new file with mode: 0644]

index 39fe573..1b3b630 100644 (file)
@@ -5,6 +5,7 @@ plugins {
   id("org.jetbrains.kotlin.plugin.allopen") version "1.4.20"
   id("kotlinx.benchmark") version "0.2.0-dev-20"
   id("io.morethan.jmhreport") version "0.9.0"
+  id("de.undercouch.download") version "4.1.1"
 }
 
 // allOpen plugin is needed for the benchmark annotations.
@@ -32,6 +33,8 @@ benchmark {
       iterations = 5
       iterationTime = 300
       iterationTimeUnit = "ms"
+      // uncomment for benchmarking JSON op only
+      // include(".*JsonBenchmark.*")
     }
   }
   targets {
@@ -76,6 +79,11 @@ kotlin {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
         implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.1")
 
+        //moshi
+        implementation("com.squareup.moshi:moshi-kotlin:1.11.0")
+
+        //gson
+        implementation("com.google.code.gson:gson:2.8.5")
       }
     }
 
@@ -88,3 +96,16 @@ kotlin {
     }
   }
 }
+
+// This task download all JSON files used for benchmarking
+tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadMultipleFiles") {
+  // We are downloading json benchmark samples from serdes-rs project.
+  // see: https://github.com/serde-rs/json-benchmark/blob/master/data
+  val baseUrl = "https://github.com/serde-rs/json-benchmark/raw/master/data/"
+  src(listOf("$baseUrl/canada.json", "$baseUrl/twitter.json", "$baseUrl/citm_catalog.json"))
+  dest(File("${project.projectDir.absolutePath}/src/jvmMain/resources"))
+}
+
+project.tasks.named("compileKotlinJvm") {
+  dependsOn("downloadMultipleFiles")
+}
index 49f7443..ade57d9 100644 (file)
@@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit
 @BenchmarkMode(Mode.AverageTime)
 @OutputTimeUnit(TimeUnit.NANOSECONDS)
 @Measurement(iterations = 20, time = 1, timeUnit = TimeUnit.NANOSECONDS)
-class KotlinBenchmark {
+class FlexBuffersBenchmark {
 
   var initialCapacity = 1024
   var value: Double = 0.0
diff --git a/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt b/kotlin/benchmark/src/jvmMain/kotlin/com/google/flatbuffers/kotlin/benchmark/JsonBenchmark.kt
new file mode 100644 (file)
index 0000000..7d2ae50
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.flatbuffers.kotlin.benchmark
+
+import com.google.flatbuffers.kotlin.ArrayReadBuffer
+import com.google.flatbuffers.kotlin.JSONParser
+import com.google.flatbuffers.kotlin.Reference
+import com.google.flatbuffers.kotlin.toJson
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import com.google.gson.JsonParser
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import kotlinx.benchmark.Blackhole
+import okio.Buffer
+import org.openjdk.jmh.annotations.Benchmark
+import org.openjdk.jmh.annotations.BenchmarkMode
+import org.openjdk.jmh.annotations.Measurement
+import org.openjdk.jmh.annotations.Mode
+import org.openjdk.jmh.annotations.OutputTimeUnit
+import org.openjdk.jmh.annotations.Scope
+import org.openjdk.jmh.annotations.State
+import java.io.ByteArrayInputStream
+import java.io.InputStreamReader
+import java.util.concurrent.TimeUnit
+
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Measurement(iterations = 100, time = 1, timeUnit = TimeUnit.MICROSECONDS)
+class JsonBenchmark {
+
+  final val moshi = Moshi.Builder()
+    .addLast(KotlinJsonAdapterFactory())
+    .build()
+  final val moshiAdapter = moshi.adapter(Map::class.java)
+
+  final val gson = Gson()
+  final val gsonParser = JsonParser()
+
+  val fbParser = JSONParser()
+
+  final val twitterData = this.javaClass.classLoader.getResourceAsStream("twitter.json")!!.readBytes()
+  final val canadaData = this.javaClass.classLoader.getResourceAsStream("canada.json")!!.readBytes()
+  final val citmData = this.javaClass.classLoader.getResourceAsStream("citm_catalog.json")!!.readBytes()
+
+  val fbCitmRef = JSONParser().parse(ArrayReadBuffer(citmData))
+  val moshiCitmRef = moshi.adapter(Map::class.java).fromJson(citmData.decodeToString())
+  val gsonCitmRef = gsonParser.parse(citmData.decodeToString())
+
+  fun readFlexBuffers(data: ByteArray): Reference = fbParser.parse(ArrayReadBuffer(data))
+
+  fun readMoshi(data: ByteArray): Map<*, *>? {
+    val buffer = Buffer().write(data)
+    return moshiAdapter.fromJson(buffer)
+  }
+
+  fun readGson(data: ByteArray): JsonObject {
+    val parser = JsonParser()
+    val jsonReader = InputStreamReader(ByteArrayInputStream(data))
+    return parser.parse(jsonReader).asJsonObject
+  }
+
+  // TWITTER
+  @Benchmark
+  fun readTwitterFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(twitterData))
+  @Benchmark
+  fun readTwitterMoshi(hole: Blackhole?) = hole?.consume(readMoshi(twitterData))
+  @Benchmark
+  fun readTwitterGson(hole: Blackhole?) = hole?.consume(readGson(twitterData))
+
+  @Benchmark
+  fun roundTripTwitterFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(twitterData).toJson())
+  @Benchmark
+  fun roundTripTwitterMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(readMoshi(twitterData)))
+  @Benchmark
+  fun roundTripTwitterGson(hole: Blackhole?) = hole?.consume(gson.toJson(readGson(twitterData)))
+
+  // CITM
+  @Benchmark
+  fun readCITMFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(citmData))
+  @Benchmark
+  fun readCITMMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(readMoshi(citmData)))
+  @Benchmark
+  fun readCITMGson(hole: Blackhole?) = hole?.consume(gson.toJson(readGson(citmData)))
+
+  @Benchmark
+  fun roundTripCITMFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(citmData).toJson())
+  @Benchmark
+  fun roundTripCITMMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(readMoshi(citmData)))
+  @Benchmark
+  fun roundTripCITMGson(hole: Blackhole?) = hole?.consume(gson.toJson(readGson(citmData)))
+
+  @Benchmark
+  fun writeCITMFlexBuffers(hole: Blackhole? = null) = hole?.consume(fbCitmRef.toJson())
+  @Benchmark
+  fun writeCITMMoshi(hole: Blackhole?) = hole?.consume(moshiAdapter.toJson(moshiCitmRef))
+  @Benchmark
+  fun writeCITMGson(hole: Blackhole?) = hole?.consume(gson.toJson(gsonCitmRef))
+
+  // CANADA
+  @Benchmark
+  fun readCanadaFlexBuffers(hole: Blackhole? = null) = hole?.consume(readFlexBuffers(canadaData))
+  @Benchmark
+  fun readCanadaMoshi(hole: Blackhole?) = hole?.consume(readMoshi(canadaData))
+  @Benchmark
+  fun readCanadaGson(hole: Blackhole?) = hole?.consume(readGson(canadaData))
+}
index 998cab7..9851d90 100644 (file)
@@ -322,7 +322,8 @@ public interface ReadWriteBuffer : ReadBuffer {
   public fun requestCapacity(capacity: Int)
 }
 
-public class ArrayReadBuffer(private val buffer: ByteArray, override var limit: Int = buffer.size) : ReadBuffer {
+public open class ArrayReadBuffer(protected var buffer: ByteArray, override val limit: Int = buffer.size) : ReadBuffer {
+
   override fun findFirst(value: Byte, start: Int, end: Int): Int {
     val e = min(end, limit)
     val s = max(0, start)
@@ -369,9 +370,9 @@ public class ArrayReadBuffer(private val buffer: ByteArray, override var limit:
  * All operations assumes Little Endian byte order.
  */
 public class ArrayReadWriteBuffer(
-  private var buffer: ByteArray,
+  buffer: ByteArray,
   override var writePosition: Int = 0
-) : ReadWriteBuffer {
+) : ArrayReadBuffer(buffer, writePosition), ReadWriteBuffer {
 
   public constructor(initialCapacity: Int = 10) : this(ByteArray(initialCapacity))
 
@@ -379,34 +380,6 @@ public class ArrayReadWriteBuffer(
 
   override fun clear(): Unit = run { writePosition = 0 }
 
-  override fun getBoolean(index: Int): Boolean = buffer[index] != 0.toByte()
-
-  override operator fun get(index: Int): Byte = buffer[index]
-
-  override fun getUByte(index: Int): UByte = buffer.getUByte(index)
-
-  override fun getShort(index: Int): Short = buffer.getShort(index)
-
-  override fun getUShort(index: Int): UShort = buffer.getUShort(index)
-
-  override fun getInt(index: Int): Int = buffer.getInt(index)
-
-  override fun getUInt(index: Int): UInt = buffer.getUInt(index)
-
-  override fun getLong(index: Int): Long = buffer.getLong(index)
-
-  override fun getULong(index: Int): ULong = buffer.getULong(index)
-
-  override fun getFloat(index: Int): Float = buffer.getFloat(index)
-
-  override fun getDouble(index: Int): Double = buffer.getDouble(index)
-
-  override fun getString(start: Int, size: Int): String = buffer.decodeToString(start, start + size)
-
-  override fun data(): ByteArray = buffer
-
-  override fun slice(start: Int, size: Int): ReadBuffer = ArrayReadWriteBuffer(buffer, writePosition)
-
   override fun put(value: Boolean) {
     set(writePosition, value)
     writePosition++
@@ -509,13 +482,6 @@ public class ArrayReadWriteBuffer(
     buffer = buffer.copyOf(newCapacity)
   }
 
-  override fun findFirst(value: Byte, start: Int, end: Int): Int {
-    val e = min(end, buffer.size)
-    val s = max(0, start)
-    for (i in s until e) if (buffer[i] == value) return i
-    return -1
-  }
-
   private inline fun withCapacity(size: Int, crossinline action: ByteArray.() -> Unit) {
     requestCapacity(size)
     buffer.action()
diff --git a/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt b/kotlin/flatbuffers-kotlin/src/commonMain/kotlin/com/google/flatbuffers/kotlin/JSON.kt
new file mode 100644 (file)
index 0000000..ce302ed
--- /dev/null
@@ -0,0 +1,828 @@
+/*
+ * Copyright 2021 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.google.flatbuffers.kotlin
+
+import com.google.flatbuffers.kotlin.FlexBuffersBuilder.Companion.SHARE_KEYS_AND_STRINGS
+import kotlin.experimental.and
+import kotlin.math.pow
+
+/**
+ * Returns a minified version of this FlexBuffer as a JSON.
+ */
+public fun Reference.toJson(): String = ArrayReadWriteBuffer(1024).let {
+  toJson(it)
+  val data = it.data() // it.getString(0, it.writePosition)
+  return data.decodeToString(0, it.writePosition)
+}
+
+/**
+ * Returns a minified version of this FlexBuffer as a JSON.
+ * @param out [ReadWriteBuffer] the JSON will be written.
+ */
+public fun Reference.toJson(out: ReadWriteBuffer) {
+  when (type) {
+    T_STRING -> {
+      val start = buffer.indirect(end, parentWidth)
+      val size = buffer.readULong(start - byteWidth, byteWidth).toInt()
+      out.jsonEscape(buffer, start, size)
+    }
+    T_KEY -> {
+      val start = buffer.indirect(end, parentWidth)
+      val end = buffer.findFirst(0.toByte(), start)
+      out.jsonEscape(buffer, start, end - start)
+    }
+    T_BLOB -> {
+      val blob = toBlob()
+      out.jsonEscape(out, blob.end, blob.size)
+    }
+    T_INT -> out.put(toLong().toString())
+    T_UINT -> out.put(toULong().toString())
+    T_FLOAT -> out.put(toDouble().toString())
+    T_NULL -> out.put("null")
+    T_BOOL -> out.put(toBoolean().toString())
+    T_MAP -> toMap().toJson(out)
+    T_VECTOR, T_VECTOR_BOOL, T_VECTOR_FLOAT, T_VECTOR_INT,
+    T_VECTOR_UINT, T_VECTOR_KEY, T_VECTOR_STRING_DEPRECATED -> toVector().toJson(out)
+    else -> error("Unable to convert type ${type.typeToString()} to JSON")
+  }
+}
+
+/**
+ * Returns a minified version of this FlexBuffer as a JSON.
+ */
+public fun Map.toJson(): String = ArrayReadWriteBuffer(1024).let { toJson(it); it.toString() }
+
+/**
+ * Returns a minified version of this FlexBuffer as a JSON.
+ * @param out [ReadWriteBuffer] the JSON will be written.
+ */
+public fun Map.toJson(out: ReadWriteBuffer) {
+  out.put('{'.toByte())
+  // key values pairs
+  for (i in 0 until size) {
+    val key = keyAt(i)
+    out.jsonEscape(buffer, key.start, key.sizeInBytes)
+    out.put(':'.toByte())
+    get(i).toJson(out)
+    if (i != size - 1) {
+      out.put(','.toByte())
+    }
+  }
+  // close bracket
+  out.put('}'.toByte())
+}
+
+/**
+ * Returns a minified version of this FlexBuffer as a JSON.
+ */
+public fun Vector.toJson(): String = ArrayReadWriteBuffer(1024).let { toJson(it); it.toString() }
+
+/**
+ * Returns a minified version of this FlexBuffer as a JSON.
+ * @param out that the JSON is being concatenated.
+ */
+public fun Vector.toJson(out: ReadWriteBuffer) {
+  out.put('['.toByte())
+  for (i in 0 until size) {
+    get(i).toJson(out)
+    if (i != size - 1) {
+      out.put(','.toByte())
+    }
+  }
+  out.put(']'.toByte())
+}
+
+/**
+ * JSONParser class is used to parse a JSON as FlexBuffers. Calling [JSONParser.parse] fiils [output]
+ * and returns a [Reference] ready to be used.
+ */
+public class JSONParser(public var output: FlexBuffersBuilder = FlexBuffersBuilder(1024, SHARE_KEYS_AND_STRINGS)) {
+  private var readPos = 0
+  private var scopes = ScopeStack()
+
+  /**
+   * Parse a json as [String] and returns a [Reference] to a FlexBuffer.
+   */
+  public fun parse(data: String): Reference = parse(ArrayReadBuffer(data.encodeToByteArray()))
+
+  /**
+   * Parse a json as [ByteArray] and returns a [Reference] to a FlexBuffer.
+   */
+  public fun parse(data: ByteArray): Reference = parse(ArrayReadBuffer(data))
+
+  /**
+   * Parse a json as [ReadBuffer] and returns a [Reference] to a FlexBuffer.
+   */
+  public fun parse(data: ReadBuffer): Reference {
+    reset()
+    parseValue(data, nextToken(data), null)
+    if (readPos < data.limit) {
+      val tok = skipWhitespace(data)
+      if (tok != CHAR_EOF) {
+        makeError(data, "Extraneous charaters after parse has finished", tok)
+      }
+    }
+    output.finish()
+    return getRoot(output.buffer)
+  }
+
+  private fun parseValue(data: ReadBuffer, token: Token, key: String? = null): FlexBufferType {
+    return when (token) {
+      TOK_BEGIN_OBJECT -> parseObject(data, key)
+      TOK_BEGIN_ARRAY -> parseArray(data, key)
+      TOK_TRUE -> T_BOOL.also { output[key] = true }
+      TOK_FALSE -> T_BOOL.also { output[key] = false }
+      TOK_NULL -> T_NULL.also { output.putNull(key) }
+      TOK_BEGIN_QUOTE -> parseString(data, key)
+      TOK_NUMBER -> parseNumber(data, data.data(), key)
+      else -> makeError(data, "Unexpected Character while parsing", 'x'.toByte())
+    }
+  }
+
+  private fun parseObject(data: ReadBuffer, key: String? = null): FlexBufferType {
+    this.scopes.push(SCOPE_OBJ_EMPTY)
+
+    val fPos = output.startMap()
+    val limit = data.limit
+    while (readPos <= limit) {
+      when (val tok = nextToken(data)) {
+        TOK_END_OBJECT -> {
+          this.scopes.pop()
+          output.endMap(fPos, key); return T_MAP
+        }
+        TOK_BEGIN_QUOTE -> {
+          val childKey = readString(data)
+          parseValue(data, nextToken(data), childKey)
+        }
+        else -> makeError(data, "Expecting start of object key", tok)
+      }
+    }
+    makeError(data, "Unable to parse the object", "x".toByte())
+  }
+
+  private fun parseArray(data: ReadBuffer, key: String? = null): FlexBufferType {
+    this.scopes.push(SCOPE_ARRAY_EMPTY)
+    val fPos = output.startVector()
+    var elementType = T_INVALID
+    var multiType = false
+    val limit = data.limit
+
+    while (readPos <= limit) {
+      when (val tok = nextToken(data)) {
+        TOK_END_ARRAY -> {
+          this.scopes.pop()
+          return if (!multiType && elementType.isScalar()) {
+            output.endTypedVector(fPos, key)
+            elementType.toElementTypedVector()
+          } else {
+            output.endVector(key, fPos)
+            T_VECTOR
+          }
+        }
+
+        else -> {
+          val newType = parseValue(data, tok, null)
+
+          if (elementType == T_INVALID) {
+            elementType = newType
+          } else if (newType != elementType) {
+            multiType = true
+          }
+        }
+      }
+    }
+    makeError(data, "Unable to parse the array")
+  }
+
+  private fun parseNumber(data: ReadBuffer, array: ByteArray, key: String?): FlexBufferType {
+    val ary = array
+    var cursor = readPos
+    var c = data[readPos++]
+    var useDouble = false
+    val limit = ary.size
+    var sign = 1
+    var double = 0.0
+    var long = 0L
+    var digits = 0
+
+    if (c == CHAR_MINUS) {
+      cursor++
+      checkEOF(data, cursor)
+      c = ary[cursor]
+      sign = -1
+    }
+
+    // peek first byte
+    when (c) {
+      CHAR_0 -> {
+        cursor++
+        if (cursor != limit) {
+          c = ary[cursor]
+        }
+      }
+      !in CHAR_0..CHAR_9 -> makeError(data, "Invalid Number", c)
+      else -> {
+        do {
+          val digit = c - CHAR_0
+          // double = 10.0 * double + digit
+          long = 10 * long + digit
+          digits++
+          cursor++
+          if (cursor == limit) break
+          c = ary[cursor]
+        } while (c in CHAR_0..CHAR_9)
+      }
+    }
+
+    var exponent = 0
+    // If we find '.' we need to convert to double
+    if (c == CHAR_DOT) {
+      useDouble = true
+      checkEOF(data, cursor)
+      c = ary[++cursor]
+      if (c < CHAR_0 || c > CHAR_9) {
+        makeError(data, "Invalid Number", c)
+      }
+      do {
+        // double = double * 10 + (tok - CHAR_0)
+        long = 10 * long + (c - CHAR_0)
+        digits++
+        --exponent
+        cursor++
+        if (cursor == limit) break
+        c = ary[cursor]
+      } while (c in CHAR_0..CHAR_9)
+    }
+
+    // If we find 'e' we need to convert to double
+    if (c == CHAR_e || c == CHAR_E) {
+      useDouble = true
+      ++cursor
+      checkEOF(data, cursor)
+      c = ary[cursor]
+      var negativeExponent = false
+      if (c == CHAR_MINUS) {
+        ++cursor
+        checkEOF(data, cursor)
+        negativeExponent = true
+        c = ary[cursor]
+      } else if (c == CHAR_PLUS) {
+        ++cursor
+        checkEOF(data, cursor)
+        c = ary[cursor]
+      }
+      if (c < CHAR_0 || c > CHAR_9) {
+        makeError(data, "Missing exponent", c)
+      }
+      var exp = 0
+      do {
+        val digit = c - CHAR_0
+        exp = 10 * exp + digit
+        ++cursor
+        if (cursor == limit) break
+        c = ary[cursor]
+      } while (c in CHAR_0..CHAR_9)
+
+      exponent += if (negativeExponent) -exp else exp
+    }
+
+    if (digits > 17 || exponent < -19 || exponent > 19) {
+      // if the float number is not simple enough
+      // we use language's Double parsing, which is slower but
+      // produce more expected results for extreme numbers.
+      val firstPos = readPos - 1
+      val str = data.getString(firstPos, cursor - firstPos)
+      if (useDouble) {
+        double = str.toDouble()
+        output[key] = double
+      } else {
+        long = str.toLong()
+        output[key] = long
+      }
+    } else {
+      // this happens on single numbers outside any object
+      // or array
+      if (useDouble || exponent != 0) {
+        double = if (long == 0L) 0.0 else long.toDouble() * 10.0.pow(exponent)
+        double *= sign
+        output[key] = double
+      } else {
+        long *= sign
+        output[key] = long
+      }
+    }
+    readPos = cursor
+    return if (useDouble) T_FLOAT else T_INT
+  }
+
+  private fun parseString(data: ReadBuffer, key: String?): FlexBufferType {
+    output[key] = readString(data)
+    return T_STRING
+  }
+
+  private fun readString(data: ReadBuffer): String {
+    val limit = data.limit
+    if (data is ArrayReadBuffer) {
+      val ary = data.data()
+      // enables range check elimination
+      return readString(data, limit) { ary[it] }
+    }
+    return readString(data, limit) { data[it] }
+  }
+
+  private inline fun readString(data: ReadBuffer, limit: Int, crossinline fetch: (Int) -> Byte): String {
+    var cursorPos = readPos
+    var foundEscape = false
+    var currentChar: Byte = 0
+    // we loop over every 4 bytes until find any non-plain char
+    while (limit - cursorPos >= 4) {
+      currentChar = fetch(cursorPos)
+      if (!isPlainStringChar(currentChar)) {
+        foundEscape = true
+        break
+      }
+      currentChar = fetch(cursorPos + 1)
+      if (!isPlainStringChar(currentChar)) {
+        cursorPos += 1
+        foundEscape = true
+        break
+      }
+      currentChar = fetch(cursorPos + 2)
+      if (!isPlainStringChar(currentChar)) {
+        cursorPos += 2
+        foundEscape = true
+        break
+      }
+      currentChar = fetch(cursorPos + 3)
+      if (!isPlainStringChar(currentChar)) {
+        cursorPos += 3
+        foundEscape = true
+        break
+      }
+      cursorPos += 4
+    }
+    if (!foundEscape) {
+      // if non-plain string char is not found we loop over
+      // the remaining bytes
+      while (true) {
+        if (cursorPos >= limit) {
+          error("Unexpected end of string")
+        }
+        currentChar = fetch(cursorPos)
+        if (!isPlainStringChar(currentChar)) {
+          break
+        }
+        ++cursorPos
+      }
+    }
+    if (currentChar == CHAR_DOUBLE_QUOTE) {
+      val str = data.getString(readPos, cursorPos - readPos)
+      readPos = cursorPos + 1
+      return str
+    }
+    if (currentChar in 0..0x1f) {
+      error("Illegal Codepoint")
+    } else {
+      // backslash or >0x7f
+      return readStringSlow(data, currentChar, cursorPos)
+    }
+  }
+
+  private fun readStringSlow(data: ReadBuffer, first: Byte, lastPos: Int): String {
+    var cursorPos = lastPos
+
+    var endOfString = lastPos
+    while (true) {
+      val pos = data.findFirst(CHAR_DOUBLE_QUOTE, endOfString)
+      when {
+        pos == -1 -> makeError(data, "Unexpected EOF, missing end of string '\"'", first)
+        data[pos - 1] == CHAR_BACKSLASH && data[pos - 2] != CHAR_BACKSLASH -> {
+          // here we are checking for double quotes preceded by backslash. eg \"
+          // we have to look past pos -2 to make sure that the backlash is not
+          // part of a previous escape, eg "\\"
+          endOfString = pos + 1
+        }
+        else -> {
+          endOfString = pos; break
+        }
+      }
+    }
+    // copy everything before the escape
+    val builder = StringBuilder(data.getString(readPos, lastPos - readPos))
+    while (true) {
+      when (val pos = data.findFirst(CHAR_BACKSLASH, cursorPos, endOfString)) {
+        -1 -> {
+          val doubleQuotePos = data.findFirst(CHAR_DOUBLE_QUOTE, cursorPos)
+          if (doubleQuotePos == -1) makeError(data, "Reached EOF before enclosing string", first)
+          val rest = data.getString(cursorPos, doubleQuotePos - cursorPos)
+          builder.append(rest)
+          readPos = doubleQuotePos + 1
+          return builder.toString()
+        }
+
+        else -> {
+          // we write everything up to \
+          builder.append(data.getString(cursorPos, pos - cursorPos))
+          val c = data[pos + 1]
+          builder.append(readEscapedChar(data, c, pos))
+          cursorPos = pos + if (c == CHAR_u) 6 else 2
+        }
+      }
+    }
+  }
+
+  private inline fun isPlainStringChar(c: Byte): Boolean {
+    val flags = parseFlags
+    // return c in 0x20..0x7f && c != 0x22.toByte() && c != 0x5c.toByte()
+    return (flags[c.toInt() and 0xFF] and 1) != 0.toByte()
+  }
+
+  private inline fun isWhitespace(c: Byte): Boolean {
+    val flags = parseFlags
+    // return c == '\r'.toByte() || c == '\n'.toByte() || c == '\t'.toByte() || c == ' '.toByte()
+    return (flags[c.toInt() and 0xFF] and 2) != 0.toByte()
+  }
+
+  private fun reset() {
+    readPos = 0
+    output.clear()
+    scopes.reset()
+  }
+
+  private fun nextToken(data: ReadBuffer): Token {
+    val scope = this.scopes.last
+
+    when (scope) {
+      SCOPE_ARRAY_EMPTY -> this.scopes.last = SCOPE_ARRAY_FILLED
+      SCOPE_ARRAY_FILLED -> {
+        when (val c = skipWhitespace(data)) {
+          CHAR_CLOSE_ARRAY -> return TOK_END_ARRAY
+          CHAR_COMMA -> Unit
+          else -> makeError(data, "Unfinished Array", c)
+        }
+      }
+      SCOPE_OBJ_EMPTY, SCOPE_OBJ_FILLED -> {
+        this.scopes.last = SCOPE_OBJ_KEY
+        // Look for a comma before the next element.
+        if (scope == SCOPE_OBJ_FILLED) {
+          when (val c = skipWhitespace(data)) {
+            CHAR_CLOSE_OBJECT -> return TOK_END_OBJECT
+            CHAR_COMMA -> Unit
+            else -> makeError(data, "Unfinished Object", c)
+          }
+        }
+        return when (val c = skipWhitespace(data)) {
+          CHAR_DOUBLE_QUOTE -> TOK_BEGIN_QUOTE
+          CHAR_CLOSE_OBJECT -> if (scope != SCOPE_OBJ_FILLED) {
+            TOK_END_OBJECT
+          } else {
+            makeError(data, "Expected Key", c)
+          }
+          else -> {
+            makeError(data, "Expected Key/Value", c)
+          }
+        }
+      }
+      SCOPE_OBJ_KEY -> {
+        this.scopes.last = SCOPE_OBJ_FILLED
+        when (val c = skipWhitespace(data)) {
+          CHAR_COLON -> Unit
+          else -> makeError(data, "Expect ${CHAR_COLON.print()}", c)
+        }
+      }
+      SCOPE_DOC_EMPTY -> this.scopes.last = SCOPE_DOC_FILLED
+      SCOPE_DOC_FILLED -> {
+        val c = skipWhitespace(data)
+        if (c != CHAR_EOF)
+          makeError(data, "Root object already finished", c)
+        return TOK_EOF
+      }
+    }
+
+    val c = skipWhitespace(data)
+    when (c) {
+      CHAR_CLOSE_ARRAY -> if (scope == SCOPE_ARRAY_EMPTY) return TOK_END_ARRAY
+      CHAR_COLON -> makeError(data, "Unexpected character", c)
+      CHAR_DOUBLE_QUOTE -> return TOK_BEGIN_QUOTE
+      CHAR_OPEN_ARRAY -> return TOK_BEGIN_ARRAY
+      CHAR_OPEN_OBJECT -> return TOK_BEGIN_OBJECT
+      CHAR_t -> {
+        checkEOF(data, readPos + 2)
+        // 0x65757274 is equivalent to ['t', 'r', 'u', 'e' ] as a 4 byte Int
+        if (data.getInt(readPos - 1) != 0x65757274) {
+          makeError(data, "Expecting keyword \"true\"", c)
+        }
+        readPos += 3
+        return TOK_TRUE
+      }
+      CHAR_n -> {
+        checkEOF(data, readPos + 2)
+        // 0x6c6c756e  is equivalent to ['n', 'u', 'l', 'l' ] as a 4 byte Int
+        if (data.getInt(readPos - 1) != 0x6c6c756e) {
+          makeError(data, "Expecting keyword \"null\"", c)
+        }
+        readPos += 3
+        return TOK_NULL
+      }
+      CHAR_f -> {
+        checkEOF(data, readPos + 3)
+        // 0x65736c61 is equivalent to ['a', 'l', 's', 'e' ] as a 4 byte Int
+        if (data.getInt(readPos) != 0x65736c61) {
+          makeError(data, "Expecting keyword \"false\"", c)
+        }
+        readPos += 4
+        return TOK_FALSE
+      }
+      CHAR_0, CHAR_1, CHAR_2, CHAR_3, CHAR_4, CHAR_5,
+      CHAR_6, CHAR_7, CHAR_8, CHAR_9, CHAR_MINUS -> return TOK_NUMBER.also {
+        readPos-- // rewind one position so we don't lose first digit
+      }
+    }
+    makeError(data, "Expecting element", c)
+  }
+
+  // keeps increasing [readPos] until finds a non-whitespace byte
+  private inline fun skipWhitespace(data: ReadBuffer): Byte {
+    val limit = data.limit
+    if (data is ArrayReadBuffer) {
+      // enables range check elimination
+      val ary = data.data()
+      return skipWhitespace(limit) { ary[it] }
+    }
+    return skipWhitespace(limit) { data[it] }
+  }
+
+  private inline fun skipWhitespace(limit: Int, crossinline fetch: (Int) -> Byte): Byte {
+    var pos = readPos
+    while (pos < limit) {
+      val d = fetch(pos++)
+      if (!isWhitespace(d)) {
+        readPos = pos
+        return d
+      }
+    }
+    readPos = limit
+    return CHAR_EOF
+  }
+
+  // byte1 is expected to be first char before `\`
+  private fun readEscapedChar(data: ReadBuffer, byte1: Byte, cursorPos: Int): Char {
+    return when (byte1) {
+      CHAR_u -> {
+        checkEOF(data, cursorPos + 1 + 4)
+        var result: Char = 0.toChar()
+        var i = cursorPos + 2 // cursorPos is on '\\', cursorPos + 1 is 'u'
+        val end = i + 4
+        while (i < end) {
+          val part: Byte = data[i]
+          result = (result.toInt() shl 4).toChar()
+          result += when (part) {
+            in CHAR_0..CHAR_9 -> part - CHAR_0
+            in CHAR_a..CHAR_f -> part - CHAR_a + 10
+            in CHAR_A..CHAR_F -> part - CHAR_A + 10
+            else -> makeError(data, "Invalid utf8 escaped character", -1)
+          }
+          i++
+        }
+        result
+      }
+      CHAR_b -> '\b'
+      CHAR_t -> '\t'
+      CHAR_r -> '\r'
+      CHAR_n -> '\n'
+      CHAR_f -> 12.toChar() // '\f'
+      CHAR_DOUBLE_QUOTE, CHAR_BACKSLASH, CHAR_FORWARDSLASH -> byte1.toChar()
+      else -> makeError(data, "Invalid escape sequence.", byte1)
+    }
+  }
+
+  private fun Byte.print(): String = when (this) {
+    in 0x21..0x7E -> "'${this.toChar()}'" // visible ascii chars
+    CHAR_EOF -> "EOF"
+    else -> "'0x${this.toString(16)}'"
+  }
+
+  private inline fun makeError(data: ReadBuffer, msg: String, tok: Byte? = null): Nothing {
+    val (line, column) = calculateErrorPosition(data, readPos)
+    if (tok != null) {
+      error("Error At ($line, $column): $msg, got ${tok.print()}")
+    } else {
+      error("Error At ($line, $column): $msg")
+    }
+  }
+
+  private inline fun makeError(data: ReadBuffer, msg: String, tok: Token): Nothing {
+    val (line, column) = calculateErrorPosition(data, readPos)
+    error("Error At ($line, $column): $msg, got ${tok.print()}")
+  }
+
+  private inline fun checkEOF(data: ReadBuffer, pos: Int) {
+    if (pos >= data.limit)
+      makeError(data, "Unexpected end of file", -1)
+  }
+
+  private fun calculateErrorPosition(data: ReadBuffer, endPos: Int): Pair<Int, Int> {
+    var line = 1
+    var column = 1
+    var current = 0
+    while (current < endPos - 1) {
+      if (data[current++] == CHAR_NEWLINE) {
+        ++line
+        column = 1
+      } else {
+        ++column
+      }
+    }
+    return Pair(line, column)
+  }
+}
+
+internal inline fun Int.toPaddedHex(): String = "\\u${this.toString(16).padStart(4, '0')}"
+
+private inline fun ReadWriteBuffer.jsonEscape(data: ReadBuffer, start: Int, size: Int) {
+  val replacements = JSON_ESCAPE_CHARS
+  put(CHAR_DOUBLE_QUOTE)
+  var last = start
+  val length: Int = size
+  val ary = data.data()
+  for (i in start until start + length) {
+    val c = ary[i].toUByte()
+    var replacement: ByteArray?
+    if (c.toInt() < 128) {
+      replacement = replacements[c.toInt()]
+      if (replacement == null) {
+        continue
+      }
+    } else {
+      continue
+    }
+    if (last < i) {
+      put(ary, last, i - last)
+    }
+    put(replacement, 0, replacement.size)
+    last = i + 1
+  }
+  if (last < (last + length)) {
+    put(ary, last, (start + length) - last)
+  }
+  put(CHAR_DOUBLE_QUOTE)
+}
+
+// Following escape strategy defined in RFC7159.
+private val JSON_ESCAPE_CHARS: Array<ByteArray?> = arrayOfNulls<ByteArray>(128).apply {
+  this['\n'.toInt()] = "\\n".encodeToByteArray()
+  this['\t'.toInt()] = "\\t".encodeToByteArray()
+  this['\r'.toInt()] = "\\r".encodeToByteArray()
+  this['\b'.toInt()] = "\\b".encodeToByteArray()
+  this[0x0c] = "\\f".encodeToByteArray()
+  this['"'.toInt()] = "\\\"".encodeToByteArray()
+  this['\\'.toInt()] = "\\\\".encodeToByteArray()
+  for (i in 0..0x1f) {
+    this[i] = "\\u${i.toPaddedHex()}".encodeToByteArray()
+  }
+}
+
+// Scope is used to the define current space that the scanner is operating.
+private inline class Scope(val id: Int)
+private val SCOPE_DOC_EMPTY = Scope(0)
+private val SCOPE_DOC_FILLED = Scope(1)
+private val SCOPE_OBJ_EMPTY = Scope(2)
+private val SCOPE_OBJ_KEY = Scope(3)
+private val SCOPE_OBJ_FILLED = Scope(4)
+private val SCOPE_ARRAY_EMPTY = Scope(5)
+private val SCOPE_ARRAY_FILLED = Scope(6)
+
+// Keeps the stack state of the scopes being scanned. Currently defined to have a
+// max stack size of 22, as per tests cases defined in http://json.org/JSON_checker/
+private class ScopeStack(
+  private val ary: IntArray = IntArray(22) { SCOPE_DOC_EMPTY.id },
+  var lastPos: Int = 0
+) {
+  var last: Scope
+    get() = Scope(ary[lastPos])
+    set(x) {
+      ary[lastPos] = x.id
+    }
+
+  fun reset() {
+    lastPos = 0
+    ary[0] = SCOPE_DOC_EMPTY.id
+  }
+
+  fun pop(): Scope {
+    // println("Popping: ${last.print()}")
+    return Scope(ary[lastPos--])
+  }
+
+  fun push(scope: Scope): Scope {
+    if (lastPos == ary.size - 1)
+      error("Too much nesting reached. Max nesting is ${ary.size} levels")
+    // println("PUSHING : ${scope.print()}")
+    ary[++lastPos] = scope.id
+    return scope
+  }
+}
+
+private inline class Token(val id: Int) {
+  fun print(): String = when (this) {
+    TOK_EOF -> "TOK_EOF"
+    TOK_NONE -> "TOK_NONE"
+    TOK_BEGIN_OBJECT -> "TOK_BEGIN_OBJECT"
+    TOK_END_OBJECT -> "TOK_END_OBJECT"
+    TOK_BEGIN_ARRAY -> "TOK_BEGIN_ARRAY"
+    TOK_END_ARRAY -> "TOK_END_ARRAY"
+    TOK_NUMBER -> "TOK_NUMBER"
+    TOK_TRUE -> "TOK_TRUE"
+    TOK_FALSE -> "TOK_FALSE"
+    TOK_NULL -> "TOK_NULL"
+    TOK_BEGIN_QUOTE -> "TOK_BEGIN_QUOTE"
+    else -> this.toString()
+  }
+}
+
+private val TOK_EOF = Token(-1)
+private val TOK_NONE = Token(0)
+private val TOK_BEGIN_OBJECT = Token(1)
+private val TOK_END_OBJECT = Token(2)
+private val TOK_BEGIN_ARRAY = Token(3)
+private val TOK_END_ARRAY = Token(4)
+private val TOK_NUMBER = Token(5)
+private val TOK_TRUE = Token(6)
+private val TOK_FALSE = Token(7)
+private val TOK_NULL = Token(8)
+private val TOK_BEGIN_QUOTE = Token(9)
+
+private const val CHAR_NEWLINE = '\n'.toByte()
+private const val CHAR_OPEN_OBJECT = '{'.toByte()
+private const val CHAR_COLON = ':'.toByte()
+private const val CHAR_CLOSE_OBJECT = '}'.toByte()
+private const val CHAR_OPEN_ARRAY = '['.toByte()
+private const val CHAR_CLOSE_ARRAY = ']'.toByte()
+private const val CHAR_DOUBLE_QUOTE = '"'.toByte()
+private const val CHAR_BACKSLASH = '\\'.toByte()
+private const val CHAR_FORWARDSLASH = '/'.toByte()
+private const val CHAR_f = 'f'.toByte()
+private const val CHAR_a = 'a'.toByte()
+private const val CHAR_r = 'r'.toByte()
+private const val CHAR_t = 't'.toByte()
+private const val CHAR_n = 'n'.toByte()
+private const val CHAR_b = 'b'.toByte()
+private const val CHAR_e = 'e'.toByte()
+private const val CHAR_E = 'E'.toByte()
+private const val CHAR_u = 'u'.toByte()
+private const val CHAR_A = 'A'.toByte()
+private const val CHAR_F = 'F'.toByte()
+private const val CHAR_EOF = (-1).toByte()
+private const val CHAR_COMMA = ','.toByte()
+private const val CHAR_0 = '0'.toByte()
+private const val CHAR_1 = '1'.toByte()
+private const val CHAR_2 = '2'.toByte()
+private const val CHAR_3 = '3'.toByte()
+private const val CHAR_4 = '4'.toByte()
+private const val CHAR_5 = '5'.toByte()
+private const val CHAR_6 = '6'.toByte()
+private const val CHAR_7 = '7'.toByte()
+private const val CHAR_8 = '8'.toByte()
+private const val CHAR_9 = '9'.toByte()
+private const val CHAR_MINUS = '-'.toByte()
+private const val CHAR_PLUS = '+'.toByte()
+private const val CHAR_DOT = '.'.toByte()
+
+// This template utilizes the One Definition Rule to create global arrays in a
+// header. As seen in:
+// https://github.com/chadaustin/sajson/blob/master/include/sajson.h
+// bit 0 (1) - set if: plain ASCII string character
+// bit 1 (2) - set if: whitespace
+// bit 4 (0x10) - set if: 0-9 e E .
+private val parseFlags = byteArrayOf(
+// 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 2, 0, 0, // 0
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1
+  3, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0x11, 1, // 2
+  0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 1, 1, 1, 1, 1, 1, // 3
+  1, 1, 1, 1, 1, 0x11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 5
+  1, 1, 1, 1, 1, 0x11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7
+
+  // 128-255
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+)
index 99297e6..4b02cc5 100644 (file)
@@ -338,6 +338,8 @@ public object Utf8 {
     // Designed to take advantage of
     // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination
 
+    if (utf16Length == 0)
+      return 0
     var cc: Char = input[i]
     while (i < utf16Length && i + j < limit && input[i].also { cc = it }.toInt() < 0x80) {
       out[j + i] = cc.toByte()
index f5aa0e4..71820b6 100644 (file)
@@ -210,7 +210,7 @@ class FlexBuffersTest {
     val builder = FlexBuffersBuilder(shareFlag = FlexBuffersBuilder.SHARE_KEYS_AND_STRINGS)
     builder.putVector {
       put(10)
-      builder.putMap {
+      putMap {
         this["chello"] = "world"
         this["aint"] = 10
         this["bfloat"] = 12.3
diff --git a/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/JSONTest.kt b/kotlin/flatbuffers-kotlin/src/commonTest/kotlin/com/google/flatbuffers/kotlin/JSONTest.kt
new file mode 100644 (file)
index 0000000..16039e8
--- /dev/null
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2021 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.flatbuffers.kotlin
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class JSONTest {
+
+  @Test
+  fun parse2Test() {
+    val dataStr = """
+      { "myKey" : [1, "yay"] }
+    """.trimIndent()
+    val data = dataStr.encodeToByteArray()
+    val buffer = ArrayReadWriteBuffer(data, writePosition = data.size)
+    val parser = JSONParser()
+    val root = parser.parse(buffer)
+    println(root.toJson())
+  }
+
+  @Test
+  fun parseSample() {
+    val dataStr = """
+      {
+        "ary" : [1, 2, 3],
+        "boolean_false": false,
+"boolean_true": true, "double": 1.2E33,
+        "hello":"world"
+   ,"interesting": "value",
+
+ "null_value":  null,
+
+
+  "object" : {
+    "field1": "hello"
+  }
+ }
+    """
+    val data = dataStr.encodeToByteArray()
+    val root = JSONParser().parse(ArrayReadWriteBuffer(data, writePosition = data.size))
+    println(root.toJson())
+    val map = root.toMap()
+
+    val minified = data.filterNot { it == ' '.toByte() || it == '\n'.toByte() }.toByteArray().decodeToString()
+    assertEquals(8, map.size)
+    assertEquals("world", map["hello"].toString())
+    assertEquals("value", map["interesting"].toString())
+    assertEquals(12e32, map["double"].toDouble())
+    assertArrayEquals(intArrayOf(1, 2, 3), map["ary"].toIntArray())
+    assertEquals(true, map["boolean_true"].toBoolean())
+    assertEquals(false, map["boolean_false"].toBoolean())
+    assertEquals(true, map["null_value"].isNull)
+    assertEquals("hello", map["object"]["field1"].toString())
+
+    val obj = map["object"]
+    assertEquals(true, obj.isMap)
+    assertEquals("{\"field1\":\"hello\"}", obj.toJson())
+    assertEquals(minified, root.toJson())
+  }
+
+  @Test
+  fun testDoubles() {
+    val values = arrayOf(
+      "-0.0",
+      "1.0",
+      "1.7976931348613157",
+      "0.0",
+      "-0.5",
+      "3.141592653589793",
+      "2.718281828459045E-3",
+      "2.2250738585072014E-308",
+      "4.9E-15",
+    )
+    val parser = JSONParser()
+    assertEquals(-0.0, parser.parse(values[0]).toDouble())
+    assertEquals(1.0, parser.parse(values[1]).toDouble())
+    assertEquals(1.7976931348613157, parser.parse(values[2]).toDouble())
+    assertEquals(0.0, parser.parse(values[3]).toDouble())
+    assertEquals(-0.5, parser.parse(values[4]).toDouble())
+    assertEquals(3.141592653589793, parser.parse(values[5]).toDouble())
+    assertEquals(2.718281828459045e-3, parser.parse(values[6]).toDouble())
+    assertEquals(2.2250738585072014E-308, parser.parse(values[7]).toDouble())
+    assertEquals(4.9E-15, parser.parse(values[8]).toDouble())
+  }
+
+  @Test
+  fun testInts() {
+    val values = arrayOf(
+      "-0",
+      "0",
+      "-1",
+      "${Int.MAX_VALUE}",
+      "${Int.MIN_VALUE}",
+      "${Long.MAX_VALUE}",
+      "${Long.MIN_VALUE}",
+    )
+    val parser = JSONParser()
+
+    assertEquals(parser.parse(values[0]).toInt(), 0)
+    assertEquals(parser.parse(values[1]).toInt(), 0)
+    assertEquals(parser.parse(values[2]).toInt(), -1)
+    assertEquals(parser.parse(values[3]).toInt(), Int.MAX_VALUE)
+    assertEquals(parser.parse(values[4]).toInt(), Int.MIN_VALUE)
+    assertEquals(parser.parse(values[5]).toLong(), Long.MAX_VALUE)
+    assertEquals(parser.parse(values[6]).toLong(), Long.MIN_VALUE)
+  }
+
+  @Test
+  fun testBooleansAndNull() {
+    val values = arrayOf(
+      "true",
+      "false",
+      "null"
+    )
+    val parser = JSONParser()
+
+    assertEquals(true, parser.parse(values[0]).toBoolean())
+    assertEquals(false, parser.parse(values[1]).toBoolean())
+    assertEquals(true, parser.parse(values[2]).isNull)
+  }
+
+  @Test
+  fun testStrings() {
+    val values = arrayOf(
+      "\"\"",
+      "\"a\"",
+      "\"hello world\"",
+      "\"\\\"\\\\\\/\\b\\f\\n\\r\\t cool\"",
+      "\"\\u0000\"",
+      "\"\\u0021\"",
+      "\"hell\\u24AC\\n\\ro wor \\u0021 ld\"",
+      "\"\\/_\\\\_\\\"_\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\n\\r\\t`1~!@#\$%^&*()_+-=[]{}|;:',./<>?\"",
+    )
+    val parser = JSONParser()
+
+    // empty
+    var ref = parser.parse(values[0])
+    assertEquals(true, ref.isString)
+    assertEquals("", ref.toString())
+    // a
+    ref = parser.parse(values[1])
+    assertEquals(true, ref.isString)
+    assertEquals("a", ref.toString())
+    // hello world
+    ref = parser.parse(values[2])
+    assertEquals(true, ref.isString)
+    assertEquals("hello world", ref.toString())
+    // "\\\"\\\\\\/\\b\\f\\n\\r\\t\""
+    ref = parser.parse(values[3])
+    assertEquals(true, ref.isString)
+    assertEquals("\"\\/\b${12.toChar()}\n\r\t cool", ref.toString())
+    // 0
+    ref = parser.parse(values[4])
+    assertEquals(true, ref.isString)
+    assertEquals(0.toChar().toString(), ref.toString())
+    // u0021
+    ref = parser.parse(values[5])
+    assertEquals(true, ref.isString)
+    assertEquals(0x21.toChar().toString(), ref.toString())
+    // "\"hell\\u24AC\\n\\ro wor \\u0021 ld\"",
+    ref = parser.parse(values[6])
+    assertEquals(true, ref.isString)
+    assertEquals("hell${0x24AC.toChar()}\n\ro wor ${0x21.toChar()} ld", ref.toString())
+
+    ref = parser.parse(values[7])
+    println(ref.toJson())
+    assertEquals(true, ref.isString)
+    assertEquals("/_\\_\"_쫾몾ꮘﳞ볚\b\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?", ref.toString())
+  }
+
+  @Test
+  fun testUnicode() {
+    // took from test/unicode_test.json
+    val data = """
+      {
+        "name": "unicode_test",
+        "testarrayofstring": [
+          "Цлїςσδε",
+          "フムアムカモケモ",
+          "フムヤムカモケモ",
+          "㊀㊁㊂㊃㊄",
+          "☳☶☲",
+          "𡇙𝌆"
+        ],
+        "testarrayoftables": [
+          {
+            "name": "Цлїςσδε"
+          },
+          {
+            "name": "☳☶☲"
+          },
+          {
+            "name": "フムヤムカモケモ"
+          },
+          {
+            "name": "㊀㊁㊂㊃㊄"
+          },
+          {
+            "name": "フムアムカモケモ"
+          },
+          {
+            "name": "𡇙𝌆"
+          }
+        ]
+      }
+    """.trimIndent()
+    val parser = JSONParser()
+    val ref = parser.parse(data)
+
+    // name
+    assertEquals(3, ref.toMap().size)
+    assertEquals("unicode_test", ref["name"].toString())
+    // testarrayofstring
+    assertEquals(6, ref["testarrayofstring"].toVector().size)
+    assertEquals("Цлїςσδε", ref["testarrayofstring"][0].toString())
+    assertEquals("フムアムカモケモ", ref["testarrayofstring"][1].toString())
+    assertEquals("フムヤムカモケモ", ref["testarrayofstring"][2].toString())
+    assertEquals("㊀㊁㊂㊃㊄", ref["testarrayofstring"][3].toString())
+    assertEquals("☳☶☲", ref["testarrayofstring"][4].toString())
+    assertEquals("𡇙𝌆", ref["testarrayofstring"][5].toString())
+    // testarrayoftables
+    assertEquals(6, ref["testarrayoftables"].toVector().size)
+    assertEquals("Цлїςσδε", ref["testarrayoftables"][0]["name"].toString())
+    assertEquals("☳☶☲", ref["testarrayoftables"][1]["name"].toString())
+    assertEquals("フムヤムカモケモ", ref["testarrayoftables"][2]["name"].toString())
+    assertEquals("㊀㊁㊂㊃㊄", ref["testarrayoftables"][3]["name"].toString())
+    assertEquals("フムアムカモケモ", ref["testarrayoftables"][4]["name"].toString())
+    assertEquals("𡇙𝌆", ref["testarrayoftables"][5]["name"].toString())
+  }
+
+  @Test
+  fun testArrays() {
+    val values = arrayOf(
+      "[]",
+      "[1]",
+      "[0,1, 2,3  , 4 ]",
+      "[1.0, 2.2250738585072014E-308,  4.9E-320]",
+      "[1.0, 2,  \"hello world\"]   ",
+      "[ 1.1, 2, [ \"hello\" ] ]",
+      "[[[1]]]"
+    )
+    val parser = JSONParser()
+
+    // empty
+    var ref = parser.parse(values[0])
+    assertEquals(true, ref.isVector)
+    assertEquals(0, parser.parse(values[0]).toVector().size)
+    // single
+    ref = parser.parse(values[1])
+    assertEquals(true, ref.isTypedVector)
+    assertEquals(1, ref[0].toInt())
+    // ints
+    ref = parser.parse(values[2])
+    assertEquals(true, ref.isTypedVector)
+    assertEquals(T_VECTOR_INT, ref.type)
+    assertEquals(5, ref.toVector().size)
+    for (i in 0..4) {
+      assertEquals(i, ref[i].toInt())
+    }
+    // floats
+    ref = parser.parse(values[3])
+    assertEquals(true, ref.isTypedVector)
+    assertEquals(T_VECTOR_FLOAT, ref.type)
+    assertEquals(3, ref.toVector().size)
+    assertEquals(1.0, ref[0].toDouble())
+    assertEquals(2.2250738585072014E-308, ref[1].toDouble())
+    assertEquals(4.9E-320, ref[2].toDouble())
+    // mixed
+    ref = parser.parse(values[4])
+    assertEquals(false, ref.isTypedVector)
+    assertEquals(T_VECTOR, ref.type)
+    assertEquals(1.0, ref[0].toDouble())
+    assertEquals(2, ref[1].toInt())
+    assertEquals("hello world", ref[2].toString())
+    // nester array
+    ref = parser.parse(values[5])
+    assertEquals(false, ref.isTypedVector)
+    assertEquals(T_VECTOR, ref.type)
+    assertEquals(1.1, ref[0].toDouble())
+    assertEquals(2, ref[1].toInt())
+    assertEquals("hello", ref[2][0].toString())
+  }
+
+  /**
+   * Several test cases provided by json.org
+   * For more details, see: http://json.org/JSON_checker/, with only
+   * one exception. Single strings are considered accepted, whereas on
+   * the test suit is should fail.
+   */
+  @Test
+  fun testParseMustFail() {
+    val failList = listOf(
+      "[\"Unclosed array\"",
+      "{unquoted_key: \"keys must be quoted\"}",
+      "[\"extra comma\",]",
+      "[\"double extra comma\",,]",
+      "[   , \"<-- missing value\"]",
+      "[\"Comma after the close\"],",
+      "[\"Extra close\"]]",
+      "{\"Extra comma\": true,}",
+      "{\"Extra value after close\": true} \"misplaced quoted value\"",
+      "{\"Illegal expression\": 1 + 2}",
+      "{\"Illegal invocation\": alert()}",
+      "{\"Numbers cannot have leading zeroes\": 013}",
+      "{\"Numbers cannot be hex\": 0x14}",
+      "[\"Illegal backslash escape: \\x15\"]",
+      "[\\naked]",
+      "[\"Illegal backslash escape: \\017\"]",
+      "[[[[[[[[[[[[[[[[[[[[[[[\"Too deep\"]]]]]]]]]]]]]]]]]]]]]]]",
+      "{\"Missing colon\" null}",
+      "{\"Double colon\":: null}",
+      "{\"Comma instead of colon\", null}",
+      "[\"Colon instead of comma\": false]",
+      "[\"Bad value\", truth]",
+      "['single quote']",
+      "[\"\ttab\tcharacter\tin\tstring\t\"]",
+      "[\"tab\\   character\\   in\\  string\\  \"]",
+      "[\"line\nbreak\"]",
+      "[\"line\\\nbreak\"]",
+      "[0e]",
+      "[0e+]",
+      "[0e+-1]",
+      "{\"Comma instead if closing brace\": true,",
+      "[\"mismatch\"}"
+    )
+    for (data in failList) {
+      try {
+        JSONParser().parse(ArrayReadBuffer(data.encodeToByteArray()))
+        assertTrue(false, "SHOULD NOT PASS: $data")
+      } catch (e: IllegalStateException) {
+        println("FAIL $e")
+      }
+    }
+  }
+
+  @Test
+  fun testParseMustPass() {
+    val passList = listOf(
+      "[\n" +
+        "    \"JSON Test Pattern pass1\",\n" +
+        "    {\"object with 1 member\":[\"array with 1 element\"]},\n" +
+        "    {},\n" +
+        "    [],\n" +
+        "    -42,\n" +
+        "    true,\n" +
+        "    false,\n" +
+        "    null,\n" +
+        "    {\n" +
+        "        \"integer\": 1234567890,\n" +
+        "        \"real\": -9876.543210,\n" +
+        "        \"e\": 0.123456789e-12,\n" +
+        "        \"E\": 1.234567890E+34,\n" +
+        "        \"\":  23456789012E66,\n" +
+        "        \"zero\": 0,\n" +
+        "        \"one\": 1,\n" +
+        "        \"space\": \" \",\n" +
+        "        \"quote\": \"\\\"\",\n" +
+        "        \"backslash\": \"\\\\\",\n" +
+        "        \"controls\": \"\\b\\f\\n\\r\\t\",\n" +
+        "        \"slash\": \"/ & \\/\",\n" +
+        "        \"alpha\": \"abcdefghijklmnopqrstuvwyz\",\n" +
+        "        \"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",\n" +
+        "        \"digit\": \"0123456789\",\n" +
+        "        \"0123456789\": \"digit\",\n" +
+        "        \"special\": \"`1~!@#\$%^&*()_+-={':[,]}|;.</>?\",\n" +
+        "        \"hex\": \"\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A\",\n" +
+        "        \"true\": true,\n" +
+        "        \"false\": false,\n" +
+        "        \"null\": null,\n" +
+        "        \"array\":[  ],\n" +
+        "        \"object\":{  },\n" +
+        "        \"address\": \"50 St. James Street\",\n" +
+        "        \"url\": \"http://www.JSON.org/\",\n" +
+        "        \"comment\": \"// /* <!-- --\",\n" +
+        "        \"# -- --> */\": \" \",\n" +
+        "        \" s p a c e d \" :[1,2 , 3\n" +
+        "\n" +
+        ",\n" +
+        "\n" +
+        "4 , 5        ,          6           ,7        ],\"compact\":[1,2,3,4,5,6,7],\n" +
+        "        \"jsontext\": \"{\\\"object with 1 member\\\":[\\\"array with 1 element\\\"]}\",\n" +
+        "        \"quotes\": \"&#34; \\u0022 %22 0x22 034 &#x22;\",\n" +
+        "        \"\\/\\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#\$%^&*()_+-=[]{}|;:',./<>?\"\n" +
+        ": \"A key can be any string\"\n" +
+        "    },\n" +
+        "    0.5 ,98.6\n" +
+        ",\n" +
+        "99.44\n" +
+        ",\n" +
+        "\n" +
+        "1066,\n" +
+        "1e1,\n" +
+        "0.1e1,\n" +
+        "1e-1,\n" +
+        "1e00,2e+00,2e-00\n" +
+        ",\"rosebud\"]",
+      "{\n" +
+        "    \"JSON Test Pattern pass3\": {\n" +
+        "        \"The outermost value\": \"must be an object or array.\",\n" +
+        "        \"In this test\": \"It is an object.\"\n" +
+        "    }\n" +
+        "}",
+      "[[[[[[[[[[[[[[[[[[[\"Not too deep\"]]]]]]]]]]]]]]]]]]]",
+    )
+    for (data in passList) {
+      JSONParser().parse(ArrayReadBuffer(data.encodeToByteArray()))
+    }
+  }
+}