Skip to content

Commit c8ec804

Browse files
committed
Improve binary serialization, add support for typed BigInt
1 parent 67be890 commit c8ec804

File tree

6 files changed

+90
-42
lines changed

6 files changed

+90
-42
lines changed

core/js/src/main/scala/com/avsystem/commons/serialization/nativejs/NativeFormatOptions.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,29 @@ object NativeDateFormat extends AbstractValueEnumCompanion[NativeDateFormat] {
2525
final val JsDate: Value = new NativeDateFormat
2626
}
2727

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 NativeBitIntFormat(implicit ctx: EnumCtx) extends AbstractValueEnum
35+
object NativeBitIntFormat extends AbstractValueEnumCompanion[NativeBitIntFormat] {
36+
final val RawString: Value = new NativeBitIntFormat
37+
final val JsBigInt: Value = new NativeBitIntFormat
38+
}
39+
2840
/**
2941
* Adjusts format produced by [[NativeJsonOutput]].
3042
*
3143
* @param longFormat format used to [[Long]]
3244
* @param dateFormat format used to represent timestamps
45+
* @param bigIntFormat format used to represent [[BigInt]]
3346
*/
3447
final case class NativeFormatOptions(
3548
longFormat: NativeLongFormat = NativeLongFormat.RawString,
3649
dateFormat: NativeDateFormat = NativeDateFormat.RawString,
50+
bigIntFormat: NativeBitIntFormat = NativeBitIntFormat.RawString,
3751
)
3852
object NativeFormatOptions {
3953
final val RawString = NativeFormatOptions()

core/js/src/main/scala/com/avsystem/commons/serialization/nativejs/NativeJsonInput.scala

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,46 +32,49 @@ class NativeJsonInput(value: js.Any, options: NativeFormatOptions) extends Input
3232
}
3333

3434
override def readLong(): Long = {
35-
def longFromString(s: String): Long =
35+
def fromString(s: String): Long =
3636
try s.toLong
3737
catch {
3838
case e: NumberFormatException => throw new ReadFailure(s"Cannot read Long", e)
3939
}
4040
(value: Any) match {
41-
case s: String => longFromString(s)
41+
case s: String => fromString(s)
4242
case i: Int => i
4343
case d: Double if d.isWhole => d.toLong
44-
case b: js.BigInt => longFromString(b.toString)
45-
case b if js.typeOf(b) == "bigint" => longFromString(b.asInstanceOf[js.BigInt].toString)
44+
case b: js.BigInt => fromString(b.toString)
45+
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
4646
case o => throw new ReadFailure(s"Cannot read Long, got: ${js.typeOf(o)}")
4747
}
4848
}
4949

5050
override def readBigInt(): BigInt = {
51-
def fail = throw new ReadFailure(s"BigInt expected.")
51+
def fromString(s: String): BigInt =
52+
try BigInt(s)
53+
catch {
54+
case e: NumberFormatException => throw new ReadFailure(s"Cannot read BitInt", e)
55+
}
56+
5257
(value: Any) match {
53-
case s: String =>
54-
try BigInt(s)
55-
catch {
56-
case _: NumberFormatException => fail
57-
}
58+
case s: String => fromString(s)
5859
case i: Int => BigInt(i)
5960
case d: Double if d.isWhole => BigInt(d.toLong)
60-
case _ => fail
61+
case b: js.BigInt => fromString(b.toString)
62+
case b if js.typeOf(b) == "bigint" => fromString(b.asInstanceOf[js.BigInt].toString)
63+
case o => throw new ReadFailure(s"Cannot read BitInt, got: ${js.typeOf(o)}")
6164
}
6265
}
6366

6467
override def readBigDecimal(): BigDecimal = {
65-
def fail = throw new ReadFailure(s"BigDecimal expected.")
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+
}
6673
(value: Any) match {
67-
case s: String =>
68-
try BigDecimal(s)
69-
catch {
70-
case _: NumberFormatException => fail
71-
}
74+
case s: String => fromString(s)
7275
case i: Int => BigDecimal(i)
7376
case d: Double => BigDecimal(d)
74-
case _ => fail
77+
case o => throw new ReadFailure(s"Cannot read BigDecimal, got: ${js.typeOf(o)}")
7578
}
7679
}
7780

@@ -102,7 +105,9 @@ class NativeJsonInput(value: js.Any, options: NativeFormatOptions) extends Input
102105
override def skip(): Unit = ()
103106

104107
override def readBinary(): Array[Byte] =
105-
readList().iterator(_.readSimple().readInt().toByte).toArray
108+
read("List") {
109+
case array: js.Array[Int @unchecked] => array.iterator.map(_.toByte).toArray
110+
}
106111

107112
override def readCustom[T](typeMarker: TypeMarker[T]): Opt[T] =
108113
typeMarker match {
@@ -120,15 +125,14 @@ final class JsonListInput(list: js.Array[js.Any], options: NativeFormatOptions)
120125
it < list.length
121126

122127
override def nextElement(): Input = {
123-
js.BigInt
124128
val in = new NativeJsonInput(list(it), options)
125129
it += 1
126130
in
127131
}
128132
}
129133

130134
final class JsonObjectInput(dict: js.Dictionary[js.Any], options: NativeFormatOptions) extends ObjectInput {
131-
val it = dict.keysIterator
135+
val it = dict.iterator
132136

133137
override def hasNext: Boolean =
134138
it.hasNext
@@ -137,8 +141,8 @@ final class JsonObjectInput(dict: js.Dictionary[js.Any], options: NativeFormatOp
137141
if (dict.contains(name)) Opt(new NativeJsonFieldInput(name, dict(name), options)) else Opt.Empty
138142

139143
override def nextField(): FieldInput = {
140-
val key = it.next()
141-
new NativeJsonFieldInput(key, dict.apply(key), options)
144+
val (key, value) = it.next()
145+
new NativeJsonFieldInput(key, value, options)
142146
}
143147
}
144148

core/js/src/main/scala/com/avsystem/commons/serialization/nativejs/NativeJsonOutput.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ final class NativeJsonOutput(
3030
case NativeLongFormat.JsBigInt => writeRaw(js.BigInt(long.toString))
3131
}
3232

33-
override def writeBigInt(bigInt: BigInt): Unit =
34-
writeString(bigInt.toString)
33+
override def writeBigInt(bigInt: BigInt): Unit = options.bigIntFormat match {
34+
case NativeBitIntFormat.RawString => writeString(bigInt.toString)
35+
case NativeBitIntFormat.JsBigInt => writeRaw(js.BigInt(bigInt.toString))
36+
}
3537

3638
override def writeBigDecimal(bigDecimal: BigDecimal): Unit =
3739
writeString(bigDecimal.toString)
@@ -46,9 +48,8 @@ final class NativeJsonOutput(
4648
new NativeJsonObjectOutput(valueConsumer, options)
4749

4850
override def writeBinary(binary: Array[Byte]): Unit = {
49-
val l = writeList()
50-
binary.foreach(b => l.writeElement().writeSimple().writeInt(b))
51-
l.finish()
51+
import js.JSConverters._
52+
valueConsumer(binary.toJSArray)
5253
}
5354

5455
override def writeTimestamp(millis: Long): Unit = options.dateFormat match {
Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.avsystem.commons
22
package serialization.nativejs
33

4-
import com.avsystem.commons.misc.Timestamp
4+
import com.avsystem.commons.misc.{Bytes, Timestamp}
55
import com.avsystem.commons.serialization.json.WrappedJson
66
import com.avsystem.commons.serialization.{GenCodec, HasGenCodec}
77
import org.scalatest.funsuite.AnyFunSuite
@@ -15,6 +15,8 @@ object NativeJsonInputOutputTest {
1515
time: Timestamp,
1616
list: Seq[Int],
1717
map: Map[String, String],
18+
binary: Bytes,
19+
bigInt: BigInt,
1820
rawJson: WrappedJson,
1921
)
2022
object TestModel extends HasGenCodec[TestModel]
@@ -23,19 +25,37 @@ object NativeJsonInputOutputTest {
2325
class NativeJsonInputOutputTest extends AnyFunSuite {
2426
import NativeJsonInputOutputTest._
2527

26-
test("Bilateral serialization - raw string options") {
27-
val options = NativeFormatOptions.RawString
28-
bilateralTyped(testModel, options)
29-
}
28+
case class BilateralTestCase(name: String, options: NativeFormatOptions, testStringRepr: Boolean = true)
3029

31-
test("Bilateral serialization - number options") {
32-
val options = NativeFormatOptions(longFormat = NativeLongFormat.JsNumber, dateFormat = NativeDateFormat.JsNumber)
33-
bilateralTyped(testModel, options)
34-
}
30+
private val testCases = Seq(
31+
BilateralTestCase("raw string options", NativeFormatOptions.RawString),
32+
BilateralTestCase(
33+
"number options",
34+
NativeFormatOptions(
35+
longFormat = NativeLongFormat.JsNumber,
36+
dateFormat = NativeDateFormat.JsNumber),
37+
),
38+
BilateralTestCase(
39+
"typed options",
40+
NativeFormatOptions(
41+
longFormat = NativeLongFormat.JsBigInt,
42+
NativeDateFormat.JsDate,
43+
bigIntFormat = NativeBitIntFormat.JsBigInt,
44+
),
45+
testStringRepr = false, // scala.scalajs.js.JavaScriptException: TypeError: Do not know how to serialize a BigInt
46+
),
47+
)
48+
49+
testCases.foreach { case BilateralTestCase(name, options, testStringRepr) =>
50+
test(s"Bilateral serialization - $name") {
51+
bilateralTyped(testModel, options)
52+
}
3553

36-
test("Bilateral serialization - typed options") {
37-
val options = NativeFormatOptions(longFormat = NativeLongFormat.JsBigInt, dateFormat = NativeDateFormat.JsDate)
38-
bilateralTyped(testModel, options)
54+
if (testStringRepr) {
55+
test(s"Bilateral serialization to string - $name") {
56+
bilateralString(testModel, options)
57+
}
58+
}
3959
}
4060

4161
private def testModel: TestModel = TestModel(
@@ -45,6 +65,8 @@ class NativeJsonInputOutputTest extends AnyFunSuite {
4565
time = Timestamp.now(),
4666
list = Seq(1, 2, 3),
4767
map = Map("Abc" -> "1", "xyz" -> "10000"),
68+
binary = new Bytes(Array(1, 2, 0, 5)),
69+
bigInt = BigInt("10000000000000000000"),
4870
rawJson = WrappedJson("""{"a":1,"b":"c"}"""),
4971
)
5072

@@ -53,4 +75,10 @@ class NativeJsonInputOutputTest extends AnyFunSuite {
5375
val deserialized = NativeJsonInput.read[T](raw, options)
5476
assert(deserialized == input)
5577
}
78+
79+
private def bilateralString[T: GenCodec](input: T, options: NativeFormatOptions): Unit = {
80+
val raw = NativeJsonOutput.writeAsString(input, options)
81+
val deserialized = NativeJsonInput.readString[T](raw, options)
82+
assert(deserialized == input)
83+
}
5684
}

core/src/main/scala/com/avsystem/commons/serialization/json/WrappedJson.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.avsystem.commons
22
package serialization.json
33

4+
import com.avsystem.commons.misc.CaseMethods
45
import com.avsystem.commons.serialization.GenCodec
56

67
/**
@@ -9,7 +10,7 @@ import com.avsystem.commons.serialization.GenCodec
910
* It will be serialized as JSON value when used with [[com.avsystem.commons.serialization.Output]] supporting
1011
* [[RawJson]] marker.
1112
*/
12-
final case class WrappedJson(value: String) extends AnyVal
13+
final case class WrappedJson(value: String) extends AnyVal with CaseMethods
1314
object WrappedJson {
1415
implicit val codec: GenCodec[WrappedJson] = GenCodec.create(
1516
in => WrappedJson(in.readCustom(RawJson).getOrElse(in.readSimple().readString())),

core/src/test/scala/com/avsystem/commons/serialization/json/JsonStringInputOutputTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class JsonStringInputOutputTest extends AnyFunSuite with SerializationTestUtils
6161
}
6262

6363
test("WrappedJson") {
64-
val json = "{\"a\": 123, \"b\": 3.14}"
64+
val json = """{"a": 123, "b": 3.14}"""
6565
assert(JsonStringOutput.write(WrappedJson(json)) == json)
6666
assert(JsonStringInput.read[WrappedJson](json) == WrappedJson(json))
6767
}

0 commit comments

Comments
 (0)