Skip to content

Commit

Permalink
Merge pull request #1 from pooranpatel2512/feature/propertiesJsonSupport
Browse files Browse the repository at this point in the history
Feature/properties json support
  • Loading branch information
pooranpatel2512 committed Apr 7, 2015
2 parents 2df1c06 + 084a6bb commit 3264894
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.yetu.siren.json
package playjson

import com.yetu.siren.model.Property.Value

trait PlayJsonSirenFormat {

import com.yetu.siren.model._
import Entity._
import play.api.libs.functional.syntax._
import play.api.libs.json._

import collection.immutable.{ Seq ImmutableSeq }

import play.api.libs.json._
import play.api.libs.functional.syntax._

/**
* Play-JSON format for a Siren root entity.
*/
Expand All @@ -31,6 +32,7 @@ trait PlayJsonSirenFormat {
case e: Entity.EmbeddedLink embeddedLinkWriter writes e
case e: Entity.EmbeddedRepresentation embeddedRepresentationWriter writes e
}

override def reads(json: JsValue): JsResult[EmbeddedEntity] =
embeddedLinkReads.reads(json) orElse embeddedRepresentationReads.reads(json)
}
Expand Down Expand Up @@ -89,14 +91,31 @@ trait PlayJsonSirenFormat {
case Property.StringValue(s) JsString(s)
case Property.NumberValue(n) JsNumber(n)
case Property.BooleanValue(b) JsBoolean(b)
case Property.JsObjectValue(o) JsObject(o.map {
case (k, v) (k, writes(v))
})
case Property.JsArrayValue(a) JsArray(a.map(v writes(v)))
case Property.NullValue JsNull
}

override def reads(json: JsValue): JsResult[Property.Value] = json match {
case JsString(s) JsSuccess(Property.StringValue(s))
case JsNumber(n) JsSuccess(Property.NumberValue(n))
case JsBoolean(b) JsSuccess(Property.BooleanValue(b))
case JsNull JsSuccess(Property.NullValue)
case _ JsError("error.expected.sirenpropertyvalue")
case JsObject(obj)
val propsObjValue: Seq[(String, Value)] = obj.map(seq {
val (key, jsValue) = seq
val read = reads(jsValue).getOrElse(Property.NullValue)
(key, read)
})
JsSuccess(Property.JsObjectValue(propsObjValue))
case JsArray(arr)
val propsArrayValue: Seq[Value] = arr.map(jsValue {
reads(jsValue).getOrElse(Property.NullValue)
})
JsSuccess(Property.JsArrayValue(propsArrayValue))
case JsNull JsSuccess(Property.NullValue)
case _ JsError("error.expected.sirenpropertyvalue")
}
}

Expand All @@ -105,9 +124,10 @@ trait PlayJsonSirenFormat {
*/
implicit val propertiesWriter: Format[Properties] = new Format[Properties] {
override def writes(properties: Properties): JsValue = {
val fields = properties.map (p p.name -> Json.toJson(p.value))
val fields = properties.map(p p.name -> Json.toJson(p.value))
JsObject(fields)
}

override def reads(json: JsValue): JsResult[Properties] = json match {
case JsObject(fields)
fields.foldLeft[JsResult[Properties]](JsSuccess(Vector.empty)) {
Expand All @@ -131,6 +151,7 @@ trait PlayJsonSirenFormat {
*/
implicit val methodFormat: Format[Action.Method] = new Format[Action.Method] {
override def writes(method: Action.Method): JsValue = JsString(method.name)

override def reads(json: JsValue): JsResult[Action.Method] =
json.asOpt[String] flatMap Action.Method.forName asJsResult "error.expected.method"
}
Expand All @@ -140,6 +161,7 @@ trait PlayJsonSirenFormat {
*/
implicit val encodingFormat: Format[Action.Encoding] = new Format[Action.Encoding] {
override def writes(encoding: Action.Encoding): JsValue = JsString(encoding.name)

override def reads(json: JsValue): JsResult[Action.Encoding] =
json.asOpt[String] flatMap Action.Encoding.forName asJsResult "error.expected.encoding"
}
Expand All @@ -149,6 +171,7 @@ trait PlayJsonSirenFormat {
*/
implicit val fieldTypeFormat: Format[Action.Field.Type] = new Format[Action.Field.Type] {
override def writes(fieldType: Action.Field.Type): JsValue = JsString(fieldType.name)

override def reads(json: JsValue): JsResult[Action.Field.Type] =
json.asOpt[String] flatMap Action.Field.Type.forName asJsResult "error.expected.fieldtype"
}
Expand Down
24 changes: 18 additions & 6 deletions src/main/scala/com/yetu/siren/json/sprayjson/SirenJsonFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ package com.yetu.siren
package json
package sprayjson

import play.api.libs.json.JsSuccess
import spray.json._
import com.yetu.siren.model.Action._
import com.yetu.siren.model.Action.Field.Type
Expand Down Expand Up @@ -152,15 +153,26 @@ trait SirenJsonFormat { self: DefaultJsonProtocol ⇒
case JsString(s) Property.StringValue(s)
case JsNumber(n) Property.NumberValue(n)
case JsBoolean(b) Property.BooleanValue(b)
case JsNull Property.NullValue
case x throwDesEx(s"$x is not a valid property value")
case JsObject(obj)
val propsObjValue: Map[String, Property.Value] = obj.map(seq {
val (key, jsValue) = seq
key -> read(jsValue)
})
Property.JsObjectValue(propsObjValue.toSeq)
case JsArray(arr)
val propsArrayValue: Seq[Value] = arr.map(jsValue read(jsValue))
Property.JsArrayValue(propsArrayValue)
case JsNull Property.NullValue
case x throwDesEx(s"$x is not a valid property value")
}
override def write(obj: Value): JsValue =
obj match {
case Property.StringValue(s) s.toJson
case Property.NumberValue(n) n.toJson
case Property.BooleanValue(b) b.toJson
case Property.NullValue JsNull
case Property.StringValue(s) s.toJson
case Property.NumberValue(n) n.toJson
case Property.BooleanValue(b) b.toJson
case Property.JsObjectValue(o) o.toMap.toJson
case Property.JsArrayValue(a) a.toJson
case Property.NullValue JsNull
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/main/scala/com/yetu/siren/model/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ package object model {
* @param value the boolean value
*/
case class BooleanValue(value: Boolean) extends Value
/**
* A property value which has a type JSON object
* @param value Sequence of key(String) - value(siren property value) pairs
*/
case class JsObjectValue(value: Seq[(String, Value)]) extends Value
/**
* A property value which has a type JSON array
* @param value Sequence of Siren property values
*/
case class JsArrayValue(value: Seq[Value]) extends Value

/**
* The property value that represents a non-existing value.
*/
Expand Down
96 changes: 90 additions & 6 deletions src/test/scala/com/yetu/siren/json/JsonBaseSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec {

protected lazy val propsJson = parseJson(propsJsonString)

protected lazy val invalidPropsJson = parseJson(invalidPropsJsonString)
protected lazy val propsWithArrayJson = parseJson(propsJsonStringWithArray)

protected lazy val propsWithComplexArrayJson = parseJson(propsJsonStringWithComplexArray)

protected lazy val propsWithJsonObjectJson = parseJson(propsJsonStringWithNestedJsonObject)

protected lazy val classesJson = parseJson(classesJsonString)

Expand Down Expand Up @@ -135,6 +139,48 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec {
Property("foo", Property.BooleanValue(value = false)),
Property("bar", Property.NullValue))

protected lazy val propsWithArray: Properties = List(
Property("temperature", Property.NumberValue(42)),
Property("mode", Property.NumberValue(3)),
Property("capabilities", Property.JsArrayValue(Seq(
Property.StringValue("dimmable"),
Property.StringValue("switchable"))
)),
Property("status", Property.StringValue("pending")),
Property("isOn", Property.BooleanValue(value = true)))

protected lazy val propsWithComplexArray: Properties = List(
Property("temperature", Property.NumberValue(42)),
Property("mode", Property.NumberValue(3)),
Property("colors", Property.JsArrayValue(Seq(
Property.JsObjectValue(Seq(
"name" -> Property.StringValue("superred"),
"hue" -> Property.NumberValue(42),
"saturation" -> Property.NumberValue(56),
"brightness" -> Property.NumberValue(10)
)),
Property.JsObjectValue(Seq(
"name" -> Property.StringValue("yetugreen"),
"hue" -> Property.NumberValue(45),
"saturation" -> Property.NumberValue(23),
"brightness" -> Property.NumberValue(5)
))
))),
Property("status", Property.StringValue("pending")),
Property("isOn", Property.BooleanValue(value = true)))

protected lazy val propsWithJsonObject: Properties = List(
Property("temperature", Property.NumberValue(42)),
Property("mode", Property.NumberValue(3)),
Property("color", Property.JsObjectValue(Seq(
"name" -> Property.StringValue("superred"),
"hue" -> Property.NumberValue(42),
"saturation" -> Property.NumberValue(56),
"brightness" -> Property.NumberValue(10)
))),
Property("status", Property.StringValue("pending")),
Property("isOn", Property.BooleanValue(value = true)))

protected val properties = List(
Property("orderNumber", Property.NumberValue(42)),
Property("itemCount", Property.NumberValue(3)),
Expand Down Expand Up @@ -173,14 +219,52 @@ trait JsonBaseSpec[JsonBaseType] extends WordSpec {
}
""".stripMargin

protected lazy val invalidPropsJsonString =
protected lazy val propsJsonStringWithArray =
"""
{
"orderNumber": 42,
"itemCount": [],
"temperature": 42,
"mode": 3,
"capabilities" : ["dimmable", "switchable"],
"status": "pending",
"foo": {},
"bar": null
"isOn": true
}
""".stripMargin

protected lazy val propsJsonStringWithComplexArray =
"""
{
"temperature": 42,
"mode": 3,
"colors" : [{
"name" : "superred",
"hue": 42,
"saturation" : 56,
"brightness" : 10
},
{
"name" : "yetugreen",
"hue": 45,
"saturation" : 23,
"brightness" : 5
}],
"status": "pending",
"isOn": true
}
""".stripMargin

protected lazy val propsJsonStringWithNestedJsonObject =
"""
{
"temperature": 42,
"mode": 3,
"color": {
"name" : "superred",
"hue": 42,
"saturation" : 56,
"brightness" : 10
},
"status": "pending",
"isOn": true
}
""".stripMargin

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ class PlayJsonSirenFormatSpec extends JsonBaseSpec[JsValue]
assert(Json.toJson(props) === propsJson)
}

"serialize Siren properties to json Array" in {
assert(Json.toJson(propsWithArray) === propsWithArrayJson)
}

"serialize Siren properties to complex json Array" in {
assert(Json.toJson(propsWithComplexArray) === propsWithComplexArrayJson)
}

"serialize Siren properties to json object" in {
assert(Json.toJson(propsWithJsonObject) === propsWithJsonObjectJson)
}

"deserialize Siren properties" in {
assert(Json.fromJson[Properties](propsJson) === JsSuccess(props))
}

"deserialize Siren properties with json Array" in {
assert(Json.fromJson[Properties](propsWithArrayJson) === JsSuccess(propsWithArray))
}

"deserialize Siren properties with complex json Array" in {
assert(Json.fromJson[Properties](propsWithComplexArrayJson) === JsSuccess(propsWithComplexArray))
}

"deserialize Siren properties with json object" in {
assert(Json.fromJson[Properties](propsWithJsonObjectJson) === JsSuccess(propsWithJsonObject))
}

"serialize Siren classes" in {
assert(Json.toJson(classes) === classesJson)
}
Expand All @@ -47,10 +75,6 @@ class PlayJsonSirenFormatSpec extends JsonBaseSpec[JsValue]
assert(Json.toJson(entity) === entityJson)
}

"deserialize Siren properties" in {
assert(Json.fromJson[Properties](propsJson) === JsSuccess(props))
}

"deserialize Siren classes" in {
assert(Json.fromJson[ImmutableSeq[String]](classesJson) === JsSuccess(classes))
}
Expand Down Expand Up @@ -86,22 +110,13 @@ class PlayJsonSirenFormatSpec extends JsonBaseSpec[JsValue]
assert(Json.fromJson[Action.Method](JsString("foo")).isError)
assert(Json.fromJson[Action.Method](JsNumber(23)).isError)
}
"fail to deserialize invalid Siren properties collecting all errors" in {
val result = Json.fromJson[Properties](invalidPropsJson)
inside(result) {
case JsError(errors)
assert(errors.size === 2)
assert(errors.exists(_._1 === JsPath \ "itemCount"))
assert(errors.exists(_._1 === JsPath \ "foo"))
}
}

"fail to deserialize invalid Siren embedded representation collecting all errors" in {
// properties allow json object so the only error is wrong type in actions
val result = Json.fromJson[Entity.EmbeddedRepresentation](invalidEmbeddedRepresentationJson)
inside(result) {
case JsError(errors)
assert(errors.size === 3)
assert(errors.exists(_._1 === JsPath \ "properties" \ "customerId"))
assert(errors.exists(_._1 === JsPath \ "properties" \ "name"))
assert(errors.size === 1)
assert(errors.exists(_._1 === (JsPath \ "actions")(0) \ "name"))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,27 @@ class SirenJsonFormatSpec extends JsonBaseSpec[JsValue] with MustMatchers with S
"serialize Siren properties" in {
props.toJson mustEqual propsJson
}
"serialize Siren properties with array" in {
propsWithArray.toJson mustEqual propsWithArrayJson
}
"serialize Siren properties with complex array" in {
propsWithComplexArray.toJson mustEqual propsWithComplexArrayJson
}
"serialize Siren properties with json object" in {
propsWithJsonObject.toJson mustEqual propsWithJsonObjectJson
}
"deserialize Siren properties" in {
propsJson.convertTo[Properties] must contain theSameElementsAs props
}
"deserialize Siren properties with array" in {
propsWithArrayJson.convertTo[Properties] must contain theSameElementsAs propsWithArray
}
"deserialize Siren properties with complex array" in {
propsWithComplexArrayJson.convertTo[Properties] must contain theSameElementsAs propsWithComplexArray
}
"deserialize Siren properties with json object" in {
propsWithJsonObjectJson.convertTo[Properties] must contain theSameElementsAs propsWithJsonObject
}
"serialize Siren classes" in {
classes.toJson mustEqual classesJson
}
Expand All @@ -60,6 +78,7 @@ class SirenJsonFormatSpec extends JsonBaseSpec[JsValue] with MustMatchers with S
"deserialize a Siren embedded representation" in {
embeddedRepresentationJson.convertTo[Entity.EmbeddedRepresentation] mustEqual embeddedRepresentation
}

"serialize a Siren action method" in {
val method: Action.Method = Action.Method.GET
method.toJson mustEqual JsString("GET")
Expand Down Expand Up @@ -96,9 +115,6 @@ class SirenJsonFormatSpec extends JsonBaseSpec[JsValue] with MustMatchers with S
"deserialize non-array json" in {
intercept[DeserializationException] { """{ "foo": "bar" }""".parseJson.convertTo[Seq[Link]] }
}
"deserialize wrong type of Siren properties" in {
intercept[DeserializationException] { """{ "xyz" : { "foo": "bar" } }""".parseJson.convertTo[Properties] }
}
}
"serialize a complete Siren entity with embedded linked and fully represented sub-entities correctly" in {
entity.toJson mustEqual entityJson
Expand Down

0 comments on commit 3264894

Please sign in to comment.