-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Jsoniter integration
- Loading branch information
Showing
12 changed files
with
467 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
play-json-jsoniter/src/main/scala/com/evolutiongaming/jsonitertool/PlayJsonJsoniter.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package com.evolutiongaming.jsonitertool | ||
|
||
import com.github.plokhotnyuk.jsoniter_scala.core._ | ||
import play.api.libs.json._ | ||
|
||
import scala.collection.IndexedSeq | ||
import scala.util.Try | ||
|
||
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 = { | ||
val b = in.nextToken() | ||
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 >= '0' && b <= '9') || b == '-') { | ||
in.rollbackToken() | ||
val s = JsonParserSettings.settings.bigDecimalParseSettings | ||
JsNumber(in.readBigDecimal(null, s.mathContext, s.scaleLimit, s.digitsLimit)) | ||
} else if (b == '[') { | ||
val array: IndexedSeq[JsValue] = | ||
if (in.isNextToken(']')) new Array[JsValue](0) | ||
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() | ||
} | ||
JsArray(array) | ||
} else if (b == '{') { | ||
/* | ||
* Because of DoS vulnerability in Scala 2.12 HashMap https://github.com/scala/bug/issues/11203 | ||
* we use a Java LinkedHashMap because it better handles hash code collisions for Comparable keys. | ||
*/ | ||
val kvs = | ||
if (in.isNextToken('}')) new java.util.LinkedHashMap[String, JsValue]() | ||
else { | ||
val underlying = new java.util.LinkedHashMap[String, JsValue]() | ||
in.rollbackToken() | ||
do { | ||
underlying.put(in.readKeyAsString(), decodeValue(in, default)) | ||
} while (in.isNextToken(',')) | ||
|
||
if (!in.isCurrentToken('}')) | ||
in.objectEndOrCommaError() | ||
|
||
underlying | ||
} | ||
import scala.jdk.CollectionConverters._ | ||
JsObject(kvs.asScala) | ||
} 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 { | ||
case (k, v) => | ||
out.writeKey(k) | ||
encodeValue(v, out) | ||
} | ||
out.writeObjectEnd() | ||
case JsNull => | ||
out.writeNull() | ||
} | ||
|
||
override val nullValue: JsValue = JsNull | ||
} | ||
|
||
def serialize(payload: JsValue): Array[Byte] = | ||
writeToArray(payload) | ||
|
||
def deserialize(bytes: Array[Byte]): Try[JsValue] = | ||
Try(readFromArray[JsValue](bytes)) | ||
} |
14 changes: 14 additions & 0 deletions
14
...son-jsoniter/src/test/scala-2.13/com/evolutiongaming/jsonitertool/PlayJsonImplicits.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.evolutiongaming.jsonitertool | ||
|
||
import play.api.libs.json.Json | ||
import com.evolutiongaming.jsonitertool.TestDataGenerators.{Address, User} | ||
|
||
trait PlayJsonImplicits { | ||
|
||
implicit val e = Json.reads[Address] | ||
implicit val f = Json.writes[Address] | ||
|
||
implicit val g = Json.reads[User] | ||
implicit val h = Json.writes[User] | ||
|
||
} |
49 changes: 49 additions & 0 deletions
49
...rc/test/scala-2.13/com/evolutiongaming/jsonitertool/PlayJsonWithJsoniterBackendSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package com.evolutiongaming.jsonitertool | ||
|
||
import com.evolutiongaming.jsonitertool.TestDataGenerators.{User, genUser} | ||
import org.scalacheck.Prop.forAll | ||
import org.scalacheck.{Arbitrary, Gen, Test} | ||
import play.api.libs.json.{JsSuccess, Json} | ||
|
||
//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) | ||
val actJsValue = PlayJsonJsoniter.deserialize(bts).map(Json.fromJson[User](_)) | ||
JsSuccess(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).map(Json.fromJson[User](_)).get | ||
JsSuccess(user) == actJsValue | ||
} | ||
|
||
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).map(Json.fromJson[User](_)) | ||
JsSuccess(user) == actJsValue.get | ||
} | ||
|
||
bools.find(_ == false).isEmpty | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...-jsoniter/src/test/scala-2.13/com/evolutiongaming/jsonitertool/RandomJsonArraysSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.evolutiongaming.jsonitertool | ||
|
||
import org.scalacheck.Prop.forAll | ||
import org.scalacheck.{Arbitrary, Gen, Test} | ||
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.get | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...jsoniter/src/test/scala-2.13/com/evolutiongaming/jsonitertool/RandomJsonObjectsSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.evolutiongaming.jsonitertool | ||
|
||
import org.scalacheck.Prop.forAll | ||
import org.scalacheck.{Arbitrary, Gen, Test} | ||
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.get | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
...on-jsoniter/src/test/scala-2.13/com/evolutiongaming/jsonitertool/TestDataGenerators.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package com.evolutiongaming.jsonitertool | ||
|
||
import play.api.libs.json.Json | ||
import valuegen.Preamble._ | ||
import valuegen.{JsArrayGen, JsObjGen} | ||
import value.JsObj | ||
import org.scalacheck.{Arbitrary, Gen} | ||
|
||
object TestDataGenerators extends PlayJsonImplicits { | ||
|
||
val ALPHABET: Vector[String] = "abcdefghijklmnopqrstuvwzyz".split("").toVector | ||
|
||
def str(len: Int): Gen[String] = Gen.listOfN(len, Gen.choose(0, ALPHABET.size - 1).map(ALPHABET(_))).map(_.mkString("")) | ||
|
||
def flagGen: Gen[Boolean] = Arbitrary.arbitrary[Boolean] | ||
|
||
def nameGen: Gen[String] = Gen.choose(5, 20).flatMap(str) | ||
|
||
def birthDateGen: Gen[String] = Gen.choose(5, 20).flatMap(str) | ||
|
||
def latitudeGen: Gen[Double] = Arbitrary.arbitrary[Double] | ||
|
||
def longitudeGen: Gen[Double] = Arbitrary.arbitrary[Double] | ||
|
||
def longGen: Gen[Long] = Arbitrary.arbitrary[Long] | ||
|
||
def ints: Gen[Int] = Arbitrary.arbitrary[Int] | ||
|
||
def emailGen: Gen[String] = Gen.choose(5, 20).flatMap(str) | ||
|
||
def countryGen: Gen[String] = Gen.choose(5, 20).flatMap(str) | ||
|
||
def friendGen(depth: Int): Gen[JsObj] = JsObjGen( | ||
"name" -> nameGen, | ||
"email" -> emailGen, | ||
"flag" -> flagGen, | ||
"l" -> longGen, | ||
"from" -> JsObjGen("country" -> countryGen, "doubles" -> JsArrayGen(latitudeGen, longitudeGen)), | ||
"metrics" -> JsArrayGen(ints, ints), | ||
"friends" -> (if (depth > 0) JsArrayGen(friendGen(depth - 1)) else JsArrayGen()) | ||
) | ||
|
||
def objGen: Gen[JsObj] = JsObjGen( | ||
"name" -> nameGen, | ||
"email" -> emailGen, | ||
"flag" -> flagGen, | ||
"l" -> longGen, | ||
"from" -> JsObjGen("country" -> countryGen, "doubles" -> JsArrayGen(latitudeGen, longitudeGen)), | ||
"metrics" -> JsArrayGen(ints, ints, ints, ints, ints), | ||
"friends" -> JsArrayGen(friendGen(depth = 50)) | ||
) | ||
|
||
def genUser: Gen[User] = objGen | ||
.map(json => Json.fromJson[User](Json.parse(json.toString)).get) | ||
|
||
case class Address(country: String, doubles: List[Double]) | ||
|
||
case class User(name: String, email: String, flag: Boolean, from: Address, l: Long, metrics: List[Int], | ||
friends: List[User]) | ||
} |
Oops, something went wrong.