Skip to content

Commit b828541

Browse files
authored
Merge pull request #74 from AVSystem/io-refactor
Input/Output refactor, breaking changes
2 parents df73349 + 19ab22b commit b828541

File tree

39 files changed

+742
-326
lines changed

39 files changed

+742
-326
lines changed
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.avsystem.commons
22

3-
import com.avsystem.commons.ser.Benchmarks
3+
import com.avsystem.commons.ser.{IsoInstantBenchmarks, JsonBenchmarks}
44
import japgolly.scalajs.benchmark.gui.BenchmarkGUI
55
import org.scalajs.dom._
66

77
object Main {
88
def main(args: Array[String]): Unit = {
99
val body = document.getElementById("body")
10-
BenchmarkGUI.renderSuite(body)(Benchmarks.suite)
10+
BenchmarkGUI.renderMenu(body)(
11+
IsoInstantBenchmarks.suite,
12+
JsonBenchmarks.suite
13+
)
1114
}
1215
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.avsystem.commons
2+
package ser
3+
4+
import com.avsystem.commons.serialization.GenCodec.ReadFailure
5+
import japgolly.scalajs.benchmark.gui.GuiSuite
6+
import japgolly.scalajs.benchmark.{Benchmark, Suite}
7+
8+
import scala.scalajs.js
9+
import scala.scalajs.js.RegExp
10+
11+
object IsoInstantBenchmarks {
12+
private val regex: RegExp =
13+
js.RegExp("""^(\+|-)?[0-9]+-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?Z$""")
14+
15+
def parse(string: String, validate: Boolean): Long = {
16+
def fail = throw new ReadFailure(s"invalid ISO instant: $string")
17+
if (!validate || regex.test(string)) {
18+
val parsed = js.Date.parse(string)
19+
if (parsed.isNaN) fail
20+
else parsed.toLong
21+
} else fail
22+
}
23+
24+
val suite = GuiSuite(
25+
Suite("IsoInstant parsing")(
26+
Benchmark("with regex validation") {
27+
parse("2013-11-27T12:55:32.234Z", validate = true)
28+
},
29+
Benchmark("without regex validation") {
30+
parse("2013-11-27T12:55:32.234Z", validate = false)
31+
}
32+
)
33+
)
34+
}

commons-benchmark/js/src/main/scala/com/avsystem/commons/ser/Benchmarks.scala renamed to commons-benchmark/js/src/main/scala/com/avsystem/commons/ser/JsonBenchmarks.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import io.circe.syntax._
77
import japgolly.scalajs.benchmark.gui.GuiSuite
88
import japgolly.scalajs.benchmark.{Benchmark, Suite}
99

10-
object Benchmarks {
10+
object JsonBenchmarks {
1111
val suite = GuiSuite(
1212
Suite("JSON serialization benchmarks")(
1313
Benchmark("Writing case class: GenCodec") {
Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,54 @@
11
package com.avsystem.commons
22
package ser
33

4-
import com.avsystem.commons.serialization.GenCodec.ReadFailure
54
import com.avsystem.commons.serialization._
5+
import com.avsystem.commons.serialization.json.{JsonBinaryFormat, JsonOptions, JsonStringInput, JsonStringOutput}
66
import org.openjdk.jmh.annotations._
77

8-
@Warmup(iterations = 5)
9-
@Measurement(iterations = 20)
8+
@Warmup(iterations = 5, time = 1)
9+
@Measurement(iterations = 10, time = 2)
1010
@Fork(1)
1111
@BenchmarkMode(Array(Mode.Throughput))
1212
class GenCodecBenchmarks {
1313
@Benchmark
14-
def testCaseClassCodec(): Simple = GenCodec.read[Simple](DummyInput)
15-
}
14+
def someWriting: String = JsonStringOutput.write(GenCodecBenchmarks.somes)
15+
16+
@Benchmark
17+
def noneWriting: String = JsonStringOutput.write(GenCodecBenchmarks.nones)
1618

17-
case class Simple(int: Int, str: String)
18-
object Simple {
19-
implicit val codec: GenCodec[Simple] = GenCodec.materialize
19+
@Benchmark
20+
def cleanSomeWriting: String = {
21+
implicit val cleanCodec: GenCodec[Option[String]] = GenCodecBenchmarks.cleanOptionCodec[String]
22+
JsonStringOutput.write(GenCodecBenchmarks.somes)
23+
}
24+
25+
@Benchmark
26+
def cleanNoneWriting: String = {
27+
implicit val cleanCodec: GenCodec[Option[String]] = GenCodecBenchmarks.cleanOptionCodec[String]
28+
JsonStringOutput.write(GenCodecBenchmarks.nones)
29+
}
30+
31+
@Benchmark
32+
def binaryReading: Array[Byte] =
33+
JsonStringInput.read[Array[Byte]](GenCodecBenchmarks.hex, GenCodecBenchmarks.options)
2034
}
2135

22-
object DummyInput extends Input {
23-
private def ignored = throw new ReadFailure("don't care")
24-
25-
def inputType = InputType.Object
26-
def readBinary() = ignored
27-
def readLong() = ignored
28-
def readNull() = ignored
29-
def readObject() = new ObjectInput {
30-
private val it = Iterator(
31-
new SimpleValueFieldInput("int", 42),
32-
new SimpleValueFieldInput("str", "lol")
36+
object GenCodecBenchmarks {
37+
implicit def cleanOptionCodec[T: GenCodec]: GenCodec[Option[T]] =
38+
GenCodec.create[Option[T]](
39+
i => if (i.isNull) {
40+
i.readNull()
41+
None
42+
} else Some(GenCodec.read[T](i)),
43+
(o, vo) => vo match {
44+
case Some(v) => GenCodec.write[T](o, v)
45+
case None => o.writeNull()
46+
}
3347
)
34-
def nextField() = it.next()
35-
def hasNext = it.hasNext
36-
}
37-
def readInt() = ignored
38-
def readString() = ignored
39-
def readList() = ignored
40-
def readBoolean() = ignored
41-
def readDouble() = ignored
42-
def readBigInt() = ignored
43-
def readBigDecimal() = ignored
44-
def skip() = ()
48+
49+
val somes: Seq[Option[String]] = MArrayBuffer.tabulate(1000)(i => Some(i.toString))
50+
val nones: Seq[Option[String]] = MArrayBuffer.fill(1000)(None)
51+
52+
val options = JsonOptions(binaryFormat = JsonBinaryFormat.HexString)
53+
val hex: String = JsonStringOutput.write(Array.tabulate[Byte](1024)(_.toByte), options)
4554
}

commons-benchmark/jvm/src/main/scala/com/avsystem/commons/ser/JsonSerializationBenchmark.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import io.circe.parser._
77
import io.circe.syntax._
88
import org.openjdk.jmh.annotations.{Benchmark, BenchmarkMode, Fork, Measurement, Mode, Warmup}
99

10-
@Warmup(iterations = 10)
11-
@Measurement(iterations = 20)
10+
@Warmup(iterations = 5, time = 1)
11+
@Measurement(iterations = 10, time = 2)
1212
@Fork(1)
1313
@BenchmarkMode(Array(Mode.Throughput))
1414
abstract class JsonSerializationBenchmark

commons-benchmark/src/main/scala/com/avsystem/commons/ser/CirceJsonInputOutput.scala

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,7 @@ class CirceJsonInput(json: Json) extends Input {
5555

5656
private def asNumber = json.asNumber.getOrElse(failNot("number"))
5757

58-
final def inputType: InputType =
59-
if (json.isNull) InputType.Null
60-
else if (json.isArray) InputType.List
61-
else if (json.isObject) InputType.Object
62-
else InputType.Simple
63-
58+
def isNull: Boolean = json.isNull
6459
def readNull(): Null = if (json.isNull) null else failNot("null")
6560
def readString(): String = json.asString.getOrElse(failNot("string"))
6661
def readBoolean(): Boolean = json.asBoolean.getOrElse(failNot("boolean"))
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.avsystem.commons
2+
package jsiop
3+
4+
import com.avsystem.commons.misc.TimestampConversions
5+
6+
import scala.scalajs.js
7+
8+
trait JsInterop {
9+
implicit def jsDateTimestampConversions(jsDate: js.Date): TimestampConversions =
10+
new TimestampConversions(jsDate.getTime.toLong)
11+
}
12+
object JsInterop extends JsInterop
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.avsystem.commons
2+
package misc
3+
4+
import scala.scalajs.js
5+
6+
final class TimestampConversions(private val millis: Long) extends AnyVal {
7+
def toTimestamp: Timestamp = Timestamp(millis)
8+
def toJsDate: js.Date = new js.Date(millis.toDouble)
9+
def toJDate: JDate = new JDate(millis)
10+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.avsystem.commons
2+
package serialization
3+
4+
import com.avsystem.commons.serialization.GenCodec.ReadFailure
5+
6+
import scala.scalajs.js
7+
import scala.scalajs.js.RegExp
8+
9+
object IsoInstant {
10+
private val regex: RegExp =
11+
js.RegExp("""^(\+|-)?[0-9]+-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?Z$""")
12+
13+
def format(millis: Long): String =
14+
new js.Date(millis.toDouble).toISOString
15+
16+
def parse(string: String): Long = {
17+
def fail = throw new ReadFailure(s"invalid ISO instant: $string")
18+
if (regex.test(string)) {
19+
val parsed = js.Date.parse(string)
20+
if (parsed.isNaN) fail
21+
else parsed.toLong
22+
} else fail
23+
}
24+
}

commons-core/jvm/src/main/scala/com/avsystem/commons/jiop/JavaInterop.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ trait JavaInterop extends AnyRef
1212
with JFunctionUtils
1313
with JStreamUtils
1414
with JOptionalUtils
15+
with JavaTimeInterop
1516

1617
object JavaInterop extends JavaInterop
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.avsystem.commons
2+
package jiop
3+
4+
import java.time.Instant
5+
6+
import com.avsystem.commons.jiop.JavaTimeInterop.InstantOps
7+
import com.avsystem.commons.misc.Timestamp
8+
9+
trait JavaTimeInterop {
10+
implicit def instantOps(instant: Instant): InstantOps = new InstantOps(instant)
11+
}
12+
object JavaTimeInterop {
13+
class InstantOps(private val instant: Instant) extends AnyVal {
14+
def truncateToTimestamp: Timestamp = Timestamp(instant.toEpochMilli)
15+
def truncateToJDate: JDate = new JDate(instant.toEpochMilli)
16+
}
17+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.avsystem.commons
2+
package jsiop
3+
4+
trait JsInterop
5+
object JsInterop extends JsInterop
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.avsystem.commons
2+
package misc
3+
4+
import java.time.Instant
5+
6+
final class TimestampConversions(private val millis: Long) extends AnyVal {
7+
def toTimestamp: Timestamp = Timestamp(millis)
8+
def toInstant: Instant = Instant.ofEpochMilli(millis)
9+
def toJDate: JDate = new JDate(millis)
10+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.avsystem.commons
2+
package serialization
3+
4+
import java.time.Instant
5+
import java.time.format.DateTimeParseException
6+
7+
import com.avsystem.commons.serialization.GenCodec.ReadFailure
8+
9+
object IsoInstant {
10+
def format(millis: Long): String = {
11+
val res = Instant.ofEpochMilli(millis).toString
12+
// add trailing .000Z if omitted to align with JS implementation
13+
if (res.charAt(res.length - 5) == '.') res
14+
else res.substring(0, res.length - 1) + ".000Z"
15+
}
16+
17+
def parse(string: String): Long =
18+
try Instant.parse(string).toEpochMilli catch {
19+
case _: DateTimeParseException => throw new ReadFailure(s"invalid ISO instant: $string")
20+
}
21+
}

commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ object SharedExtensions extends SharedExtensions {
6868
@silent
6969
def discard: Unit = ()
7070

71+
def thenReturn[T](value: T): T = value
72+
7173
def option: Option[A] = Option(a)
7274

7375
def opt: Opt[A] = Opt(a)

commons-core/src/main/scala/com/avsystem/commons/jiop/JBasicUtils.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import java.util.Comparator
55
import java.util.concurrent.Callable
66
import java.{lang => jl, math => jm, util => ju}
77

8-
import com.avsystem.commons.misc.Sam
8+
import com.avsystem.commons.misc.{Sam, TimestampConversions}
99

1010
trait JBasicUtils {
1111
def jRunnable(code: => Any) = Sam[Runnable](code)
1212
def jCallable[T](expr: => T) = Sam[Callable[T]](expr)
1313
def jComparator[T](cmp: (T, T) => Int) = Sam[Comparator[T]](cmp)
1414

15+
implicit def jDateTimestampConversions(date: JDate): TimestampConversions =
16+
new TimestampConversions(date.getTime)
17+
1518
type JByte = jl.Byte
1619
type JShort = jl.Short
1720
type JInteger = jl.Integer

commons-core/src/main/scala/com/avsystem/commons/misc/BoxingUnboxing.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ object Boxing extends LowPrioBoxing {
1313
implicit val FloatBoxing: Boxing[Float, JFloat] = fromImplicitConv
1414
implicit val DoubleBoxing: Boxing[Double, JDouble] = fromImplicitConv
1515
}
16-
trait LowPrioBoxing {this: Boxing.type =>
17-
implicit def anyRefBoxing[A >: Null <: AnyRef]: Boxing[A, A] = Boxing(identity)
16+
trait LowPrioBoxing { this: Boxing.type =>
17+
implicit def nullableBoxing[A >: Null]: Boxing[A, A] = Boxing(identity)
1818
}
1919

2020
case class Unboxing[+A, -B](fun: B => A) extends AnyVal
@@ -29,6 +29,6 @@ object Unboxing extends LowPrioUnboxing {
2929
implicit val FloatUnboxing: Unboxing[Float, JFloat] = fromImplicitConv
3030
implicit val DoubleUnboxing: Unboxing[Double, JDouble] = fromImplicitConv
3131
}
32-
trait LowPrioUnboxing {this: Unboxing.type =>
33-
implicit def anyRefUnboxing[A >: Null <: AnyRef]: Unboxing[A, A] = Unboxing(identity)
32+
trait LowPrioUnboxing { this: Unboxing.type =>
33+
implicit def nullableUnboxing[A >: Null]: Unboxing[A, A] = Unboxing(identity)
3434
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.avsystem.commons
2+
package misc
3+
4+
import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, IsoInstant}
5+
6+
import scala.concurrent.duration.{FiniteDuration, TimeUnit}
7+
8+
/**
9+
* Millisecond-precision, general purpose, cross compiled timestamp representation.
10+
*
11+
* @param millis milliseconds since UNIX epoch, UTC
12+
*/
13+
class Timestamp(val millis: Long) extends AnyVal {
14+
def toJDate: JDate = new JDate(millis)
15+
16+
def <(other: Timestamp): Boolean = millis < other.millis
17+
def <=(other: Timestamp): Boolean = millis <= other.millis
18+
def >(other: Timestamp): Boolean = millis > other.millis
19+
def >=(other: Timestamp): Boolean = millis >= other.millis
20+
21+
def add(amount: Long, unit: TimeUnit): Timestamp = Timestamp(millis + unit.toMillis(amount))
22+
23+
def +(duration: FiniteDuration): Timestamp = Timestamp(millis + duration.toMillis)
24+
def -(duration: FiniteDuration): Timestamp = Timestamp(millis - duration.toMillis)
25+
26+
override def toString: String = IsoInstant.format(millis)
27+
}
28+
object Timestamp {
29+
final val Zero = Timestamp(0)
30+
31+
def apply(millis: Long): Timestamp = new Timestamp(millis)
32+
def unapply(timestamp: Timestamp): Opt[Long] = Opt(timestamp.millis)
33+
def parse(str: String): Timestamp = Timestamp(IsoInstant.parse(str))
34+
35+
def now(): Timestamp = Timestamp(System.currentTimeMillis())
36+
37+
implicit def conversions(tstamp: Timestamp): TimestampConversions =
38+
new TimestampConversions(tstamp.millis)
39+
40+
implicit val keyCodec: GenKeyCodec[Timestamp] =
41+
GenKeyCodec.create(parse, _.toString)
42+
43+
implicit val codec: GenCodec[Timestamp] =
44+
GenCodec.create(i => Timestamp(i.readTimestamp()), (o, t) => o.writeTimestamp(t.millis))
45+
46+
implicit val ordering: Ordering[Timestamp] =
47+
Ordering.by(_.millis)
48+
}

commons-core/src/main/scala/com/avsystem/commons/package.scala

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

33
import com.avsystem.commons.collection.CollectionAliases
44
import com.avsystem.commons.jiop.JavaInterop
5+
import com.avsystem.commons.jsiop.JsInterop
56

67
package object commons
7-
extends SharedExtensions with CommonAliases with CollectionAliases with JavaInterop
8+
extends SharedExtensions with CommonAliases with CollectionAliases with JavaInterop with JsInterop

0 commit comments

Comments
 (0)