From 582582ef109dd5d84a258275e2d8f784ce57704c Mon Sep 17 00:00:00 2001 From: Cristian Vrabie Date: Mon, 5 Aug 2013 19:30:31 +0100 Subject: [PATCH] Added selective field getters for JsObject --- src/main/scala/spray/json/JsValue.scala | 28 ++++++++++++ .../scala/spray/json/CustomFormatSpec.scala | 44 ++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/main/scala/spray/json/JsValue.scala b/src/main/scala/spray/json/JsValue.scala index 8b25c980..34a61501 100644 --- a/src/main/scala/spray/json/JsValue.scala +++ b/src/main/scala/spray/json/JsValue.scala @@ -51,6 +51,34 @@ sealed abstract class JsValue { case class JsObject(fields: Map[String, JsValue]) extends JsValue { override def asJsObject(errorMsg: String) = this def getFields(fieldNames: String*): Seq[JsValue] = fieldNames.flatMap(fields.get) + + def getField(fieldName: String):Option[JsValue] = fields.get(fieldName) + + def getString(fieldName: String):Either[String, String] = fields.get(fieldName) match { + case Some(JsString(str)) => Right(str) + case Some(x) => Left("Expected a JsString in field %s but got %s".format(fieldName, x)) + case None => Left("Field %s is missing".format(fieldName)) + } + + def getNumber(fieldName: String):Either[String, BigDecimal] = fields.get(fieldName) match { + case Some(JsNumber(num)) => Right(num) + case Some(x) => Left("Expected a JsNumber in field %s but got %s".format(fieldName, x)) + case None => Left("Field %s is missing".format(fieldName)) + } + + def getInt(fieldName: String):Either[String, Int] = getNumber(fieldName).right.map(_.toInt) + + def getLong(fieldName: String):Either[String, Long] = getNumber(fieldName).right.map(_.toLong) + + def getFloat(fieldName: String):Either[String, Double] = getNumber(fieldName).right.map(_.toFloat) + + def getDouble(fieldName: String):Either[String, Double] = getNumber(fieldName).right.map(_.toDouble) + + def getBoolean(fieldName: String):Either[String, Boolean] = fields.get(fieldName) match { + case Some(JsBoolean(bool)) => Right(bool) + case Some(x) => Left("Expected a JsBoolean in field %s but got %s".format(fieldName, x)) + case None => Left("Field %s is missing".format(fieldName)) + } } object JsObject { // we use a ListMap in order to preserve the field order diff --git a/src/test/scala/spray/json/CustomFormatSpec.scala b/src/test/scala/spray/json/CustomFormatSpec.scala index dcea4f2d..8ebe2924 100644 --- a/src/test/scala/spray/json/CustomFormatSpec.scala +++ b/src/test/scala/spray/json/CustomFormatSpec.scala @@ -22,7 +22,7 @@ class CustomFormatSpec extends Specification with DefaultJsonProtocol { case class MyType(name: String, value: Int) - implicit val MyTypeProtocol = new RootJsonFormat[MyType] { + val MyTypeProtocol = new RootJsonFormat[MyType] { def read(json: JsValue) = { json.asJsObject.getFields("name", "value") match { case Seq(JsString(name), JsNumber(value)) => MyType(name, value.toInt) @@ -32,14 +32,56 @@ class CustomFormatSpec extends Specification with DefaultJsonProtocol { def write(obj: MyType) = JsObject("name" -> JsString(obj.name), "value" -> JsNumber(obj.value)) } + val MyTypeProtocolWithOptionals = new RootJsonFormat[MyType] { + + def maybeJsObject(json:JsValue):Either[String,JsObject] = + try{ Right(json.asJsObject) } + catch{ case e:Throwable => Left(e.getMessage)} + + //this could be more elegant with a for comprehension but currently definitions are not allowed with either + //https://issues.scala-lang.org/browse/SI-5793 + def maybeMyType(json:JsValue):Either[String,MyType] = maybeJsObject(json).right.flatMap{ obj => + obj.getString("name").right.map{ name =>{ + val value = obj.getInt("value").right.getOrElse(42) + MyType(name,value) + }} + } + + def read(json: JsValue) = maybeMyType(json) match { + case Right(my) => my + case Left(error) => deserializationError(error) + } + + def write(obj: MyType) = JsObject("name" -> JsString(obj.name), "value" -> JsNumber(obj.value)) + } + "A custom JsonFormat built with 'asJsonObject'" should { val value = MyType("bob", 42) + "correctly deserialize valid JSON content" in { + implicit val format = MyTypeProtocol """{ "name": "bob", "value": 42 }""".asJson.convertTo[MyType] mustEqual value } + "support full round-trip (de)serialization" in { + implicit val format = MyTypeProtocol value.toJson.convertTo[MyType] mustEqual value } + + "support optional parameters" in { + implicit val format = MyTypeProtocolWithOptionals + """{ "name": "bob" }""".asJson.convertTo[MyType] mustEqual value + } + + "allow overriding default parameters" in{ + implicit val format = MyTypeProtocolWithOptionals + """{ "name": "mike", "value": 123 }""".asJson.convertTo[MyType] mustEqual MyType("mike", 123) + } + + "throw an exception if a required field is mission" in{ + implicit val format = MyTypeProtocolWithOptionals + """{ "value": 123 }""".asJson.convertTo[MyType] must throwA(new DeserializationException("Field name is missing")) + } } } \ No newline at end of file