Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for BigInt integers #12

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name := "persist-json"

organization := "com.persist"

version := "1.2.0"
version := "1.2.1-SNAPSHOT"

scalaVersion := "2.12.0"
scalaVersion := "2.12.3"

scalacOptions ++= Seq("-deprecation", "-unchecked")

Expand Down Expand Up @@ -55,4 +55,4 @@ pomExtra := (
<url>https://github.com/jedesah</url>
</developer>
</developers>
)
)
54 changes: 46 additions & 8 deletions src/main/scala/com/persist/JsonFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ package object json {

object ReadCodec extends LowPriorityReadCodec {

val ShortMinValueAsBI = BigInt(Short.MinValue)
val ShortMaxValueAsBI = BigInt(Short.MaxValue)

val ByteMinValueAsBI = BigInt(Byte.MinValue)
val ByteMaxValueAsBI = BigInt(Byte.MaxValue)

def apply[T](implicit st: Lazy[ReadCodec[T]]): ReadCodec[T] = st.value

implicit val string = new ReadCodec[String] {
Expand All @@ -96,7 +102,10 @@ package object json {
case x: Short => x.toInt
case x: Int => x
case x: Long =>
if (x <= Int.MaxValue) x.toInt
if (x >= Int.MinValue && x <= Int.MaxValue) x.toInt
else throw new MappingException(s"Expected number that can fit into an Int, but found $x")
case x: BigInt =>
if (x >= Int.MinValue && x <= Int.MaxValue) x.toInt
else throw new MappingException(s"Expected number that can fit into an Int, but found $x")
case _ => throw new MappingException(s"Expected: Int but found $x")
}
Expand All @@ -110,6 +119,9 @@ package object json {
case x: Int => x.toLong
case x: Short => x.toLong
case x: Long => x
case x: BigInt =>
if (x >= Long.MinValue && x <= Long.MaxValue) x.toLong
else throw new MappingException(s"Expected number that can fit into a Long, but found $x")
case _ => throw new MappingException(s"Expected: Long but found $x")
}
}
Expand All @@ -119,10 +131,13 @@ package object json {
case x: Byte => x.toShort
case x: Short => x
case x: Int =>
if (x <= Short.MaxValue) x.toShort
if (x >= Short.MinValue && x <= Short.MaxValue) x.toShort
else precisionException(x)
case x: Long =>
if (x <= Short.MaxValue) x.toShort
if (x >= Short.MinValue && x <= Short.MaxValue) x.toShort
else precisionException(x)
case x: BigInt =>
if (x >= ShortMinValueAsBI && x <= ShortMinValueAsBI) x.toShort
else precisionException(x)
case _ => throw new MappingException(s"Expected: Short, but found $x")
}
Expand All @@ -132,47 +147,69 @@ package object json {
def read(x: Json): Byte = x match {
case x: Byte => x
case x: Int =>
if (x <= Byte.MaxValue) x.toByte
if (x >= Byte.MinValue && x <= Byte.MaxValue) x.toByte
else precisionException(x)
case x: Short =>
if (x <= Byte.MaxValue) x.toByte
if (x >= Byte.MinValue && x <= Byte.MaxValue) x.toByte
else precisionException(x)
case x: Long =>
if (x <= Short.MaxValue) x.toByte
if (x >= Byte.MinValue && x <= Byte.MaxValue) x.toByte
else precisionException(x)
case x: BigInt =>
if (x >= ByteMinValueAsBI && x <= ByteMinValueAsBI) x.toByte
else precisionException(x)
case _ => throw new MappingException(s"Expected: Byte, but found $x")
}
}
implicit val double = new ReadCodec[Double] {
def read(x: Json): Double = x match {
case x: Byte => x.toDouble
case x: Int => x.toDouble
case x: Float => x.toDouble
case x: Short => x.toDouble
case x: Long => x.toDouble
case x: BigInt => x.toDouble
case x: BigDecimal => x.toDouble
case x: Double => x
case _ => throw new MappingException(s"Expected: Double, but found $x")
}
}
implicit val float = new ReadCodec[Float] {
def read(x: Json): Float = x match {
case x: Byte => x.toFloat
case x: Float => x
case x: Double => x.toFloat
case x: Int => x.toFloat
case x: Short => x.toFloat
case x: Long => x.toFloat
case x: BigInt => x.toFloat
case x: BigDecimal => x.toFloat
case _ => throw new MappingException(s"Expected Float, but found $x")
}
}
implicit val bigInteger = new ReadCodec[BigInt] {
def read(x: Json): BigInt = x match {
case x: Byte => BigInt(x)
case x: BigDecimal => x.toBigInt()
case x: Float => BigDecimal(x.toDouble).toBigInt()
case x: Double => BigDecimal(x).toBigInt()
case x: Int => BigInt(x)
case x: Short => BigInt(x)
case x: Long => BigInt(x)
case x: BigInt => x
case _ => throw new MappingException(s"Expected BigDecimal, but found $x")
}
}
implicit val bigDecimal = new ReadCodec[BigDecimal] {
def read(x: Json): BigDecimal = x match {
case x: Byte => BigDecimal(x)
case x: BigDecimal => x
case x: Float => BigDecimal(x.toDouble)
case x: Double => BigDecimal(x)
case x: Int => BigDecimal(x)
case x: Short => BigDecimal(x)
case x: Long => BigDecimal(x)
case x: BigInt => BigDecimal(x)
case _ => throw new MappingException(s"Expected BigDecimal, but found $x")
}
}
Expand Down Expand Up @@ -255,8 +292,8 @@ package object json {
def read(json: Json): FieldType[K, Option[V]] :: T = {
val map = castOrThrow(json)
val name = key.value.name
// This is so that we gracefully handle a missing field if it's type is optional
val fieldValue = map.get(name)
// This is so that we gracefully handle a missing or null-valued field if it's type is optional
val fieldValue = map.get(name).flatMap(Option(_))
// Try reading the value of the field
// If we get a mapping exception, intercept it and add the name of this field to the path
// If we get another exception, don't touch!
Expand Down Expand Up @@ -308,6 +345,7 @@ package object json {
implicit object ShortCodec extends SimpleCodec[Short]
implicit object ByteCodec extends SimpleCodec[Byte]
implicit object DoubleCodec extends SimpleCodec[Double]
implicit object BigIntCodec extends SimpleCodec[BigInt]
implicit object BigDecimalCodec extends SimpleCodec[BigDecimal]
implicit object IntegerCodec extends SimpleCodec[Integer]
implicit def simpleMap[V: WriteCodec] = new WriteCodec[scala.collection.Map[String, V]] {
Expand Down
37 changes: 37 additions & 0 deletions src/main/scala/com/persist/JsonOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,13 @@ object JsonOps {
*/
def jgetInt(a: Json, ilist: Any*): Int = {
jget(a, ilist: _*) match {
case bi: BigInt =>
val i = bi.toInt
if(BigInt(i) == bi) {
i
} else {
0
}
case l: Long => {
val i = l.toInt
if (i == l) {
Expand Down Expand Up @@ -476,12 +483,40 @@ object JsonOps {
*/
def jgetLong(a: Json, ilist: Any*): Long = {
jget(a, ilist: _*) match {
case bi: BigInt =>
val l = bi.toLong
if(BigInt(l) == bi) {
l
} else {
0L
}
case l: Long => l
case i: Int => i
case x => 0
}
}

/**
*
* Get a BitInt value within a nested Json value.
*
* @param ilist a list of values that are either strings or integers
* - A string selects the value of a JsonObject name-value pair where
* the name equals the string.
* - An integer i selects the ith elements of a JsonArray.
*
* @return the selected long value or 0 if not present.
*
*/
def jgetBigInt(a: Json, ilist: Any*): BigInt = {
jget(a, ilist: _*) match {
case bi: BigInt => bi
case l: Long => BigInt(l)
case i: Int => BigInt(i)
case x => 0
}
}

/**
*
* Get a big decimal value within a nested Json value.
Expand All @@ -498,6 +533,7 @@ object JsonOps {
jget(a, ilist: _*) match {
case b: BigDecimal => b
case d: Double => BigDecimal(d)
case bi: BigInt => BigDecimal(bi)
case l: Long => l
case i: Int => i
case x => 0
Expand All @@ -520,6 +556,7 @@ object JsonOps {
jget(a, ilist: _*) match {
case d: Double => d
case b: BigDecimal => b.toDouble
case bi: BigInt => bi.toDouble
case l: Long => l
case i: Int => i
case x => 0
Expand Down
19 changes: 18 additions & 1 deletion src/main/scala/com/persist/JsonParse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ import scala.annotation.switch

private[persist] object JsonParse {

val IntMinValueBI = BigInt(Int.MinValue)
val IntMaxValueBI = BigInt(Int.MaxValue)

val LongMinValueBI = BigInt(Long.MinValue)
val LongMaxValueBI = BigInt(Long.MaxValue)

// *** Character Kinds

final type CharKind = Int
Expand Down Expand Up @@ -122,6 +128,7 @@ private[persist] object JsonParse {
}

private[persist] class JsonParse(s: String) {
import JsonParse._

// *** Import Shared Data ***

Expand Down Expand Up @@ -446,10 +453,19 @@ private[persist] class JsonParse(s: String) {
val v = try {
tokenValue.toLong
} catch {
case _: NumberFormatException =>
BigInt(tokenValue)
case _: Throwable => tokenError("Bad integer")
}
tokenNext
val r: Json = if (v >= Int.MinValue && v <= Int.MaxValue) v.toInt else v
val r: Json = v match {
case i: Int => i
case l: Long => if(l >= Int.MinValue && l <= Int.MaxValue) l.toInt else l
case bi: BigInt =>
if (bi >= IntMinValueBI && bi <= IntMaxValueBI) bi.toInt
else if (bi >= LongMinValueBI && bi <= LongMaxValueBI) bi.toLong
else bi
}
r
}
case BIGNUMBER => {
Expand Down Expand Up @@ -563,6 +579,7 @@ private[persist] object JsonUnparse {
}
case x: Int => sb.append(x.toString)
case x: Long => sb.append(x.toString)
case x: BigInt => sb.append(x.toString)
case x: BigDecimal => sb.append(x.toString)
case x: Double =>
sb.append("%1$e".format(x)) // g->e
Expand Down
50 changes: 46 additions & 4 deletions src/test/scala/com/persist/MapperTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,69 @@ import com.persist.JsonOps._
import com.persist.JsonMapper._
import com.persist.json._

case class Person(name: String, age: Option[Int], friend: Option[Person])
case class Person(name: String, age: Option[Int], friend: Option[Person], id: BigInt)

case class Group(city: String, people: Seq[Person], kinds:Array[String],
var cnt: Int, props: JsonObject, value: BigDecimal)

case class HasNullable(data: Option[String])

case class Attribution(
name: String
)
case class Media(
blah: String,
attribution: Option[Attribution]
)
case class Response(data: Seq[Media])

@RunWith(classOf[JUnitRunner])
class MapperTest extends FunSuite {

def removeNullValuedFields(j: Json): Json = j match {
case map: Map[_, _] =>
val m1 = map filter ({ case (_, v) => v != null })
m1 mapValues (removeNullValuedFields)
case seq: Seq[_] =>
seq map (removeNullValuedFields)
case x => x
}

test("mapper") {


val j: Json = Json( """{city:"Seattle", cnt:2, props:{i:1, j:2}, value:2.3,
people:[{name:"Joe", friend:{name:"Sam"}},
{name:"Tom", age:20}], kinds:["red","blue"]
people:[{name:"Joe", friend:{name:"Sam", id: 9824987}, id: 9872349872349827349872349872},
{name:"Tom", age:20, id: 989823498723498234982349829}], kinds:["red","blue"]
}""")

val group = ToObject[Group](j)

val j1: Json = ToJson(group)

assert(j1 === j, "mapper fail")
assert(j1 === removeNullValuedFields(j), "mapper fail")
}

test("map-null-value") {

val j: Json = Json("""{"data": null}""")

val response = ToObject[HasNullable](j)

val j1: Json = ToJson(response)

assert(j1 == removeNullValuedFields(j), "mapper failed")
}

test("complex-mapper") {

val j: Json = Json("""{"data": [{"blah": "blah", "attribution": null}, {"blah": "blah", "attribution": {"name": "Bob"}}]}""")

val response = ToObject[Response](j)

val j1: Json = ToJson(response)

assert(j1 === removeNullValuedFields(j), "mapper fail")
}

}