Skip to content

Commit 14d430b

Browse files
authored
Merge pull request #610 from AVSystem/nativejs-inout
NativeJsonInput/Output with custom format
2 parents 80a2d81 + 16a3166 commit 14d430b

File tree

8 files changed

+478
-8
lines changed

8 files changed

+478
-8
lines changed

benchmark/js/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
How to run benchmark:
2+
- compile `sbt commons-benchmark-js/fullOptJS`
3+
- open `fullopt-2.13.html` file in a browser
4+
- select test suite and run

benchmark/js/src/main/scala/com/avsystem/commons/ser/JsonBenchmarks.scala

+33-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.avsystem.commons
22
package ser
33

44
import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput}
5+
import com.avsystem.commons.serialization.nativejs.{NativeJsonInput, NativeJsonOutput}
56
import io.circe.parser._
67
import io.circe.syntax._
78
import japgolly.scalajs.benchmark.gui.GuiSuite
@@ -10,62 +11,86 @@ import japgolly.scalajs.benchmark.{Benchmark, Suite}
1011
object JsonBenchmarks {
1112
val suite = GuiSuite(
1213
Suite("JSON serialization benchmarks")(
13-
Benchmark("Writing case class: GenCodec") {
14+
Benchmark("Writing case class: GenCodec, String Json format") {
1415
JsonStringOutput.write(Something.Example)
1516
},
17+
Benchmark("Writing case class: GenCodec, Native Json format") {
18+
NativeJsonOutput.writeAsString(Something.Example)
19+
},
1620
Benchmark("Writing case class: Circe") {
1721
Something.Example.asJson.noSpaces
1822
},
1923
Benchmark("Writing case class: uPickle") {
2024
upickle.default.write(Something.Example)
2125
},
22-
Benchmark("Reading case class: GenCodec") {
26+
Benchmark("Reading case class: GenCodec, String Json format") {
2327
JsonStringInput.read[Something](Something.ExampleJsonString)
2428
},
29+
Benchmark("Reading case class: GenCodec, Native Json format") {
30+
NativeJsonInput.readString[Something](Something.ExampleJsonString)
31+
},
2532
Benchmark("Reading case class: Circe") {
2633
decode[Something](Something.ExampleJsonString).fold(e => throw e, identity)
2734
},
2835
Benchmark("Reading case class: uPickle") {
2936
upickle.default.read[Something](Something.ExampleJsonString)
3037
},
3138

32-
Benchmark("Writing sealed hierarchy: GenCodec") {
39+
Benchmark("Writing sealed hierarchy: GenCodec, String Json format") {
3340
JsonStringOutput.write(SealedStuff.ExampleList)
3441
},
35-
Benchmark("Writing sealed hierarchy: GenCodec (flat)") {
42+
Benchmark("Writing sealed hierarchy: GenCodec (flat), String Json format") {
3643
JsonStringOutput.write(FlatSealedStuff.ExampleList)
3744
},
45+
Benchmark("Writing sealed hierarchy: GenCodec, Native Json format") {
46+
NativeJsonOutput.writeAsString(SealedStuff.ExampleList)
47+
},
48+
Benchmark("Writing sealed hierarchy: GenCodec (flat), Native Json format") {
49+
NativeJsonOutput.writeAsString(FlatSealedStuff.ExampleList)
50+
},
3851
Benchmark("Writing sealed hierarchy: Circe") {
3952
SealedStuff.ExampleList.asJson.noSpaces
4053
},
4154
Benchmark("Writing sealed hierarchy: uPickle") {
4255
upickle.default.write(SealedStuff.ExampleList)
4356
},
44-
Benchmark("Reading sealed hierarchy: GenCodec") {
57+
Benchmark("Reading sealed hierarchy: GenCodec, String Json format") {
4558
JsonStringInput.read[List[SealedStuff]](SealedStuff.ExampleJsonString)
4659
},
47-
Benchmark("Reading sealed hierarchy: GenCodec (flat)") {
60+
Benchmark("Reading sealed hierarchy: GenCodec (flat), String Json format") {
4861
JsonStringInput.read[List[FlatSealedStuff]](FlatSealedStuff.ExampleJsonString)
4962
},
63+
Benchmark("Reading sealed hierarchy: GenCodec, Native Json format") {
64+
NativeJsonInput.readString[List[SealedStuff]](SealedStuff.ExampleJsonString)
65+
},
66+
Benchmark("Reading sealed hierarchy: GenCodec (flat), Native Json format") {
67+
NativeJsonInput.readString[List[FlatSealedStuff]](FlatSealedStuff.ExampleJsonString)
68+
},
5069
Benchmark("Reading sealed hierarchy: Circe") {
5170
decode[List[SealedStuff]](SealedStuff.ExampleJsonString).fold(e => throw e, identity)
5271
},
5372
Benchmark("Reading sealed hierarchy: uPickle") {
5473
upickle.default.read[List[SealedStuff]](SealedStuff.ExampleUpickleJsonString)
5574
},
5675

57-
Benchmark("Writing foos: GenCodec") {
76+
Benchmark("Writing foos: GenCodec, String Json format") {
5877
JsonStringOutput.write(Foo.ExampleMap)
5978
},
79+
Benchmark("Writing foos: GenCodec, Native Json format") {
80+
NativeJsonOutput.writeAsString(Foo.ExampleMap)
81+
},
6082
Benchmark("Writing foos: Circe") {
6183
Foo.ExampleMap.asJson.noSpaces
6284
},
6385
Benchmark("Writing foos: uPickle") {
6486
upickle.default.write(Foo.ExampleMap)
6587
},
66-
Benchmark("Reading foos: GenCodec") {
88+
Benchmark("Reading foos: GenCodec, String Json format") {
6789
JsonStringInput.read[Map[String, Foo]](Foo.ExampleJsonString)
6890
},
91+
Benchmark("Reading foos: GenCodec with Native Json format") {
92+
NativeJsonInput.readString[Map[String, Foo]](Foo.ExampleJsonString)
93+
},
6994
Benchmark("Reading foos: Circe") {
7095
decode[Map[String, Foo]](Foo.ExampleJsonString).fold(e => throw e, identity)
7196
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.avsystem.commons
2+
package serialization.nativejs
3+
4+
import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx}
5+
6+
/**
7+
* Specifies format used by `NativeJsonOutput.writeLong` / `NativeJsonInput.readLong`
8+
* to represent [[Long]]. JS does not support 64-bit representation.
9+
*/
10+
final class NativeLongFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
11+
object NativeLongFormat extends AbstractValueEnumCompanion[NativeLongFormat] {
12+
final val RawString: Value = new NativeLongFormat
13+
final val JsNumber: Value = new NativeLongFormat
14+
final val JsBigInt: Value = new NativeLongFormat
15+
}
16+
17+
/**
18+
* Specifies format used by `NativeJsonOutput.writeTimestamp` / `NativeJsonInput.readTimestamp`
19+
* to represent timestamps.
20+
*/
21+
final class NativeDateFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
22+
object NativeDateFormat extends AbstractValueEnumCompanion[NativeDateFormat] {
23+
final val RawString: Value = new NativeDateFormat
24+
final val JsNumber: Value = new NativeDateFormat
25+
final val JsDate: Value = new NativeDateFormat
26+
}
27+
28+
/**
29+
* Specifies format used by `NativeJsonOutput.writeBigInt` / `NativeJsonInput.readBigInt`
30+
* to represent [[BigInt]].
31+
*
32+
* Note that [[scala.scalajs.js.JSON.stringify]] does not know how to serialize a BigInt and throws an error
33+
*/
34+
final class NativeBigIntFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
35+
object NativeBigIntFormat extends AbstractValueEnumCompanion[NativeBigIntFormat] {
36+
final val RawString: Value = new NativeBigIntFormat
37+
final val JsBigInt: Value = new NativeBigIntFormat
38+
}
39+
40+
/**
41+
* Adjusts format produced by [[NativeJsonOutput]].
42+
*
43+
* @param longFormat format used to [[Long]]
44+
* @param dateFormat format used to represent timestamps
45+
* @param bigIntFormat format used to represent [[BigInt]]
46+
*/
47+
final case class NativeFormatOptions(
48+
longFormat: NativeLongFormat = NativeLongFormat.RawString,
49+
dateFormat: NativeDateFormat = NativeDateFormat.RawString,
50+
bigIntFormat: NativeBigIntFormat = NativeBigIntFormat.RawString,
51+
)
52+
object NativeFormatOptions {
53+
final val RawString = NativeFormatOptions()
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.avsystem.commons
2+
package serialization.nativejs
3+
4+
import com.avsystem.commons.annotation.explicitGenerics
5+
import com.avsystem.commons.serialization.GenCodec.ReadFailure
6+
import com.avsystem.commons.serialization.*
7+
import com.avsystem.commons.serialization.json.RawJson
8+
9+
import scala.scalajs.js
10+
import scala.scalajs.js.JSON
11+
12+
class NativeJsonInput(value: js.Any, options: NativeFormatOptions) extends InputAndSimpleInput { self =>
13+
private def read[T](expected: String)(matcher: PartialFunction[Any, T]): T =
14+
matcher.applyOrElse(value, (o: Any) => throw new ReadFailure(s"Cannot read $expected, got: ${js.typeOf(o)}"))
15+
16+
override def readNull(): Boolean =
17+
value == null
18+
19+
override def readString(): String =
20+
read("String") {
21+
case s: String => s
22+
}
23+
24+
override def readDouble(): Double =
25+
read("Double") {
26+
case v: Double => v
27+
}
28+
29+
override def readInt(): Int =
30+
read("Int") {
31+
case v: Int => v
32+
}
33+
34+
override def readLong(): Long = {
35+
def fromString(s: String): Long =
36+
try s.toLong
37+
catch {
38+
case e: NumberFormatException => throw new ReadFailure(s"Cannot read Long", e)
39+
}
40+
read("Long") {
41+
case s: String => fromString(s)
42+
case i: Int => i
43+
case d: Double if d.isWhole => d.toLong
44+
case b: js.BigInt => fromString(b.toString)
45+
// for some reason pattern match on js.BigInt type does not seem to work, check type manually
46+
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
47+
}
48+
}
49+
50+
override def readBigInt(): BigInt = {
51+
def fromString(s: String): BigInt =
52+
try BigInt(s)
53+
catch {
54+
case e: NumberFormatException => throw new ReadFailure(s"Cannot read BigInt", e)
55+
}
56+
57+
read("BigInt") {
58+
case s: String => fromString(s)
59+
case i: Int => BigInt(i)
60+
case d: Double if d.isWhole => BigInt(d.toLong)
61+
case b: js.BigInt => fromString(b.toString)
62+
// for some reason pattern match on js.BigInt type does not seem to work, check type manually
63+
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
64+
}
65+
}
66+
67+
override def readBigDecimal(): BigDecimal = {
68+
def fromString(s: String): BigDecimal =
69+
try BigDecimal(s)
70+
catch {
71+
case e: NumberFormatException => throw new ReadFailure(s"Cannot read BigDecimal", e)
72+
}
73+
read("BigDecimal") {
74+
case s: String => fromString(s)
75+
case i: Int => BigDecimal(i)
76+
case d: Double => BigDecimal(d)
77+
}
78+
}
79+
80+
override def readBoolean(): Boolean =
81+
read("Boolean") {
82+
case v: Boolean => v
83+
}
84+
85+
override def readList(): ListInput =
86+
read("List") {
87+
case array: js.Array[js.Any @unchecked] => new NativeJsonListInput(array, options)
88+
}
89+
90+
override def readObject(): ObjectInput =
91+
read("Object") {
92+
case obj: js.Object => new NativeJsonObjectInput(obj.asInstanceOf[js.Dictionary[js.Any]], options)
93+
}
94+
95+
override def readTimestamp(): Long = options.dateFormat match {
96+
case NativeDateFormat.RawString | NativeDateFormat.JsNumber =>
97+
readLong() // lenient behaviour, accept any value that can be interpreted as Long
98+
case NativeDateFormat.JsDate =>
99+
read("js.Date") {
100+
case v: js.Date => v.getTime().toLong
101+
}
102+
}
103+
104+
override def skip(): Unit = ()
105+
106+
override def readBinary(): Array[Byte] =
107+
read("Binary") {
108+
case array: js.Array[Int @unchecked] => array.iterator.map(_.toByte).toArray
109+
}
110+
111+
override def readCustom[T](typeMarker: TypeMarker[T]): Opt[T] =
112+
typeMarker match {
113+
case RawJson => JSON.stringify(readRaw()).opt
114+
case _ => Opt.Empty
115+
}
116+
117+
def readRaw(): js.Any = value
118+
}
119+
120+
final class NativeJsonListInput(array: js.Array[js.Any], options: NativeFormatOptions) extends ListInput {
121+
private var it = 0
122+
123+
override def hasNext: Boolean =
124+
it < array.length
125+
126+
override def nextElement(): Input = {
127+
val in = new NativeJsonInput(array(it), options)
128+
it += 1
129+
in
130+
}
131+
}
132+
133+
final class NativeJsonObjectInput(dict: js.Dictionary[js.Any], options: NativeFormatOptions) extends ObjectInput {
134+
private val it = dict.iterator
135+
136+
override def hasNext: Boolean =
137+
it.hasNext
138+
139+
override def peekField(name: String): Opt[FieldInput] =
140+
if (dict.contains(name)) Opt(new NativeJsonFieldInput(name, dict(name), options)) else Opt.Empty
141+
142+
override def nextField(): FieldInput = {
143+
val (key, value) = it.next()
144+
new NativeJsonFieldInput(key, value, options)
145+
}
146+
}
147+
148+
final class NativeJsonFieldInput(
149+
val fieldName: String,
150+
value: js.Any,
151+
options: NativeFormatOptions,
152+
) extends NativeJsonInput(value, options)
153+
with FieldInput
154+
155+
object NativeJsonInput {
156+
@explicitGenerics
157+
def read[T: GenCodec](value: js.Any, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
158+
GenCodec.read[T](new NativeJsonInput(value, options))
159+
160+
@explicitGenerics
161+
def readString[T: GenCodec](value: String, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
162+
read[T](JSON.parse(value), options)
163+
}

0 commit comments

Comments
 (0)