From 060184f038f558999383b83b0f63e5c6237c821a Mon Sep 17 00:00:00 2001 From: Doug Roper Date: Sun, 19 Dec 2021 01:31:57 -0500 Subject: [PATCH] Add weejson-jsoniter-scala --- build.sbt | 16 ++ .../FromJsoniterScala.scala | 54 ++++++ .../JsonWriterVisitor.scala | 142 ++++++++++++++ .../WeePickleJsonValueCodecs.scala | 178 ++++++++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/FromJsoniterScala.scala create mode 100644 weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/JsonWriterVisitor.scala create mode 100644 weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/WeePickleJsonValueCodecs.scala diff --git a/build.sbt b/build.sbt index 91d97375..0ce9bb0c 100644 --- a/build.sbt +++ b/build.sbt @@ -10,6 +10,7 @@ lazy val bench = project .dependsOn( `weepickle-tests` % "compile;test", `weejson-upickle`, + `weejson-jsoniter-scala`, ) .enablePlugins(JmhPlugin) .settings( @@ -137,6 +138,21 @@ lazy val `weejson-jackson` = project ) ) +/** + * A very fast JSON parser. + * + * Uses MiMa, but not shaded. Package naming strategy across major versions is unknown. + * + * @see https://github.com/plokhotnyuk/jsoniter-scala + */ +lazy val `weejson-jsoniter-scala` = project + .dependsOn(`weepickle-core`) + .settings( + libraryDependencies ++= Seq( + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.12.0" + ), + ) + lazy val `weejson-circe` = project .dependsOn(weejson) .settings( diff --git a/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/FromJsoniterScala.scala b/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/FromJsoniterScala.scala new file mode 100644 index 00000000..daa26b60 --- /dev/null +++ b/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/FromJsoniterScala.scala @@ -0,0 +1,54 @@ +package com.rallyhealth.weejson.v1.wee_jsoniter_scala + +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.rallyhealth.weejson.v1.wee_jsoniter_scala.WeePickleJsonValueCodecs.VisitorJsonValueEncoder +import com.rallyhealth.weepickle.v1.core.{FromInput, Visitor} + +import java.io.InputStream +import java.nio.ByteBuffer + +/** + * A very fast UTF-8-only JSON parser. + * + * This integration: + * - tracks paths and returns as JsonPointer + * - does not deduplicate strings + * - throws below a fixed depth limit + * + * @see https://github.com/plokhotnyuk/jsoniter-scala + */ +object FromJsoniterScala extends FromJsoniterScala(ReaderConfig) + +class FromJsoniterScala(config: ReaderConfig) { + + def apply( + bytes: Array[Byte] + ): FromInput = new FromInput { + + override def transform[T]( + to: Visitor[_, T] + ): T = readFromArray(bytes, config)(readerCodec(to)) + } + + def apply( + in: InputStream + ): FromInput = new FromInput { + + override def transform[T]( + to: Visitor[_, T] + ): T = readFromStream(in, config)(readerCodec(to)) + } + + def apply( + buf: ByteBuffer + ): FromInput = new FromInput { + + override def transform[T]( + to: Visitor[_, T] + ): T = readFromByteBuffer(buf, config)(readerCodec(to)) + } + + private def readerCodec[J]( + v: Visitor[_, J] + ): JsonValueCodec[J] = new VisitorJsonValueEncoder[J](v, maxDepth = 64) +} diff --git a/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/JsonWriterVisitor.scala b/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/JsonWriterVisitor.scala new file mode 100644 index 00000000..40f18bcd --- /dev/null +++ b/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/JsonWriterVisitor.scala @@ -0,0 +1,142 @@ +package com.rallyhealth.weejson.v1.wee_jsoniter_scala + +import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter +import com.rallyhealth.weepickle.v1.core._ + +import java.time.Instant + +class JsonWriterVisitor( + writer: JsonWriter +) extends JsVisitor[Any, JsonWriter] { + + private[this] val arrVisitor = new ArrVisitor[Any, JsonWriter] { + override def subVisitor: Visitor[_, _] = JsonWriterVisitor.this + + override def visitValue( + v: Any + ): Unit = () + + override def visitEnd(): JsonWriter = { + writer.writeArrayEnd() + writer + } + } + + private[this] val objVisitor = new ObjVisitor[Any, JsonWriter] { + writer.writeObjectStart() + override def visitKey(): Visitor[_, _] = StringVisitor + + override def visitKeyValue( + v: Any + ): Unit = writer.writeKey(v.toString) + + override def subVisitor: Visitor[_, _] = JsonWriterVisitor.this + + override def visitValue( + v: Any + ): Unit = () + + override def visitEnd(): JsonWriter = { + writer.writeObjectEnd() + writer + } + } + + override def visitArray( + length: Int + ): ArrVisitor[Any, JsonWriter] = arrVisitor + + override def visitObject( + length: Int + ): ObjVisitor[Any, JsonWriter] = objVisitor + + override def visitNull(): JsonWriter = { + writer.writeNull() + writer + } + + override def visitFalse(): JsonWriter = { + writer.writeVal(false) + writer + } + + override def visitTrue(): JsonWriter = { + writer.writeVal(true) + writer + } + + override def visitFloat64StringParts( + cs: CharSequence, + decIndex: Int, + expIndex: Int + ): JsonWriter = { + writer.writeNonEscapedAsciiVal(cs.toString) + writer + } + + override def visitFloat64( + d: Double + ): JsonWriter = { + writer.writeVal(d) + writer + } + + override def visitFloat32( + d: Float + ): JsonWriter = { + writer.writeVal(d) + writer + } + + override def visitInt32( + i: Int + ): JsonWriter = { + writer.writeVal(i) + writer + } + + override def visitInt64( + l: Long + ): JsonWriter = { + writer.writeVal(l) + writer + } + + override def visitFloat64String( + s: String + ): JsonWriter = { + writer.writeNonEscapedAsciiVal(s) + writer + } + + override def visitString( + cs: CharSequence + ): JsonWriter = { + writer.writeVal(cs.toString) + writer + } + + override def visitChar( + c: Char + ): JsonWriter = { + writer.writeVal(c) + writer + } + + override def visitTimestamp( + instant: Instant + ): JsonWriter = { + writer.writeVal(instant) + writer + } + + override def visitBinary( + bytes: Array[Byte], + offset: Int, + len: Int + ): JsonWriter = { + val trimmed = if (bytes.length != len) bytes.take(len) else bytes + writer.writeBase64Val(trimmed, true) + writer + } +} diff --git a/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/WeePickleJsonValueCodecs.scala b/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/WeePickleJsonValueCodecs.scala new file mode 100644 index 00000000..31df9149 --- /dev/null +++ b/weejson-jsoniter-scala/src/main/scala/com/rallyhealth/weejson/v1/wee_jsoniter_scala/WeePickleJsonValueCodecs.scala @@ -0,0 +1,178 @@ +package com.rallyhealth.weejson.v1.wee_jsoniter_scala + +import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReader, JsonReaderException, JsonValueCodec, JsonWriter} +import com.rallyhealth.weepickle.v1.core.JsonPointerVisitor.JsonPointerException +import com.rallyhealth.weepickle.v1.core.{FromInput, Types, Visitor} + +import java.nio.charset.StandardCharsets +import scala.util.control.{NoStackTrace, NonFatal} + +object WeePickleJsonValueCodecs { + + implicit object FromInputJsonValueEncoder extends JsonValueCodec[FromInput] { + + override def decodeValue( + in: JsonReader, + default: FromInput + ): FromInput = throw new UnsupportedOperationException( + "only supports encoding" + ) + + override def encodeValue( + fromInput: FromInput, + out: JsonWriter + ): Unit = { + fromInput.transform(new JsonWriterVisitor(out)) + } + + override def nullValue: FromInput = null + } + + private case class JsonPathParts( + pointer: List[String], + cause: Throwable + ) extends RuntimeException(cause) + with NoStackTrace { + + override def getMessage: String = pointer.mkString("/") + } + + class VisitorJsonValueEncoder[J]( + v: Visitor[_, J], + maxDepth: Int = 64 + ) extends JsonValueCodec[J] { + + override def decodeValue( + in: JsonReader, + default: J + ): J = { + try { + decodeValue(in, v, maxDepth) + } catch { + case JsonPathParts(pointer, cause) => + val jsonPointer = pointer.view + .map(_.replace("~", "~0").replace("/", "~1")) + .mkString("/", "/", "") + throw new JsonPointerException(jsonPointer, cause) + case NonFatal(t) => + throw new JsonPointerException("", t) + } + } + + private def decodeValue[TT, JJ]( + in: JsonReader, + v: Visitor[_, JJ], + depth: Int + ): JJ = { + val b = in.nextToken() + + if (b == '"') { + in.rollbackToken() + v.visitString(in.readString(null)) + } else if (b == 't' || b == 'f') { + in.rollbackToken() + if (in.readBoolean()) v.visitTrue() else v.visitFalse() + } else if ((b >= '0' && b <= '9') || b == '-') { + in.rollbackToken() + parseNumber(in, v) + } else if (b == '[') { + val depthM1 = depth - 1 + if (depthM1 < 0) in.decodeError("depth limit exceeded") + val isEmpty = in.isNextToken(']') + var i = 0 + val arr = v.visitArray(if (isEmpty) 0 else -1).narrow + try { + if (!isEmpty) { + in.rollbackToken() + while ({ + arr.visitValue(decodeValue(in, arr.subVisitor, depthM1)) + i += 1 + in.isNextToken(',') + }) () + if (!in.isCurrentToken(']')) in.arrayEndOrCommaError() + } + } catch { + case NonFatal(t) => prependFailurePathInfo(t, String.valueOf(i)) + } + arr.visitEnd() + } else if (b == '{') { + val depthM1 = depth - 1 + if (depthM1 < 0) in.decodeError("depth limit exceeded") + val isEmpty = in.isNextToken('}') + val obj = v.visitObject(if (isEmpty) 0 else -1).narrow + if (!isEmpty) { + in.rollbackToken() + var key: String = "?" + try { + while ( { + key = in.readKeyAsString() + obj.visitKeyValue(obj.visitKey().visitString(key)) + obj.visitValue(decodeValue(in, obj.subVisitor, depthM1)) + in.isNextToken(',') + }) () + if (!in.isCurrentToken('}')) in.objectEndOrCommaError() + } catch { + case NonFatal(t) => prependFailurePathInfo(t, key) + } + } + obj.visitEnd() + } else { + in.readNullOrError(v.visitNull(), "expected `null` value") + } + } + + private def parseNumber[J]( + in: JsonReader, + v: Visitor[_, J] + ): J = { + in.setMark() + var digits = 0 + var b = in.nextByte() + if (b == '-') b = in.nextByte() + try { + while (b >= '0' && b <= '9') { + b = in.nextByte() + digits += 1 + } + } catch { + case _: JsonReaderException => // ignore the end of input error for now + } + finally in.rollbackToMark() + + if ((b | 0x20) != 'e' && b != '.') { + if (digits < 19) { + val l = in.readLong() + val i = l.toInt + if (l == i) v.visitInt32(i) + else v.visitInt64(l) + } else { + // TODO what happens with `123foobar`? + val str = new String(in.readRawValAsBytes(), StandardCharsets.US_ASCII) + v.visitFloat64StringParts(str, -1, -1) + } + } else { + // TODO find decIndex and expIndex for visitFloat64StringParts(). We're already close. + val str = new String(in.readRawValAsBytes(), StandardCharsets.US_ASCII) + v.visitFloat64String(str) + } + } + + private def prependFailurePathInfo( + t: Throwable, + pathSegment: String + ): Nothing = t match { + case JsonPathParts(pointer, cause) => + throw JsonPathParts(pathSegment :: pointer, cause) + case other: Throwable => throw JsonPathParts(pathSegment :: Nil, other) + } + + override def nullValue: J = null.asInstanceOf[J] // unused + + override def encodeValue( + x: J, + out: JsonWriter + ): Unit = { + throw new UnsupportedOperationException("only supports decoding") + } + } +}