Skip to content

Commit

Permalink
Feature/bounds checking (#124)
Browse files Browse the repository at this point in the history
* Add min-bounds checking to Json4s deserialising

* Add bounds checking to BSON short deserialiser

* v1.5.9 release

- First release of enumeratum-json4s!
- ReactiveMongo integration (adds bounds checking for byte deserialising)
  • Loading branch information
lloydmeta authored Mar 7, 2017
1 parent 260ffe7 commit bab229b
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 93 deletions.
6 changes: 3 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ lazy val enumeratumReactiveMongoBson =
.settings(testSettings: _*)
.settings(
crossScalaVersions := scalaVersionsAll,
version := "1.5.9-SNAPSHOT",
version := "1.5.10-SNAPSHOT",
libraryDependencies ++= Seq(
"org.reactivemongo" %% "reactivemongo" % reactiveMongoVersion,
"com.beachape" %% "enumeratum" % Versions.Core.stable,
Expand Down Expand Up @@ -284,11 +284,11 @@ lazy val enumeratumJson4s =
settings = commonWithPublishSettings)
.settings(testSettings: _*)
.settings(
version := "1.5.9-SNAPSHOT",
version := "1.5.10-SNAPSHOT",
crossScalaVersions := scalaVersionsAll,
libraryDependencies ++= Seq(
"org.json4s" %% "json4s-core" % json4sVersion,
"org.json4s" %% "json4s-native" % json4sVersion % "test",
"org.json4s" %% "json4s-native" % json4sVersion % Test,
"com.beachape" %% "enumeratum" % Versions.Core.stable
)
)
Expand Down
18 changes: 10 additions & 8 deletions enumeratum-json4s/src/main/scala/enumeratum/Json4s.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import org.json4s.JsonAST.JString
@SuppressWarnings(Array("org.wartremover.warts.Any"))
object Json4s {

def serializer[A <: EnumEntry: Manifest](enum: Enum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => (
{
case JString(s) if enum.withNameOption(s).isDefined => enum.withName(s)
},
{
case x: A => JString(x.entryName)
}
))
def serializer[A <: EnumEntry: Manifest](enum: Enum[A]): CustomSerializer[A] =
new CustomSerializer[A](
_ =>
(
{
case JString(s) if enum.withNameOption(s).isDefined => enum.withName(s)
}, {
case x: A => JString(x.entryName)
}
))

}
135 changes: 83 additions & 52 deletions enumeratum-json4s/src/main/scala/enumeratum/values/Json4s.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,93 @@ import org.json4s.JsonAST.{JInt, JLong, JString}
@SuppressWarnings(Array("org.wartremover.warts.Any"))
object Json4s {

def serializer[A <: IntEnumEntry: Manifest](enum: IntEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => (
{
case JInt(i) if i <= Int.MaxValue && enum.withValueOpt(i.toInt).isDefined => enum.withValue(i.toInt)
case JLong(i) if i <= Int.MaxValue && enum.withValueOpt(i.toInt).isDefined => enum.withValue(i.toInt)
},
{
case x: A => JLong(x.value.toLong)
}
))
def serializer[A <: IntEnumEntry: Manifest](enum: IntEnum[A]): CustomSerializer[A] =
new CustomSerializer[A](
_ =>
(
{
case JInt(i) if inBounds(i, Int.MaxValue) && enum.withValueOpt(i.toInt).isDefined =>
enum.withValue(i.toInt)
case JLong(i) if inBounds(i, Int.MaxValue) && enum.withValueOpt(i.toInt).isDefined =>
enum.withValue(i.toInt)
}, {
case x: A => JLong(x.value.toLong)
}
))

def serializer[A <: LongEnumEntry: Manifest](enum: LongEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => (
{
case JInt(i) if enum.withValueOpt(i.toLong).isDefined => enum.withValue(i.toLong)
case JLong(i) if enum.withValueOpt(i).isDefined => enum.withValue(i)
},
{
case x: A => JLong(x.value)
}
))
def serializer[A <: LongEnumEntry: Manifest](enum: LongEnum[A]): CustomSerializer[A] =
new CustomSerializer[A](
_ =>
(
{
case JInt(i) if isValidLong(i) && enum.withValueOpt(i.toLong).isDefined =>
enum.withValue(i.toLong)
case JLong(i) if enum.withValueOpt(i).isDefined => enum.withValue(i)
}, {
case x: A => JLong(x.value)
}
))

def serializer[A <: ShortEnumEntry: Manifest](enum: ShortEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => (
{
case JInt(i) if i <= Short.MaxValue.toInt && enum.withValueOpt(i.toShort).isDefined => enum.withValue(i.toShort)
case JLong(i) if i <= Short.MaxValue && enum.withValueOpt(i.toShort).isDefined => enum.withValue(i.toShort)
},
{
case x: A => JLong(x.value.toLong)
}
))
def serializer[A <: ShortEnumEntry: Manifest](enum: ShortEnum[A]): CustomSerializer[A] =
new CustomSerializer[A](
_ =>
(
{
case JInt(i)
if inBounds(i, Short.MaxValue) && enum.withValueOpt(i.toShort).isDefined =>
enum.withValue(i.toShort)
case JLong(i)
if inBounds(i, Short.MaxValue) && enum.withValueOpt(i.toShort).isDefined =>
enum.withValue(i.toShort)
}, {
case x: A => JLong(x.value.toLong)
}
))

def serializer[A <: StringEnumEntry: Manifest](enum: StringEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => (
{
case JString(s) if enum.withValueOpt(s).isDefined => enum.withValue(s)
},
{
case x: A => JString(x.value)
}
))
def serializer[A <: StringEnumEntry: Manifest](enum: StringEnum[A]): CustomSerializer[A] =
new CustomSerializer[A](
_ =>
(
{
case JString(s) if enum.withValueOpt(s).isDefined => enum.withValue(s)
}, {
case x: A => JString(x.value)
}
))

def serializer[A <: ByteEnumEntry: Manifest](enum: ByteEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => (
{
case JInt(i) if i <= Byte.MaxValue.toInt && enum.withValueOpt(i.toByte).isDefined => enum.withValue(i.toByte)
case JLong(i) if i <= Byte.MaxValue && enum.withValueOpt(i.toByte).isDefined => enum.withValue(i.toByte)
},
{
case x: A => JLong(x.value.toLong)
}
))
def serializer[A <: ByteEnumEntry: Manifest](enum: ByteEnum[A]): CustomSerializer[A] =
new CustomSerializer[A](
_ =>
(
{
case JInt(i) if inBounds(i, Byte.MaxValue) && enum.withValueOpt(i.toByte).isDefined =>
enum.withValue(i.toByte)
case JLong(i) if inBounds(i, Byte.MaxValue) && enum.withValueOpt(i.toByte).isDefined =>
enum.withValue(i.toByte)
}, {
case x: A => JLong(x.value.toLong)
}
))

def serializer[A <: CharEnumEntry: Manifest](enum: CharEnum[A]): CustomSerializer[A] = new CustomSerializer[A](_ => (
{
case JString(s) if s.length == 1 && enum.withValueOpt(s.head).isDefined => enum.withValue(s.head)
},
{
case x: A => JString(x.value.toString)
}
))
def serializer[A <: CharEnumEntry: Manifest](enum: CharEnum[A]): CustomSerializer[A] =
new CustomSerializer[A](
_ =>
(
{
case JString(s) if s.length == 1 && enum.withValueOpt(s.head).isDefined =>
enum.withValue(s.head)
}, {
case x: A => JString(x.value.toString)
}
))

private def inBounds[N](o: N, posMax: Int)(implicit numeric: Numeric[N]): Boolean = {
import numeric._
abs(o) <= fromInt(posMax)
}

private def isValidLong(b: BigInt): Boolean = {
b.abs <= BigInt(Long.MaxValue)
}

}
13 changes: 7 additions & 6 deletions enumeratum-json4s/src/test/scala/enumeratum/Json4sSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class Json4sSpec extends FunSpec with Matchers {

it("should parse enum members into optional values") {
TrafficLight.values.foreach { value =>
Serialization.read[DataOpt]("""{"tr":"""" + value.entryName + """"}""").tr shouldBe Some(value)
Serialization.read[DataOpt]("""{"tr":"""" + value.entryName + """"}""").tr shouldBe Some(
value)
}
}

Expand All @@ -54,11 +55,11 @@ class Json4sSpec extends FunSpec with Matchers {
}

it("should fail to parse random JSON values to members") {
a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":"bogus"}"""))
a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":17}"""))
a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":true}"""))
a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{"tr":null}"""))
a [MappingException] shouldBe thrownBy (Serialization.read[Data]("""{}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[Data]("""{"tr":"bogus"}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[Data]("""{"tr":17}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[Data]("""{"tr":true}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[Data]("""{"tr":null}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[Data]("""{}"""))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import org.scalatest.{FunSpec, Matchers}
class Json4sValueEnumSpec extends FunSpec with Matchers {

implicit val formats = DefaultFormats +
Json4s.serializer(Json4sMediaType) + Json4s.serializer(Json4sJsonLibs) +
Json4s.serializer(Json4sDevice) + Json4s.serializer(Json4sHttpMethod) +
Json4s.serializer(Json4sBool) + Json4s.serializer(Json4sDigits)
Json4s.serializer(Json4sMediaType) + Json4s.serializer(Json4sJsonLibs) +
Json4s.serializer(Json4sDevice) + Json4s.serializer(Json4sHttpMethod) +
Json4s.serializer(Json4sBool) + Json4s.serializer(Json4sDigits)

val data = Data(Json4sMediaType.`application/jpeg`, Json4sJsonLibs.Json4s, Json4sDevice.Laptop,
Json4sHttpMethod.Put, Json4sBool.Maybe, Json4sDigits.Dos)
val data = Data(Json4sMediaType.`application/jpeg`,
Json4sJsonLibs.Json4s,
Json4sDevice.Laptop,
Json4sHttpMethod.Put,
Json4sBool.Maybe,
Json4sDigits.Dos)

describe("to JSON") {
it("should serialize plain value") {
Expand All @@ -33,11 +37,13 @@ class Json4sValueEnumSpec extends FunSpec with Matchers {

describe("from JSON") {
it("should parse enum members when given proper encoding") {
Serialization.read[Data]("""{"mediaType":3,"jsonLib":2,"device":2,"httpMethod":"PUT","bool":"?","digits":2}""") shouldBe data
Serialization.read[Data](
"""{"mediaType":3,"jsonLib":2,"device":2,"httpMethod":"PUT","bool":"?","digits":2}""") shouldBe data
}

it("should parse enum members into optional values") {
Serialization.read[DataOpt]("""{"mediaType":3}""") shouldBe DataOpt(Some(Json4sMediaType.`application/jpeg`))
Serialization.read[DataOpt]("""{"mediaType":3}""") shouldBe DataOpt(
Some(Json4sMediaType.`application/jpeg`))
}

it("should parse missing value into None") {
Expand All @@ -52,10 +58,13 @@ class Json4sValueEnumSpec extends FunSpec with Matchers {
}

it("should fail to parse random JSON values to members") {
a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":"bogus"}"""))
a[MappingException] shouldBe thrownBy(
Serialization.read[DataSingle]("""{"mediaType":"bogus"}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":17}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":true}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{"mediaType":null}"""))
a[MappingException] shouldBe thrownBy(
Serialization.read[DataSingle]("""{"mediaType":true}"""))
a[MappingException] shouldBe thrownBy(
Serialization.read[DataSingle]("""{"mediaType":null}"""))
a[MappingException] shouldBe thrownBy(Serialization.read[DataSingle]("""{}"""))
}
}
Expand All @@ -73,8 +82,7 @@ case class DataOpt(mediaType: Option[Json4sMediaType])
case class DataSingle(mediaType: Json4sMediaType)

sealed abstract class Json4sMediaType(val value: Long, name: String) extends LongEnumEntry
case object Json4sMediaType
extends LongEnum[Json4sMediaType] {
case object Json4sMediaType extends LongEnum[Json4sMediaType] {
case object `text/json` extends Json4sMediaType(1L, "text/json")
case object `text/html` extends Json4sMediaType(2L, "text/html")
case object `application/jpeg` extends Json4sMediaType(3L, "application/jpeg")
Expand All @@ -83,8 +91,7 @@ case object Json4sMediaType
}

sealed abstract class Json4sJsonLibs(val value: Int) extends IntEnumEntry
case object Json4sJsonLibs
extends IntEnum[Json4sJsonLibs] {
case object Json4sJsonLibs extends IntEnum[Json4sJsonLibs] {
case object Argonaut extends Json4sJsonLibs(1)
case object Json4s extends Json4sJsonLibs(2)
case object Circe extends Json4sJsonLibs(3)
Expand All @@ -96,8 +103,7 @@ case object Json4sJsonLibs
}

sealed abstract class Json4sDevice(val value: Short) extends ShortEnumEntry
case object Json4sDevice
extends ShortEnum[Json4sDevice] {
case object Json4sDevice extends ShortEnum[Json4sDevice] {
case object Phone extends Json4sDevice(1)
case object Laptop extends Json4sDevice(2)
case object Desktop extends Json4sDevice(3)
Expand All @@ -107,8 +113,7 @@ case object Json4sDevice
}

sealed abstract class Json4sHttpMethod(val value: String) extends StringEnumEntry
case object Json4sHttpMethod
extends StringEnum[Json4sHttpMethod] {
case object Json4sHttpMethod extends StringEnum[Json4sHttpMethod] {
case object Get extends Json4sHttpMethod("GET")
case object Put extends Json4sHttpMethod("PUT")
case object Post extends Json4sHttpMethod("POST")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ object BSONValueHandlers extends BSONValueReads with BSONValueWrites {
implicit def anyBsonHandler[A](implicit reader: BSONReader[BSONValue, A],
writer: BSONWriter[A, BSONValue]): BSONHandler[BSONValue, A] =
new BSONHandler[BSONValue, A] {
def write(t: A): BSONValue = writer.write(t)
def write(t: A): BSONValue = writer.write(t)

def read(bson: BSONValue): A = reader.read(bson)
}

Expand All @@ -36,9 +37,8 @@ trait BSONValueReads {

implicit val bsonReaderShort: BSONReader[BSONValue, Short] = new BSONReader[BSONValue, Short] {
def read(bson: BSONValue): Short = bson match {
case BSONInteger(x) if Short.MaxValue >= x && Short.MinValue <= x =>
x.toShort
case _ => throw new RuntimeException(s"Could not convert $bson to Short")
case BSONInteger(x) if x.abs <= Short.MaxValue => x.toShort
case _ => throw new RuntimeException(s"Could not convert $bson to Short")
}
}

Expand Down Expand Up @@ -74,8 +74,8 @@ trait BSONValueReads {

implicit val bsonReaderByte: BSONReader[BSONValue, Byte] = new BSONReader[BSONValue, Byte] {
def read(bson: BSONValue): Byte = bson match {
case BSONInteger(x) => x.toByte
case _ => throw new RuntimeException(s"Could not convert $bson to Byte")
case BSONInteger(x) if x.abs <= Byte.MaxValue => x.toByte
case _ => throw new RuntimeException(s"Could not convert $bson to Byte")
}
}

Expand Down

0 comments on commit bab229b

Please sign in to comment.