Skip to content

Commit

Permalink
1st class BufferedValue (#91)
Browse files Browse the repository at this point in the history
* 1st class BufferedValue

* save a method call
  • Loading branch information
russellremple authored Aug 14, 2021
1 parent 0cf89e3 commit c1315d0
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,31 @@ import java.time.Instant
import com.rallyhealth.weepickle.v1.core.{ArrVisitor, FromInput, JsVisitor, ObjVisitor, Visitor}

import scala.collection.mutable
import scala.util.Try

/**
* A version of [[com.rallyhealth.weejson.v1.Value]] used to buffer data in raw form.
*
* This is used by the case class macros to buffer data for polymorphic types
* when the discriminator is not the first element, e.g. `{"foo": 1, "\$type": "discriminator"}`.
* It is important that all types be immutable.
*
* May be superior to use in client code as well under some circumstances, e.g.,
* when representing data from and to non-JSON types. For example, when piped through a
* `Value`, `java.time.Instant` will come out the other side as `visitString` rather than
* `visitTimestamp`, leading to potentially wasteful parsing/formatting. Other advantages
* are: immutable, more efficient number representation (BigDecimal has a lot of performance
* overhead when applied to smaller numeric types), and direct representation of binary
* and extension types (without assumed JSON byte array encoding).
*
* Most Visitor methods are represented by their own case classes. Exceptions are:
* - Float64String and UInt64 - along with Float64StringParts, represented as Num
* - Int32 - along with Int64, represented as NumLong
* - Float32 - along with Float64, represented as NumDouble
* - Char - along with String, represented as Str
*
* Therefore, when transforming from a [[BufferedValue]], the specific visit methods for these
* will never be called (e.g., for integers, visitInt32 will never be called, only visitInt64).
*/
sealed trait BufferedValue

Expand Down Expand Up @@ -83,9 +101,11 @@ object BufferedValueOps {

/**
* Returns an Option[Instant] in case this [[BufferedValue]] is a 'Timestamp'.
* (or a string that can be parsed as a timestamp.)
*/
def timestampOpt: Option[Instant] = bv match {
case Timestamp(i) => Some(i)
case Str(s) => Try(Instant.parse(s)).toOption // so it won't blow up if you round-trip through JSON
case _ => None
}

Expand Down Expand Up @@ -209,6 +229,21 @@ object BufferedValue extends Transformer[BufferedValue] {
*/
case class InvalidData(data: BufferedValue, msg: String) extends Exception(s"$msg (data: $data)")

implicit def BufferableSeq[T](items: Iterable[T])(implicit f: T => BufferedValue): Arr = fromElements(items.map(f))
implicit def BufferableDict[T](items: Iterable[(String, T)])(implicit f: T => BufferedValue): Obj =
fromAttributes(items.map(x => (x._1, f(x._2))))
implicit def BufferableBoolean(i: Boolean): Bool = if (i) True else False
implicit def BufferableByte(i: Byte): NumLong = NumLong(i.longValue)
implicit def BufferableShort(i: Short): NumLong = NumLong(i.longValue)
implicit def BufferableInt(i: Int): NumLong = NumLong(i.longValue)
implicit def BufferableLong(i: Long): NumLong = NumLong(i)
implicit def BufferableFloat(i: Float): NumDouble = NumDouble(i.doubleValue)
implicit def BufferableDouble(i: Double): NumDouble = NumDouble(i)
implicit def BufferableBigDecimal(i: BigDecimal): AnyNum = AnyNum(i)
implicit def BufferableNull(i: Null): Null.type = Null
implicit def BufferableString(s: CharSequence): Str = Str(s.toString)
implicit def BufferableInstant(dt: Instant): Timestamp = Timestamp(dt)

def transform[T](i: BufferedValue, to: Visitor[_, T]): T = {
i match {
case BufferedValue.Null => to.visitNull()
Expand All @@ -222,11 +257,11 @@ object BufferedValue extends Transformer[BufferedValue] {
case BufferedValue.Ext(tag, b) => to.visitExt(tag, b, 0, b.length)
case BufferedValue.Timestamp(i) => to.visitTimestamp(i)
case BufferedValue.Arr(items @ _*) =>
val ctx = to.visitArray(-1).narrow
val ctx = to.visitArray(items.size).narrow
for (item <- items) ctx.visitValue(transform(item, ctx.subVisitor))
ctx.visitEnd()
case BufferedValue.Obj(items @ _*) =>
val ctx = to.visitObject(-1).narrow
val ctx = to.visitObject(items.length).narrow
for ((k, item) <- items) {
val keyVisitor = ctx.visitKey()

Expand All @@ -237,6 +272,10 @@ object BufferedValue extends Transformer[BufferedValue] {
}
}

/**
* Extending JsVisitor here for bin compat reasons only. Overrides all methods,
* essentially extending Visitor without hidden JSON "baggage".
*/
object Builder extends JsVisitor[BufferedValue, BufferedValue] {
def visitArray(length: Int): ArrVisitor[BufferedValue, BufferedValue] =
new ArrVisitor[BufferedValue, BufferedValue.Arr] {
Expand Down Expand Up @@ -269,6 +308,7 @@ object BufferedValue extends Transformer[BufferedValue] {

override def visitFloat64StringParts(cs: CharSequence, decIndex: Int, expIndex: Int): BufferedValue =
BufferedValue.Num(cs.toString, decIndex, expIndex)

override def visitFloat64(d: Double): BufferedValue = BufferedValue.NumDouble(d)

override def visitInt64(l: Long): BufferedValue = NumLong(l)
Expand All @@ -287,5 +327,31 @@ object BufferedValue extends Transformer[BufferedValue] {
offset: Int,
len: Int
): BufferedValue = BufferedValue.Ext(tag, bytes.slice(offset, len))

/*
* Not represented by their own case cases. Restates what is in `JsVisitor` for clarity
* (would prefer not to extend JsVisitor at all).
*/
override def visitFloat64String(s: String): BufferedValue = BufferedValue.Num(
s = s,
decIndex = s.indexOf('.'),
expIndex = s.indexOf('E') match {
case -1 => s.indexOf('e')
case n => n
}
)

override def visitUInt64(ul: Long): BufferedValue = BufferedValue.Num(
s = java.lang.Long.toUnsignedString(ul),
decIndex = -1,
expIndex = -1
)

override def visitInt32(i: Int): BufferedValue = BufferedValue.NumLong(i.longValue)

override def visitFloat32(f: Float): BufferedValue = BufferedValue.NumDouble(f.doubleValue)

override def visitChar(c: Char): BufferedValue = BufferedValue.Str(c.toString)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -329,5 +329,35 @@ object StructTests extends TestSuite {
test("false") - rw(com.rallyhealth.weejson.v1.Bool(false), """false""")
test("null") - rw(com.rallyhealth.weejson.v1.Null, """null""")
}
test("BufferedValue") {
test("value") {
val value: com.rallyhealth.weejson.v1.BufferedValue = com.rallyhealth.weejson.v1.BufferedValue.Str("test")
rw(value, """ "test" """.trim)
}
test("str") - rw(com.rallyhealth.weejson.v1.BufferedValue.Str("test"), """"test"""")
test("num") - rw(com.rallyhealth.weejson.v1.BufferedValue.AnyNum(7), """7""")
test("obj") {
test("nested") - rw(
com.rallyhealth.weejson.v1.BufferedValue.Obj(
"foo" -> com.rallyhealth.weejson.v1.BufferedValue.Null,
"bar" -> com.rallyhealth.weejson.v1.BufferedValue.Obj("baz" -> com.rallyhealth.weejson.v1.BufferedValue.Str("str"))
),
"""{"foo":null,"bar":{"baz":"str"}}"""
)
test("empty") - rw(com.rallyhealth.weejson.v1.BufferedValue.Obj(), """{}""")
}
test("arr") {
test("nonEmpty") - rw(
com.rallyhealth.weejson.v1.BufferedValue.Arr(com.rallyhealth.weejson.v1.BufferedValue.AnyNum(5), com.rallyhealth.weejson.v1.BufferedValue.AnyNum(6)),
"""[5,6]"""
)
test("empty") - rw(com.rallyhealth.weejson.v1.BufferedValue.Arr(), """[]""")
}
test("true") - rw(com.rallyhealth.weejson.v1.BufferedValue.True, """true""")
test("true") - rw(com.rallyhealth.weejson.v1.BufferedValue.Bool(true), """true""")
test("false") - rw(com.rallyhealth.weejson.v1.BufferedValue.False, """false""")
test("false") - rw(com.rallyhealth.weejson.v1.BufferedValue.Bool(false), """false""")
test("null") - rw(com.rallyhealth.weejson.v1.BufferedValue.Null, """null""")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.rallyhealth.weepickle.v1.example

import java.io.{File, FileOutputStream}

import acyclic.file
import com.rallyhealth.weepickle.v1.{TestUtil, WeePickle}
import utest._
Expand All @@ -13,6 +12,8 @@ import com.rallyhealth.weejson.v1.yaml.{FromYaml, ToYaml}
import com.rallyhealth.weepack.v1.{FromMsgPack, Msg, ToMsgPack, WeePack}
import com.rallyhealth.weepickle.v1.core.{NoOpVisitor, Visitor}
import com.rallyhealth.weepickle.v1.implicits.{discriminator, dropDefault}

import java.time.Instant
object Simple {
case class Thing(myFieldA: Int, myFieldB: String)
object Thing {
Expand Down Expand Up @@ -354,6 +355,22 @@ object ExampleTests extends TestSuite {
FromScala(Bar(123, "abc")).transform(ToJson.string) ==> """["abc",123]"""
FromJson("""["abc",123]""").transform(ToScala[Bar]) ==> Bar(123, "abc")
}
test("BufferedValue") {
import com.rallyhealth.weepickle.v1.WeePickle._
import com.rallyhealth.weejson.v1.BufferedValue
import com.rallyhealth.weejson.v1.BufferedValueOps._
case class Bar(i: Int, s: String, d: Instant)
implicit val fooReadWrite: FromTo[Bar] =
fromTo[BufferedValue].bimap[Bar](
x => BufferedValue.Arr(x.s, x.i, x.d),
buffer => Bar(buffer(1).num.toInt, buffer(0).str, buffer(2).timestamp)
)
val now = Instant.now()
val nowString = now.toString

FromScala(Bar(123, "abc", now)).transform(ToJson.string) ==> s"""["abc",123,"$nowString"]"""
FromJson(s"""["abc",123,"$nowString"]""").transform(ToScala[Bar]) ==> Bar(123, "abc", now)
}
}
test("keyed") {
import com.rallyhealth.weepickle.v1.WeePickle._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,58 @@ trait FromToValue extends MacroImplicits { this: Types with Annotator =>
new From[com.rallyhealth.weejson.v1.Value] {
def transform0[Out](v: Value, out: Visitor[_, Out]): Out = v.transform(out)
}

/*
* And the same for BufferedValue types
*/
implicit val ToBufferedValue: To[com.rallyhealth.weejson.v1.BufferedValue] =
new To.Delegate(com.rallyhealth.weejson.v1.BufferedValue.Builder)

implicit def ToBufferedValueObj: To[com.rallyhealth.weejson.v1.BufferedValue.Obj] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Obj]
implicit def ToBufferedValueArr: To[com.rallyhealth.weejson.v1.BufferedValue.Arr] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Arr]
implicit def ToBufferedValueStr: To[com.rallyhealth.weejson.v1.BufferedValue.Str] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Str]
implicit def ToBufferedValueAnyNum: To[com.rallyhealth.weejson.v1.BufferedValue.AnyNum] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.AnyNum]
implicit def ToBufferedValueNum: To[com.rallyhealth.weejson.v1.BufferedValue.Num] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Num]
implicit def ToBufferedValueNumLong: To[com.rallyhealth.weejson.v1.BufferedValue.NumLong] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.NumLong]
implicit def ToBufferedValueNumDouble: To[com.rallyhealth.weejson.v1.BufferedValue.NumDouble] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.NumDouble]
implicit def ToBufferedValueBinary: To[com.rallyhealth.weejson.v1.BufferedValue.Binary] =
ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Binary]
implicit def ToBufferedValueExt: To[com.rallyhealth.weejson.v1.BufferedValue.Ext] =
ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Ext]
implicit def ToBufferedValueTimestamp: To[com.rallyhealth.weejson.v1.BufferedValue.Timestamp] =
ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Timestamp]
implicit def ToBufferedValueBool: To[com.rallyhealth.weejson.v1.BufferedValue.Bool] = ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Bool]
implicit def ToBufferedValueTrue: To[com.rallyhealth.weejson.v1.BufferedValue.True.type] =
ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.True.type]
implicit def ToBufferedValueFalse: To[com.rallyhealth.weejson.v1.BufferedValue.False.type] =
ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.False.type]
implicit def ToBufferedValueNull: To[com.rallyhealth.weejson.v1.BufferedValue.Null.type] =
ToBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Null.type]

implicit val FromBufferedValue: From[com.rallyhealth.weejson.v1.BufferedValue] =
new From[com.rallyhealth.weejson.v1.BufferedValue] {
def transform0[Out](v: com.rallyhealth.weejson.v1.BufferedValue, out: Visitor[_, Out]): Out =
com.rallyhealth.weejson.v1.BufferedValue.transform(v, out)
}

implicit def FromBufferedValueObj: From[com.rallyhealth.weejson.v1.BufferedValue.Obj] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Obj]
implicit def FromBufferedValueArr: From[com.rallyhealth.weejson.v1.BufferedValue.Arr] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Arr]
implicit def FromBufferedValueStr: From[com.rallyhealth.weejson.v1.BufferedValue.Str] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Str]
implicit def FromBufferedValueAnyNum: From[com.rallyhealth.weejson.v1.BufferedValue.AnyNum] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.AnyNum]
implicit def FromBufferedValueNum: From[com.rallyhealth.weejson.v1.BufferedValue.Num] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Num]
implicit def FromBufferedValueNumLong: From[com.rallyhealth.weejson.v1.BufferedValue.NumLong] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.NumLong]
implicit def FromBufferedValueNumDouble: From[com.rallyhealth.weejson.v1.BufferedValue.NumDouble] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.NumDouble]
implicit def FromBufferedValueBinary: From[com.rallyhealth.weejson.v1.BufferedValue.Binary] =
FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Binary]
implicit def FromBufferedValueExt: From[com.rallyhealth.weejson.v1.BufferedValue.Ext] =
FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Ext]
implicit def FromBufferedValueTimestamp: From[com.rallyhealth.weejson.v1.BufferedValue.Timestamp] =
FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Timestamp]
implicit def FromBufferedValueBool: From[com.rallyhealth.weejson.v1.BufferedValue.Bool] = FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Bool]
implicit def FromBufferedValueTrue: From[com.rallyhealth.weejson.v1.BufferedValue.True.type] =
FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.True.type]
implicit def FromBufferedValueFalse: From[com.rallyhealth.weejson.v1.BufferedValue.False.type] =
FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.False.type]
implicit def FromBufferedValueNull: From[com.rallyhealth.weejson.v1.BufferedValue.Null.type] =
FromBufferedValue.narrow[com.rallyhealth.weejson.v1.BufferedValue.Null.type]
}

0 comments on commit c1315d0

Please sign in to comment.