Skip to content

Commit

Permalink
Remove Try and switch to Scalactic Or
Browse files Browse the repository at this point in the history
Removes Try which runs on exception based handling, and moved to an accumulating Or instead. This also enabled us to get exhaustive pattern matching in, where-as before Try was catching errors at runtime thrown from pattern errors from JSON that wasn't really DynamoDB JSON.
  • Loading branch information
rmmeans committed Aug 16, 2016
1 parent ba89c09 commit 58dffbf
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 37 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ libraryDependencies ++= Seq(
"com.typesafe.play" %% "play" % "2.5.0" % "provided",
"com.typesafe.play" %% "play-ws" % "2.5.0" % "provided",
"com.amazonaws" % "aws-java-sdk-core" % "1.11.+" % "provided",
"org.scalactic" %% "scalactic" % "3.0.0" % "provided",
"net.kaliber" %% "play-s3" % "8.0.0" % "provided",
"com.typesafe.play" %% "play-test" % "2.5.0" % "test",
"com.amazonaws" % "aws-java-sdk-dynamodb" % "1.11.21" % "test",
Expand Down
79 changes: 51 additions & 28 deletions src/main/scala/com/lifeway/play/dynamo/DynamoJsonConverters.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.lifeway.play.dynamo

import org.scalactic._
import play.api.libs.json._

import scala.collection.GenTraversable
import scala.util.Try

object DynamoJsonConverters {

Expand Down Expand Up @@ -50,44 +50,67 @@ object DynamoJsonConverters {
* the dynamo types removed. Typically, you would use this to transform the Dynamo response back to a JsValue type
* that you could then pass to Play's standard Json writes method.
*
* Note that this returns a Try[JsObject], because if a non dynamoDB json object is passed to it (which we can't
* prevent), or if a currently unsupported DynamoDB type was in the object (i.e. Binary or BinarySet), then we
* would throw a match exception.
* Note that this returns a JsObject Or Every[ErrorMessage], because if a non dynamoDB json object is passed to it
* (which we can't prevent), or if a currently unsupported DynamoDB type was in the object (i.e. Binary or
* BinarySet), then we can't successfully process the request. Rather, we return the errors to the user of all
* of the fields that we couldn't convert.
*/
def fromDynamoJson: Try[JsObject] = {
def objectConversion(i: JsObject): JsObject =
JsObject(i.fields.flatMap {
def fromDynamoJson: JsObject Or Every[ErrorMessage] = {

def objectConversion(i: JsObject): JsObject Or Every[ErrorMessage] = {
val temp: Seq[(String, JsValue) Or Every[ErrorMessage]] = i.fields.flatMap {
case (k, JsObject(wrappedObj)) =>
wrappedObj.map {
case ("S", JsString(v)) => (k, JsString(v))
case ("NULL", _) => (k, JsBoolean(true))
case ("BOOL", JsBoolean(v)) => (k, JsBoolean(v))
case ("N", JsString(v)) => (k, JsNumber(BigDecimal(v)))
case ("L", v: JsArray) => (k, arrayConversion(v))
case ("M", m: JsObject) => (k, objectConversion(m))
case ("SS", v: JsArray) => (k, v)
case ("NS", v: JsArray) => (k, JsArray(v.value.map(x => JsNumber(BigDecimal(x.as[JsString].value)))))
case ("S", JsString(v)) => Good(k -> JsString(v))
case ("NULL", _) => Good(k, JsBoolean(true))
case ("BOOL", JsBoolean(v)) => Good((k, JsBoolean(v)))
case ("N", JsString(v)) => Good((k, JsNumber(BigDecimal(v))))
case ("L", v: JsArray) =>
for {
conv <- arrayConversion(v)
} yield (k, conv)
case ("M", m: JsObject) =>
for {
conv <- objectConversion(m)
} yield (k, conv)
case ("SS", v: JsArray) => Good((k, v))
case ("NS", v: JsArray) =>
Good((k, JsArray(v.value.map(x => JsNumber(BigDecimal(x.as[JsString].value))))))
case (t, _) => Bad(One(s"The field `$t` under field `$k` is not a valid / supported DynamoDB type"))
}
})
case (k, _) => Seq(Bad(One(s"The value for field `$k` is not a valid DynamoDB type.")))
}

def arrayConversion(i: JsArray): JsValue =
JsArray(i.value.flatMap {
case x: JsNumber => GenTraversable(x)
case x: JsString => GenTraversable(x)
val (goodSeq, badSeq) = temp.partition(_.isGood)
if (badSeq.isEmpty) Good(JsObject(goodSeq.map(_.get)))
else Bad(Every.from(badSeq.flatMap(_.swap.get.map(x => x))).get)
}

def arrayConversion(i: JsArray): JsValue Or Every[ErrorMessage] = {
val temp: Seq[JsValue Or Every[ErrorMessage]] = i.value.flatMap {
case x: JsNumber => GenTraversable(Good(x))
case x: JsString => GenTraversable(Good(x))
case x: JsObject =>
x.fields.map {
case ("S", v: JsString) => v
case ("NULL", _) => JsNull
case ("BOOL", v: JsBoolean) => v
case ("N", JsString(v)) => JsNumber(BigDecimal(v))
case ("S", v: JsString) => Good(v)
case ("NULL", _) => Good(JsNull)
case ("BOOL", v: JsBoolean) => Good(v)
case ("N", JsString(v)) => Good(JsNumber(BigDecimal(v)))
case ("L", v: JsArray) => arrayConversion(v)
case ("M", m: JsObject) => objectConversion(m)
case ("SS", v: JsArray) => v
case ("NS", v: JsArray) => JsArray(v.value.map(x => JsNumber(BigDecimal(x.as[JsString].value))))
case ("SS", v: JsArray) => Good(v)
case ("NS", v: JsArray) => Good(JsArray(v.value.map(x => JsNumber(BigDecimal(x.as[JsString].value)))))
case (t, _) => Bad(One(s"`$t` is not a valid / supported DynamoDB type in an array"))
}
})
case x: JsValue => Seq(Bad(One(s"`$x` is not a valid / supported DynamoDB type in a DynamoDB array")))
}

Try(objectConversion(input))
val (goodSeq, badSeq) = temp.partition(_.isGood)
if (badSeq.isEmpty) Good(JsArray(goodSeq.map(_.get)))
else Bad(Every.from(badSeq.flatMap(_.swap.get.map(x => x))).get)
}

objectConversion(input)
}
}
}
40 changes: 31 additions & 9 deletions src/test/scala/com/lifeway/play/dynamo/JsonToFromDynamoSpec.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.lifeway.play.dynamo

import com.lifeway.play.dynamo
import org.scalactic._
import org.scalatest.{MustMatchers, WordSpec}
import play.api.libs.json.{JsObject, Json}

import scala.util.Try

class JsonToFromDynamoSpec extends WordSpec with MustMatchers {

case class NestedObject(title: String, age: Byte)
Expand All @@ -25,6 +24,7 @@ class JsonToFromDynamoSpec extends WordSpec with MustMatchers {
objectType: NestedObject,
nestedObjectSet: Set[NestedObject],
nestedObjectSeq: Seq[NestedObject])

object TestSample {
implicit val reads = Json.reads[TestSample]
implicit val writes = Json.writes[TestSample]
Expand Down Expand Up @@ -141,24 +141,46 @@ class JsonToFromDynamoSpec extends WordSpec with MustMatchers {
"read from DynamoDB Json through direct Json Converters to standard case class readers" in {
import DynamoJsonConverters.Converters

val result: Try[TestSample] = sampleJson.as[JsObject].fromDynamoJson.map(_.as[TestSample])
result.isSuccess mustEqual true
val result: TestSample Or Every[ErrorMessage] = sampleJson.as[JsObject].fromDynamoJson.map(_.as[TestSample])
result.isGood mustEqual true
result.get mustEqual sampleVal
}

"read from an invalid DynamoDB JSON through direct Json Converters should return an failed Try in the event of non-dynamo Json" in {
import DynamoJsonConverters.Converters

val json = Json.parse(
"""
val json = Json.parse("""
|{
| "key": "This is normal Json",
| "otherThing": true
| "otherThing": true,
| "nestedType": {
| "someKey" : "This is bad"
| },
| "someThing": {
| "L": [ true, false ]
| },
| "anotherArray" : {
| "L": [
| {
| "S": "A valid DynamoDB value just for good measure...."
| },
| {
| "B": "Some data type that is not yet supported..."
| }
| ]
| }
|}
""".stripMargin)

val result: Try[TestSample] = json.as[JsObject].fromDynamoJson.map(_.as[TestSample])
result.isSuccess mustEqual false
val result: TestSample Or Every[ErrorMessage] = json.as[JsObject].fromDynamoJson.map(_.as[TestSample])
result.isGood mustEqual false
result.swap.get mustEqual Many(
"The value for field `key` is not a valid DynamoDB type.",
"The value for field `otherThing` is not a valid DynamoDB type.",
"The field `someKey` under field `nestedType` is not a valid / supported DynamoDB type",
"`true` is not a valid / supported DynamoDB type in a DynamoDB array",
"`false` is not a valid / supported DynamoDB type in a DynamoDB array",
"`B` is not a valid / supported DynamoDB type in an array")
}

"write to DynamoDB Json through direct Json converters that occur after the standard case class Json writers" in {
Expand Down

0 comments on commit 58dffbf

Please sign in to comment.