-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #610 from AVSystem/nativejs-inout
NativeJsonInput/Output with custom format
- Loading branch information
Showing
8 changed files
with
478 additions
and
8 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
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 |
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
54 changes: 54 additions & 0 deletions
54
core/js/src/main/scala/com/avsystem/commons/serialization/nativejs/NativeFormatOptions.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,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() | ||
} |
163 changes: 163 additions & 0 deletions
163
core/js/src/main/scala/com/avsystem/commons/serialization/nativejs/NativeJsonInput.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,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) | ||
} |
Oops, something went wrong.