Skip to content

Commit

Permalink
Merge pull request #610 from AVSystem/nativejs-inout
Browse files Browse the repository at this point in the history
NativeJsonInput/Output with custom format
  • Loading branch information
sebaciv authored Aug 5, 2024
2 parents 80a2d81 + 16a3166 commit 14d430b
Show file tree
Hide file tree
Showing 8 changed files with 478 additions and 8 deletions.
4 changes: 4 additions & 0 deletions benchmark/js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
How to run benchmark:
- compile `sbt commons-benchmark-js/fullOptJS`
- open `fullopt-2.13.html` file in a browser
- select test suite and run
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.avsystem.commons
package ser

import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput}
import com.avsystem.commons.serialization.nativejs.{NativeJsonInput, NativeJsonOutput}
import io.circe.parser._
import io.circe.syntax._
import japgolly.scalajs.benchmark.gui.GuiSuite
Expand All @@ -10,62 +11,86 @@ import japgolly.scalajs.benchmark.{Benchmark, Suite}
object JsonBenchmarks {
val suite = GuiSuite(
Suite("JSON serialization benchmarks")(
Benchmark("Writing case class: GenCodec") {
Benchmark("Writing case class: GenCodec, String Json format") {
JsonStringOutput.write(Something.Example)
},
Benchmark("Writing case class: GenCodec, Native Json format") {
NativeJsonOutput.writeAsString(Something.Example)
},
Benchmark("Writing case class: Circe") {
Something.Example.asJson.noSpaces
},
Benchmark("Writing case class: uPickle") {
upickle.default.write(Something.Example)
},
Benchmark("Reading case class: GenCodec") {
Benchmark("Reading case class: GenCodec, String Json format") {
JsonStringInput.read[Something](Something.ExampleJsonString)
},
Benchmark("Reading case class: GenCodec, Native Json format") {
NativeJsonInput.readString[Something](Something.ExampleJsonString)
},
Benchmark("Reading case class: Circe") {
decode[Something](Something.ExampleJsonString).fold(e => throw e, identity)
},
Benchmark("Reading case class: uPickle") {
upickle.default.read[Something](Something.ExampleJsonString)
},

Benchmark("Writing sealed hierarchy: GenCodec") {
Benchmark("Writing sealed hierarchy: GenCodec, String Json format") {
JsonStringOutput.write(SealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: GenCodec (flat)") {
Benchmark("Writing sealed hierarchy: GenCodec (flat), String Json format") {
JsonStringOutput.write(FlatSealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: GenCodec, Native Json format") {
NativeJsonOutput.writeAsString(SealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: GenCodec (flat), Native Json format") {
NativeJsonOutput.writeAsString(FlatSealedStuff.ExampleList)
},
Benchmark("Writing sealed hierarchy: Circe") {
SealedStuff.ExampleList.asJson.noSpaces
},
Benchmark("Writing sealed hierarchy: uPickle") {
upickle.default.write(SealedStuff.ExampleList)
},
Benchmark("Reading sealed hierarchy: GenCodec") {
Benchmark("Reading sealed hierarchy: GenCodec, String Json format") {
JsonStringInput.read[List[SealedStuff]](SealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: GenCodec (flat)") {
Benchmark("Reading sealed hierarchy: GenCodec (flat), String Json format") {
JsonStringInput.read[List[FlatSealedStuff]](FlatSealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: GenCodec, Native Json format") {
NativeJsonInput.readString[List[SealedStuff]](SealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: GenCodec (flat), Native Json format") {
NativeJsonInput.readString[List[FlatSealedStuff]](FlatSealedStuff.ExampleJsonString)
},
Benchmark("Reading sealed hierarchy: Circe") {
decode[List[SealedStuff]](SealedStuff.ExampleJsonString).fold(e => throw e, identity)
},
Benchmark("Reading sealed hierarchy: uPickle") {
upickle.default.read[List[SealedStuff]](SealedStuff.ExampleUpickleJsonString)
},

Benchmark("Writing foos: GenCodec") {
Benchmark("Writing foos: GenCodec, String Json format") {
JsonStringOutput.write(Foo.ExampleMap)
},
Benchmark("Writing foos: GenCodec, Native Json format") {
NativeJsonOutput.writeAsString(Foo.ExampleMap)
},
Benchmark("Writing foos: Circe") {
Foo.ExampleMap.asJson.noSpaces
},
Benchmark("Writing foos: uPickle") {
upickle.default.write(Foo.ExampleMap)
},
Benchmark("Reading foos: GenCodec") {
Benchmark("Reading foos: GenCodec, String Json format") {
JsonStringInput.read[Map[String, Foo]](Foo.ExampleJsonString)
},
Benchmark("Reading foos: GenCodec with Native Json format") {
NativeJsonInput.readString[Map[String, Foo]](Foo.ExampleJsonString)
},
Benchmark("Reading foos: Circe") {
decode[Map[String, Foo]](Foo.ExampleJsonString).fold(e => throw e, identity)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.avsystem.commons
package serialization.nativejs

import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx}

/**
* Specifies format used by `NativeJsonOutput.writeLong` / `NativeJsonInput.readLong`
* to represent [[Long]]. JS does not support 64-bit representation.
*/
final class NativeLongFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
object NativeLongFormat extends AbstractValueEnumCompanion[NativeLongFormat] {
final val RawString: Value = new NativeLongFormat
final val JsNumber: Value = new NativeLongFormat
final val JsBigInt: Value = new NativeLongFormat
}

/**
* Specifies format used by `NativeJsonOutput.writeTimestamp` / `NativeJsonInput.readTimestamp`
* to represent timestamps.
*/
final class NativeDateFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
object NativeDateFormat extends AbstractValueEnumCompanion[NativeDateFormat] {
final val RawString: Value = new NativeDateFormat
final val JsNumber: Value = new NativeDateFormat
final val JsDate: Value = new NativeDateFormat
}

/**
* Specifies format used by `NativeJsonOutput.writeBigInt` / `NativeJsonInput.readBigInt`
* to represent [[BigInt]].
*
* Note that [[scala.scalajs.js.JSON.stringify]] does not know how to serialize a BigInt and throws an error
*/
final class NativeBigIntFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
object NativeBigIntFormat extends AbstractValueEnumCompanion[NativeBigIntFormat] {
final val RawString: Value = new NativeBigIntFormat
final val JsBigInt: Value = new NativeBigIntFormat
}

/**
* Adjusts format produced by [[NativeJsonOutput]].
*
* @param longFormat format used to [[Long]]
* @param dateFormat format used to represent timestamps
* @param bigIntFormat format used to represent [[BigInt]]
*/
final case class NativeFormatOptions(
longFormat: NativeLongFormat = NativeLongFormat.RawString,
dateFormat: NativeDateFormat = NativeDateFormat.RawString,
bigIntFormat: NativeBigIntFormat = NativeBigIntFormat.RawString,
)
object NativeFormatOptions {
final val RawString = NativeFormatOptions()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.avsystem.commons
package serialization.nativejs

import com.avsystem.commons.annotation.explicitGenerics
import com.avsystem.commons.serialization.GenCodec.ReadFailure
import com.avsystem.commons.serialization.*
import com.avsystem.commons.serialization.json.RawJson

import scala.scalajs.js
import scala.scalajs.js.JSON

class NativeJsonInput(value: js.Any, options: NativeFormatOptions) extends InputAndSimpleInput { self =>
private def read[T](expected: String)(matcher: PartialFunction[Any, T]): T =
matcher.applyOrElse(value, (o: Any) => throw new ReadFailure(s"Cannot read $expected, got: ${js.typeOf(o)}"))

override def readNull(): Boolean =
value == null

override def readString(): String =
read("String") {
case s: String => s
}

override def readDouble(): Double =
read("Double") {
case v: Double => v
}

override def readInt(): Int =
read("Int") {
case v: Int => v
}

override def readLong(): Long = {
def fromString(s: String): Long =
try s.toLong
catch {
case e: NumberFormatException => throw new ReadFailure(s"Cannot read Long", e)
}
read("Long") {
case s: String => fromString(s)
case i: Int => i
case d: Double if d.isWhole => d.toLong
case b: js.BigInt => fromString(b.toString)
// for some reason pattern match on js.BigInt type does not seem to work, check type manually
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
}
}

override def readBigInt(): BigInt = {
def fromString(s: String): BigInt =
try BigInt(s)
catch {
case e: NumberFormatException => throw new ReadFailure(s"Cannot read BigInt", e)
}

read("BigInt") {
case s: String => fromString(s)
case i: Int => BigInt(i)
case d: Double if d.isWhole => BigInt(d.toLong)
case b: js.BigInt => fromString(b.toString)
// for some reason pattern match on js.BigInt type does not seem to work, check type manually
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
}
}

override def readBigDecimal(): BigDecimal = {
def fromString(s: String): BigDecimal =
try BigDecimal(s)
catch {
case e: NumberFormatException => throw new ReadFailure(s"Cannot read BigDecimal", e)
}
read("BigDecimal") {
case s: String => fromString(s)
case i: Int => BigDecimal(i)
case d: Double => BigDecimal(d)
}
}

override def readBoolean(): Boolean =
read("Boolean") {
case v: Boolean => v
}

override def readList(): ListInput =
read("List") {
case array: js.Array[js.Any @unchecked] => new NativeJsonListInput(array, options)
}

override def readObject(): ObjectInput =
read("Object") {
case obj: js.Object => new NativeJsonObjectInput(obj.asInstanceOf[js.Dictionary[js.Any]], options)
}

override def readTimestamp(): Long = options.dateFormat match {
case NativeDateFormat.RawString | NativeDateFormat.JsNumber =>
readLong() // lenient behaviour, accept any value that can be interpreted as Long
case NativeDateFormat.JsDate =>
read("js.Date") {
case v: js.Date => v.getTime().toLong
}
}

override def skip(): Unit = ()

override def readBinary(): Array[Byte] =
read("Binary") {
case array: js.Array[Int @unchecked] => array.iterator.map(_.toByte).toArray
}

override def readCustom[T](typeMarker: TypeMarker[T]): Opt[T] =
typeMarker match {
case RawJson => JSON.stringify(readRaw()).opt
case _ => Opt.Empty
}

def readRaw(): js.Any = value
}

final class NativeJsonListInput(array: js.Array[js.Any], options: NativeFormatOptions) extends ListInput {
private var it = 0

override def hasNext: Boolean =
it < array.length

override def nextElement(): Input = {
val in = new NativeJsonInput(array(it), options)
it += 1
in
}
}

final class NativeJsonObjectInput(dict: js.Dictionary[js.Any], options: NativeFormatOptions) extends ObjectInput {
private val it = dict.iterator

override def hasNext: Boolean =
it.hasNext

override def peekField(name: String): Opt[FieldInput] =
if (dict.contains(name)) Opt(new NativeJsonFieldInput(name, dict(name), options)) else Opt.Empty

override def nextField(): FieldInput = {
val (key, value) = it.next()
new NativeJsonFieldInput(key, value, options)
}
}

final class NativeJsonFieldInput(
val fieldName: String,
value: js.Any,
options: NativeFormatOptions,
) extends NativeJsonInput(value, options)
with FieldInput

object NativeJsonInput {
@explicitGenerics
def read[T: GenCodec](value: js.Any, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
GenCodec.read[T](new NativeJsonInput(value, options))

@explicitGenerics
def readString[T: GenCodec](value: String, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
read[T](JSON.parse(value), options)
}
Loading

0 comments on commit 14d430b

Please sign in to comment.