Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Play-json-jsoniter #13

Merged
merged 29 commits into from
Jan 10, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b9dad57
Add play-json-jsoniter subproject
haghard Dec 29, 2019
168a2fb
Add play-json-jsoniter subproject
haghard Dec 29, 2019
c45eb41
Clean up things
haghard Dec 29, 2019
74e56bc
Clean up deps
haghard Dec 29, 2019
d6609cd
1. Add playJsonJsoniter subproject to the root project aggregated set…
haghard Dec 30, 2019
4381d62
Fix a typo
haghard Jan 8, 2020
2a3781b
Improve imports
haghard Jan 8, 2020
09abc0f
Improve TestDataGenerators
haghard Jan 8, 2020
2269831
Since currently there is no build for json-scala-values-generator for…
haghard Jan 8, 2020
aeff208
Clean up
haghard Jan 8, 2020
0083a75
Following standard scala guidelines
haghard Jan 8, 2020
d3a2ec7
Bring back test/scala folder
haghard Jan 8, 2020
fd4b4a7
Switching on java LinkedHashMap for storing pairs
haghard Jan 8, 2020
9bf5a6a
Minor cleanup
haghard Jan 8, 2020
527e5f1
Switched back on mutable.ArrayBuffer
haghard Jan 8, 2020
0c83c71
sbt 1.3.6
haghard Jan 9, 2020
d873091
Fix comments
haghard Jan 9, 2020
7ab45d7
Code review: a)Switch back on java.util.LinkedHashMap b) No more .toV…
haghard Jan 9, 2020
db359fc
Added missing things for previous commit
haghard Jan 9, 2020
518a546
Replace Vector with Array
haghard Jan 9, 2020
ac942f3
PlayJsonJsoniter uses config params to parse numbers form PlayJson li…
haghard Jan 9, 2020
d924ee6
Add more tests
haghard Jan 9, 2020
6bb95fe
Cleanup
haghard Jan 9, 2020
1d3b146
More tests
haghard Jan 9, 2020
1bd477d
Fix typos
haghard Jan 9, 2020
0565e03
Minor changes
haghard Jan 9, 2020
b51444b
Local scoped JsonParserSettings
haghard Jan 9, 2020
ccdb349
Minor changes
haghard Jan 10, 2020
46773e3
Deserialize method returns Try
haghard Jan 10, 2020
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

1. play-json-tools - Set of implicit Play-JSON `Format` helper classes. Example in [FlatFormatSpec](play-json-tools/src/test/scala/com/evolutiongaming/util/FlatFormatSpec.scala)
1. play-json-generic - provides Format derivation for enum like adt's (sealed trait/case objects'). Examples in [EnumerationDerivalSpec](play-json-generic/src/test/scala/com/evolutiongaming/util/generic/EnumerationDerivalSpec.scala)
1. play-json-jsoniter - provides the fastest way to convert an instance of `play.api.libs.json.JsValue` to byte array and read it back.

## Setup

Expand Down
16 changes: 15 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ val commonSettings = Seq(
crossScalaVersions := Seq("2.13.1", "2.12.10"),
)


lazy val root = project
.in(file("."))
.disablePlugins(MimaPlugin)
Expand All @@ -28,6 +27,7 @@ lazy val root = project
.aggregate(
playJsonTools,
playJsonGeneric,
playJsonJsoniter
)


Expand Down Expand Up @@ -55,4 +55,18 @@ lazy val playJsonTools = project
playJson,
nel,
scalaTest % Test,
).map(excludeLog4j)))

lazy val playJsonJsoniter = project
.in(file("play-json-jsoniter"))
.settings(commonSettings)
.settings(Seq(
moduleName := "play-json-jsoniter",
name := "play-json-jsoniter",
libraryDependencies ++= Seq(
playJson,
nel,
jsoniter,
scalaTest % Test,
generator % Test
).map(excludeLog4j)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.evolutiongaming.jsonitertool

import com.github.plokhotnyuk.jsoniter_scala.core._
import play.api.libs.json.{JsArray, JsBoolean, JsFalse, JsNull, JsNumber, JsObject, JsString, JsTrue, JsValue}

object PlayJsonJsoniter {

implicit val jsValueCodec: JsonValueCodec[JsValue] =
new JsonValueCodec[JsValue] {

/**
* The implementation was borrowed from: https://github.com/plokhotnyuk/jsoniter-scala/blob/e80d51019b39efacff9e695de97dce0c23ae9135/jsoniter-scala-benchmark/src/main/scala/io/circe/CirceJsoniter.scala
* and adapted to meet PlayJson criteria.
*/
override def decodeValue(in: JsonReader, default: JsValue): JsValue = {
var b = in.nextToken
haghard marked this conversation as resolved.
Show resolved Hide resolved
if (b == 'n') in.readNullOrError(default, "expected `null` value")
else if (b == '"') {
in.rollbackToken()
JsString(in.readString(null))
} else if (b == 'f' || b == 't') {
in.rollbackToken()
if (in.readBoolean()) JsTrue else JsFalse
} else if (b == '-' || (b >= '0' && b <= '9')) {
JsNumber(
{
in.rollbackToken
haghard marked this conversation as resolved.
Show resolved Hide resolved
in.setMark
haghard marked this conversation as resolved.
Show resolved Hide resolved
try {
do {
b = in.nextByte
} while ((b >= '0' && b <= '9') || b == '-')
} catch {
case _: JsonReaderException => /* ignore end of input error */
} finally in.rollbackToMark
//PlayJson specific thing, since it uses BigDecimal to represent all numbers.
haghard marked this conversation as resolved.
Show resolved Hide resolved
in.readBigDecimal(null)
haghard marked this conversation as resolved.
Show resolved Hide resolved
}
)
} else if (b == '[') {
JsArray(
if (in.isNextToken(']')) Vector.empty[JsValue]
else {
in.rollbackToken()
var i = 0
var arr = new Array[JsValue](4)
do {
if (i == arr.length) arr = java.util.Arrays.copyOf(arr, i << 1)
arr(i) = decodeValue(in, default)
i += 1
} while (in.isNextToken(','))

(if (in.isCurrentToken(']'))
if (i == arr.length) arr else java.util.Arrays.copyOf(arr, i)
else in.arrayEndOrCommaError()).toVector
})
} else if (b == '{') {
new JsObject(
if (in.isNextToken('}')) scala.collection.mutable.Map.empty[String, JsValue]
else {
val underlying = scala.collection.mutable.LinkedHashMap[String, JsValue]()
haghard marked this conversation as resolved.
Show resolved Hide resolved
in.rollbackToken()

do {
underlying.put(in.readKeyAsString(), decodeValue(in, default))
} while (in.isNextToken(','))

if (!in.isCurrentToken('}'))
in.objectEndOrCommaError()

underlying
}
)
} else in.decodeError("expected JSON value")
}

override def encodeValue(jsValue: JsValue, out: JsonWriter): Unit =
jsValue match {
case JsBoolean(b) =>
out.writeVal(b)
case JsString(value) =>
out.writeVal(value)
case JsNumber(value) =>
out.writeVal(value)
case JsArray(items) =>
out.writeArrayStart()
items.foreach(encodeValue(_, out))
out.writeArrayEnd()
case JsObject(kvs) =>
out.writeObjectStart()
kvs.foreach {
haghard marked this conversation as resolved.
Show resolved Hide resolved
case (k, v) =>
out.writeKey(k)
encodeValue(v, out)
}
out.writeObjectEnd()
case JsNull =>
out.writeNull()
}

override val nullValue: JsValue = JsNull
}

/**
* @see See [[com.github.plokhotnyuk.jsoniter_scala.core.writeToArray]]
*/
def serialize(payload: JsValue): Array[Byte] =
writeToArray(payload)

/**
* @see See [[com.github.plokhotnyuk.jsoniter_scala.core.readFromArray]]
*/
def deserialize(bytes: Array[Byte]): JsValue =
readFromArray[JsValue](bytes)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.evolutiongaming.jsonitertool

import com.evolutiongaming.jsonitertool.TestDataGenerators.DataLine
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import play.api.libs.json.{JsSuccess, Json}
import TestDataGenerators._

class JsoniterSpec extends AnyFunSuite with Matchers {

test("Write using PlayJson -> Read using Jsoniter: Compare bites") {
haghard marked this conversation as resolved.
Show resolved Hide resolved

val expected: DataLine = Json.fromJson[DataLine](Json.parse(TestDataGenerators.jsonBody))
.fold(errs => throw new Exception(s"Parsing error: ${errs.mkString(",")}"), identity)

val jsValue = Json.toJson(expected)
val bts0 = PlayJsonJsoniter.serialize(jsValue)
val bts1 = Json.toBytes(jsValue)
java.util.Arrays.compare(bts0, bts1) shouldEqual 0

}

test("Write using PlayJson -> Read using Jsoniter: Compare objects") {

val expected: DataLine = Json.fromJson[DataLine](Json.parse(TestDataGenerators.jsonBody))
.fold(errs => throw new Exception(s"Parsing error: ${errs.mkString(",")}"), identity)

val bts = Json.toBytes(Json.toJson(expected))
val jsValue = PlayJsonJsoniter.deserialize(bts)
val actual = Json.fromJson[DataLine](jsValue)

JsSuccess(expected) shouldEqual actual
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.evolutiongaming.jsonitertool

import com.evolutiongaming.jsonitertool.TestDataGenerators.{Address, DataLine, Friend, User}
import play.api.libs.json.Json

trait PlayJsonImplicits {

implicit val a = Json.reads[Friend]
implicit val b = Json.writes[Friend]

implicit val c = Json.reads[DataLine]
implicit val d = Json.writes[DataLine]

implicit val e = Json.reads[Address]
implicit val f = Json.writes[Address]

implicit val g = Json.reads[User]
implicit val h = Json.writes[User]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.evolutiongaming.jsonitertool

import TestDataGenerators._
import org.scalacheck.Prop._
import play.api.libs.json.Json
import org.scalacheck.{Arbitrary, Gen, Test}

//sbt playJsonJsoniter/test:"runMain com.evolutiongaming.jsonitertool.PlayJsonWithJsoniterBackendSpec"
object PlayJsonWithJsoniterBackendSpec extends org.scalacheck.Properties("PlayJsonWithJsoniterBackend") {
val Size = 200

override def overrideParameters(p: Test.Parameters): Test.Parameters =
p.withMinSuccessfulTests(Size)

implicit def generator: Arbitrary[User] = Arbitrary(genUser)

property("Write using PlayJson -> Read using Jsoniter") = forAll { user: User =>
val jsValue = Json.toJson(user)
val bts = Json.toBytes(jsValue)
//println(s"${line.name}: ${bts.size / 1024} kb")
val actJsValue = PlayJsonJsoniter.deserialize(bts)
user == Json.fromJson[User](actJsValue).get
}

property("Write using PlayJson -> Read using Jsoniter. Batch") = forAll(
Gen.containerOfN[Vector, User](Size, genUser),
) { batch: Vector[User] =>
val bools = batch.map { user =>
val jsValue = Json.toJson(user)
val bts = Json.toBytes(jsValue)
val actJsValue = PlayJsonJsoniter.deserialize(bts)
user == Json.fromJson[User](actJsValue).get
}

bools.find(_ == false).isEmpty
}

property("Write using Jsoniter -> Read using Jsoniter. Batch") = forAll(
Gen.containerOfN[Vector, User](Size, genUser),
) { batch: Vector[User] =>
val bools = batch.map { user =>
val jsValue = Json.toJson(user)
val bts = PlayJsonJsoniter.serialize(jsValue)
val actJsValue = PlayJsonJsoniter.deserialize(bts)
user == Json.fromJson[User](actJsValue).get
}

bools.find(_ == false).isEmpty
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.evolutiongaming.jsonitertool

import org.scalacheck.{Arbitrary, Gen, Test}
import org.scalacheck.Prop.forAll
import play.api.libs.json.Json
import valuegen.RandomJsArrayGen

//sbt playJsonJsoniter/test:"runMain com.evolutiongaming.jsonitertool.RandomJsonArraysSpec"
object RandomJsonArraysSpec extends org.scalacheck.Properties("RandomJsonSpec") {

val Size = 5000

//produces any imaginable Json array
def randomArrayGen: Gen[value.JsArray] = RandomJsArrayGen()

implicit def generator: Arbitrary[value.JsArray] = Arbitrary(randomArrayGen)

override def overrideParameters(p: Test.Parameters): Test.Parameters =
p.withMinSuccessfulTests(Size)

property("Random json arrays") = forAll { array: value.JsArray =>
val json = array.toString
val jsValue = Json.parse(json)
val bts = PlayJsonJsoniter.serialize(jsValue)
val actJsValue = PlayJsonJsoniter.deserialize(bts)
jsValue == actJsValue
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.evolutiongaming.jsonitertool

import org.scalacheck.{Arbitrary, Gen, Test}
import org.scalacheck.Prop.forAll
import play.api.libs.json.Json
import valuegen.RandomJsObjGen

//sbt playJsonJsoniter/test:"runMain com.evolutiongaming.jsonitertool.RandomJsonObjectsSpec"
object RandomJsonObjectsSpec extends org.scalacheck.Properties("RandomJsonObjectsSpec") {

val Size = 5000

//produces any imaginable Json object
def randomObjGen: Gen[value.JsObj] = RandomJsObjGen()

implicit def generator: Arbitrary[value.JsObj] = Arbitrary(randomObjGen)

override def overrideParameters(p: Test.Parameters): Test.Parameters =
p.withMinSuccessfulTests(Size)

property("Random json objects") = forAll { obj: value.JsObj =>
val json = obj.toString
val jsValue = Json.parse(json)
val bts = PlayJsonJsoniter.serialize(jsValue)
val actJsValue = PlayJsonJsoniter.deserialize(bts)
jsValue == actJsValue
}
}

Loading