Skip to content

Commit 67be890

Browse files
committed
Move NativeJsonInput/Output from Udash, add possibility to customize serialization format for time and Long
1 parent 80a2d81 commit 67be890

File tree

6 files changed

+378
-0
lines changed

6 files changed

+378
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
* Adjusts format produced by [[NativeJsonOutput]].
30+
*
31+
* @param longFormat format used to [[Long]]
32+
* @param dateFormat format used to represent timestamps
33+
*/
34+
final case class NativeFormatOptions(
35+
longFormat: NativeLongFormat = NativeLongFormat.RawString,
36+
dateFormat: NativeDateFormat = NativeDateFormat.RawString,
37+
)
38+
object NativeFormatOptions {
39+
final val RawString = NativeFormatOptions()
40+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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, (_: Any) => throw new ReadFailure(s"$expected expected."))
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 longFromString(s: String): Long =
36+
try s.toLong
37+
catch {
38+
case e: NumberFormatException => throw new ReadFailure(s"Cannot read Long", e)
39+
}
40+
(value: Any) match {
41+
case s: String => longFromString(s)
42+
case i: Int => i
43+
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)
46+
case o => throw new ReadFailure(s"Cannot read Long, got: ${js.typeOf(o)}")
47+
}
48+
}
49+
50+
override def readBigInt(): BigInt = {
51+
def fail = throw new ReadFailure(s"BigInt expected.")
52+
(value: Any) match {
53+
case s: String =>
54+
try BigInt(s)
55+
catch {
56+
case _: NumberFormatException => fail
57+
}
58+
case i: Int => BigInt(i)
59+
case d: Double if d.isWhole => BigInt(d.toLong)
60+
case _ => fail
61+
}
62+
}
63+
64+
override def readBigDecimal(): BigDecimal = {
65+
def fail = throw new ReadFailure(s"BigDecimal expected.")
66+
(value: Any) match {
67+
case s: String =>
68+
try BigDecimal(s)
69+
catch {
70+
case _: NumberFormatException => fail
71+
}
72+
case i: Int => BigDecimal(i)
73+
case d: Double => BigDecimal(d)
74+
case _ => fail
75+
}
76+
}
77+
78+
override def readBoolean(): Boolean =
79+
read("Boolean") {
80+
case v: Boolean => v
81+
}
82+
83+
override def readList(): ListInput =
84+
read("List") {
85+
case array: js.Array[js.Any @unchecked] => new JsonListInput(array, options)
86+
}
87+
88+
override def readObject(): ObjectInput =
89+
read("Object") {
90+
case obj: js.Object => new JsonObjectInput(obj.asInstanceOf[js.Dictionary[js.Any]], options)
91+
}
92+
93+
override def readTimestamp(): Long = options.dateFormat match {
94+
case NativeDateFormat.RawString | NativeDateFormat.JsNumber =>
95+
readLong() // lenient behaviour, accept any value that can be interpreted as Long
96+
case NativeDateFormat.JsDate =>
97+
read("js.Date") {
98+
case v: js.Date => v.getTime().toLong
99+
}
100+
}
101+
102+
override def skip(): Unit = ()
103+
104+
override def readBinary(): Array[Byte] =
105+
readList().iterator(_.readSimple().readInt().toByte).toArray
106+
107+
override def readCustom[T](typeMarker: TypeMarker[T]): Opt[T] =
108+
typeMarker match {
109+
case RawJson => JSON.stringify(readRaw()).opt
110+
case _ => Opt.Empty
111+
}
112+
113+
def readRaw(): js.Any = value
114+
}
115+
116+
final class JsonListInput(list: js.Array[js.Any], options: NativeFormatOptions) extends ListInput {
117+
var it = 0
118+
119+
override def hasNext: Boolean =
120+
it < list.length
121+
122+
override def nextElement(): Input = {
123+
js.BigInt
124+
val in = new NativeJsonInput(list(it), options)
125+
it += 1
126+
in
127+
}
128+
}
129+
130+
final class JsonObjectInput(dict: js.Dictionary[js.Any], options: NativeFormatOptions) extends ObjectInput {
131+
val it = dict.keysIterator
132+
133+
override def hasNext: Boolean =
134+
it.hasNext
135+
136+
override def peekField(name: String): Opt[FieldInput] =
137+
if (dict.contains(name)) Opt(new NativeJsonFieldInput(name, dict(name), options)) else Opt.Empty
138+
139+
override def nextField(): FieldInput = {
140+
val key = it.next()
141+
new NativeJsonFieldInput(key, dict.apply(key), options)
142+
}
143+
}
144+
145+
final class NativeJsonFieldInput(
146+
val fieldName: String,
147+
value: js.Any,
148+
options: NativeFormatOptions,
149+
) extends NativeJsonInput(value, options)
150+
with FieldInput
151+
152+
object NativeJsonInput {
153+
@explicitGenerics
154+
def read[T: GenCodec](value: js.Any, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
155+
GenCodec.read[T](new NativeJsonInput(value, options))
156+
157+
@explicitGenerics
158+
def readString[T: GenCodec](value: String, options: NativeFormatOptions = NativeFormatOptions.RawString): T =
159+
read[T](JSON.parse(value), options)
160+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.avsystem.commons
2+
package serialization.nativejs
3+
4+
import com.avsystem.commons.serialization._
5+
import com.avsystem.commons.serialization.json.RawJson
6+
7+
import scala.scalajs.js
8+
import scala.scalajs.js.JSON
9+
10+
final class NativeJsonOutput(
11+
valueConsumer: js.Any => Unit,
12+
options: NativeFormatOptions,
13+
) extends OutputAndSimpleOutput {
14+
15+
override def writeNull(): Unit =
16+
valueConsumer(null)
17+
18+
override def writeString(str: String): Unit =
19+
valueConsumer(str)
20+
21+
override def writeDouble(double: Double): Unit =
22+
valueConsumer(double)
23+
24+
override def writeInt(int: Int): Unit =
25+
valueConsumer(int)
26+
27+
override def writeLong(long: Long): Unit = options.longFormat match {
28+
case NativeLongFormat.RawString => writeString(long.toString)
29+
case NativeLongFormat.JsNumber => writeDouble(long.toDouble)
30+
case NativeLongFormat.JsBigInt => writeRaw(js.BigInt(long.toString))
31+
}
32+
33+
override def writeBigInt(bigInt: BigInt): Unit =
34+
writeString(bigInt.toString)
35+
36+
override def writeBigDecimal(bigDecimal: BigDecimal): Unit =
37+
writeString(bigDecimal.toString)
38+
39+
override def writeBoolean(boolean: Boolean): Unit =
40+
valueConsumer(boolean)
41+
42+
override def writeList(): ListOutput =
43+
new NativeJsonListOutput(valueConsumer, options)
44+
45+
override def writeObject(): ObjectOutput =
46+
new NativeJsonObjectOutput(valueConsumer, options)
47+
48+
override def writeBinary(binary: Array[Byte]): Unit = {
49+
val l = writeList()
50+
binary.foreach(b => l.writeElement().writeSimple().writeInt(b))
51+
l.finish()
52+
}
53+
54+
override def writeTimestamp(millis: Long): Unit = options.dateFormat match {
55+
case NativeDateFormat.RawString => writeString(millis.toString)
56+
case NativeDateFormat.JsNumber => writeDouble(millis.toDouble)
57+
case NativeDateFormat.JsDate => writeRaw(new js.Date(millis.toDouble))
58+
}
59+
60+
override def writeCustom[T](typeMarker: TypeMarker[T], value: T): Boolean =
61+
typeMarker match {
62+
case RawJson => writeRaw(JSON.parse(value)); true
63+
case _ => false
64+
}
65+
66+
def writeRaw(raw: js.Any): Unit = valueConsumer(raw)
67+
}
68+
69+
final class NativeJsonListOutput(
70+
valueConsumer: js.Any => Unit,
71+
options: NativeFormatOptions,
72+
) extends ListOutput {
73+
private val builder = new js.Array[js.Any]()
74+
75+
override def writeElement(): Output = new NativeJsonOutput(el => builder.append(el), options)
76+
override def finish(): Unit = valueConsumer(builder)
77+
}
78+
79+
final class NativeJsonObjectOutput(
80+
valueConsumer: js.Any => Unit,
81+
options: NativeFormatOptions,
82+
) extends ObjectOutput {
83+
private val builder = js.Dictionary.empty[js.Any]
84+
85+
override def writeField(key: String): Output = new NativeJsonOutput(el => builder(key) = el, options)
86+
override def finish(): Unit = valueConsumer(builder)
87+
}
88+
89+
object NativeJsonOutput {
90+
def write[T: GenCodec](value: T, options: NativeFormatOptions = NativeFormatOptions.RawString): js.Any = {
91+
var result: js.Any = null
92+
GenCodec.write(new NativeJsonOutput(value => result = value, options), value)
93+
result
94+
}
95+
96+
def writeAsString[T: GenCodec](value: T, options: NativeFormatOptions = NativeFormatOptions.RawString): String =
97+
JSON.stringify(write(value, options))
98+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.avsystem.commons
2+
package serialization.nativejs
3+
4+
import com.avsystem.commons.misc.Timestamp
5+
import com.avsystem.commons.serialization.json.WrappedJson
6+
import com.avsystem.commons.serialization.{GenCodec, HasGenCodec}
7+
import org.scalatest.funsuite.AnyFunSuite
8+
9+
object NativeJsonInputOutputTest {
10+
11+
case class TestModel(
12+
str: String,
13+
int: Int,
14+
long: Long,
15+
time: Timestamp,
16+
list: Seq[Int],
17+
map: Map[String, String],
18+
rawJson: WrappedJson,
19+
)
20+
object TestModel extends HasGenCodec[TestModel]
21+
}
22+
23+
class NativeJsonInputOutputTest extends AnyFunSuite {
24+
import NativeJsonInputOutputTest._
25+
26+
test("Bilateral serialization - raw string options") {
27+
val options = NativeFormatOptions.RawString
28+
bilateralTyped(testModel, options)
29+
}
30+
31+
test("Bilateral serialization - number options") {
32+
val options = NativeFormatOptions(longFormat = NativeLongFormat.JsNumber, dateFormat = NativeDateFormat.JsNumber)
33+
bilateralTyped(testModel, options)
34+
}
35+
36+
test("Bilateral serialization - typed options") {
37+
val options = NativeFormatOptions(longFormat = NativeLongFormat.JsBigInt, dateFormat = NativeDateFormat.JsDate)
38+
bilateralTyped(testModel, options)
39+
}
40+
41+
private def testModel: TestModel = TestModel(
42+
str = "abc",
43+
int = 123,
44+
long = 10_000_000_123L,
45+
time = Timestamp.now(),
46+
list = Seq(1, 2, 3),
47+
map = Map("Abc" -> "1", "xyz" -> "10000"),
48+
rawJson = WrappedJson("""{"a":1,"b":"c"}"""),
49+
)
50+
51+
private def bilateralTyped[T: GenCodec](input: T, options: NativeFormatOptions): Unit = {
52+
val raw = NativeJsonOutput.write(input, options)
53+
val deserialized = NativeJsonInput.read[T](raw, options)
54+
assert(deserialized == input)
55+
}
56+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.avsystem.commons
2+
package serialization.json
3+
4+
import com.avsystem.commons.serialization.GenCodec
5+
6+
/**
7+
* Wrapper for raw JSON string
8+
*
9+
* It will be serialized as JSON value when used with [[com.avsystem.commons.serialization.Output]] supporting
10+
* [[RawJson]] marker.
11+
*/
12+
final case class WrappedJson(value: String) extends AnyVal
13+
object WrappedJson {
14+
implicit val codec: GenCodec[WrappedJson] = GenCodec.create(
15+
in => WrappedJson(in.readCustom(RawJson).getOrElse(in.readSimple().readString())),
16+
(out, v) => if (!out.writeCustom(RawJson, v.value)) out.writeSimple().writeString(v.value),
17+
)
18+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class JsonStringInputOutputTest extends AnyFunSuite with SerializationTestUtils
6060
assert(resBuilder.result() == jsons)
6161
}
6262

63+
test("WrappedJson") {
64+
val json = "{\"a\": 123, \"b\": 3.14}"
65+
assert(JsonStringOutput.write(WrappedJson(json)) == json)
66+
assert(JsonStringInput.read[WrappedJson](json) == WrappedJson(json))
67+
}
68+
6369
def roundtrip[T: GenCodec](name: String)(values: T*)(implicit pos: Position): Unit = {
6470
test(name) {
6571
val serialized = values.map(write[T](_))

0 commit comments

Comments
 (0)