From 94b1fba92d40568c642a0b86c719c4bcc0865b54 Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 6 Dec 2011 13:22:37 +0100 Subject: Remove JsField, turn JsObject(List[JsField]) into JsObject(Map[String, JsValue]) --- .../scala/cc/spray/json/CollectionFormats.scala | 9 +++---- src/main/scala/cc/spray/json/CompactPrinter.scala | 6 ++--- .../cc/spray/json/DeserializationException.scala | 2 +- src/main/scala/cc/spray/json/JsValue.scala | 21 ++++++---------- src/main/scala/cc/spray/json/JsonParser.scala | 4 ++-- src/main/scala/cc/spray/json/PrettyPrinter.scala | 6 ++--- src/main/scala/cc/spray/json/ProductFormats.scala | 28 +++++++++++----------- src/main/scala/cc/spray/json/package.scala | 2 ++ .../cc/spray/json/AdditionalFormatsSpec.scala | 4 ++-- .../cc/spray/json/CollectionFormatsSpec.scala | 2 +- .../scala/cc/spray/json/CompactPrinterSpec.scala | 4 ++-- src/test/scala/cc/spray/json/JsonParserSpec.scala | 6 ++--- .../scala/cc/spray/json/ProductFormatsSpec.scala | 18 +++++++------- src/test/scala/cc/spray/json/ReadmeSpec.scala | 2 +- 14 files changed, 55 insertions(+), 59 deletions(-) diff --git a/src/main/scala/cc/spray/json/CollectionFormats.scala b/src/main/scala/cc/spray/json/CollectionFormats.scala index a17c43f..c6c3403 100644 --- a/src/main/scala/cc/spray/json/CollectionFormats.scala +++ b/src/main/scala/cc/spray/json/CollectionFormats.scala @@ -47,15 +47,16 @@ trait CollectionFormats { */ implicit def mapFormat[K :JsonFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { def write(m: Map[K, V]) = JsObject { - m.toList.map { t => - t._1.toJson match { - case JsString(x) => JsField(x, t._2.toJson) + m.map { field => + field._1.toJson match { + case JsString(x) => x -> field._2.toJson case x => throw new SerializationException("Map key must be formatted as JsString, not '" + x + "'") } } } def read(value: JsValue) = value match { - case JsObject(fields) => fields.map(field => (JsString(field.name).convertTo[K], field.value.convertTo[V])).toMap + case JsObject(fields) => + fields.map(field => (JsString(field._1).convertTo[K], field._2.convertTo[V])) (collection.breakOut) case x => throw new DeserializationException("Expected Map as JsObject, but got " + x) } } diff --git a/src/main/scala/cc/spray/json/CompactPrinter.scala b/src/main/scala/cc/spray/json/CompactPrinter.scala index 329c08e..467160d 100644 --- a/src/main/scala/cc/spray/json/CompactPrinter.scala +++ b/src/main/scala/cc/spray/json/CompactPrinter.scala @@ -31,12 +31,12 @@ trait CompactPrinter extends JsonPrinter { } } - private def printObject(members: List[JsField], sb: StringBuilder) { + private def printObject(members: Map[String, JsValue], sb: StringBuilder) { sb.append('{') printSeq(members, sb.append(',')) { m => - printString(m.name, sb) + printString(m._1, sb) sb.append(':') - print(m.value, sb) + print(m._2, sb) } sb.append('}') } diff --git a/src/main/scala/cc/spray/json/DeserializationException.scala b/src/main/scala/cc/spray/json/DeserializationException.scala index 283d996..b317a14 100644 --- a/src/main/scala/cc/spray/json/DeserializationException.scala +++ b/src/main/scala/cc/spray/json/DeserializationException.scala @@ -16,4 +16,4 @@ package cc.spray.json -class DeserializationException(msg: String) extends RuntimeException(msg) \ No newline at end of file +class DeserializationException(msg: String, cause: Throwable = null) extends RuntimeException(msg, cause) \ No newline at end of file diff --git a/src/main/scala/cc/spray/json/JsValue.scala b/src/main/scala/cc/spray/json/JsValue.scala index cc1af91..f4dbf91 100644 --- a/src/main/scala/cc/spray/json/JsValue.scala +++ b/src/main/scala/cc/spray/json/JsValue.scala @@ -18,7 +18,9 @@ package cc.spray.json -import collection.mutable.ListBuffer +import collection.mutable.{LinkedHashMap, ListBuffer} +import collection.immutable.ListMap + /** * The general type of a JSON AST node. @@ -37,22 +39,13 @@ sealed trait JsValue { /** * A JSON object. */ -case class JsObject(fields: List[JsField]) extends JsValue { - lazy val asMap: Map[String, JsValue] = { - val b = Map.newBuilder[String, JsValue] - for (JsField(name, value) <- fields) b += ((name, value)) - b.result() - } -} +case class JsObject(fields: Map[String, JsValue]) extends JsValue object JsObject { - def apply(members: JsField*) = new JsObject(members.toList) + // we use a ListMap in order to preserve the field order + def apply(members: JsField*) = new JsObject(ListMap(members: _*)) + def apply(members: List[JsField]) = new JsObject(ListMap(members: _*)) } -/** - * The members/fields of a JSON object. - */ -case class JsField(name: String, value: JsValue) extends JsValue - /** * A JSON array. */ diff --git a/src/main/scala/cc/spray/json/JsonParser.scala b/src/main/scala/cc/spray/json/JsonParser.scala index b93a9a6..21b1d68 100644 --- a/src/main/scala/cc/spray/json/JsonParser.scala +++ b/src/main/scala/cc/spray/json/JsonParser.scala @@ -31,10 +31,10 @@ object JsonParser extends Parser { def Json = rule { WhiteSpace ~ Value ~ EOI } def JsonObject: Rule1[JsObject] = rule { - "{ " ~ zeroOrMore(Pair, separator = ", ") ~ "} " ~~> (JsObject(_)) + "{ " ~ zeroOrMore(Pair, separator = ", ") ~ "} " ~~> (JsObject(_ :_*)) } - def Pair = rule { JsonStringUnwrapped ~ ": " ~ Value ~~> (JsField(_, _)) } + def Pair = rule { JsonStringUnwrapped ~ ": " ~ Value ~~> ((_, _)) } def Value: Rule1[JsValue] = rule { JsonString | JsonNumber | JsonObject | JsonArray | JsonTrue | JsonFalse | JsonNull diff --git a/src/main/scala/cc/spray/json/PrettyPrinter.scala b/src/main/scala/cc/spray/json/PrettyPrinter.scala index 93ac0b6..429c984 100644 --- a/src/main/scala/cc/spray/json/PrettyPrinter.scala +++ b/src/main/scala/cc/spray/json/PrettyPrinter.scala @@ -37,13 +37,13 @@ trait PrettyPrinter extends JsonPrinter { } } - private def printObject(members: List[JsField], sb: StringBuilder, indent: Int) { + private def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int) { sb.append("{\n") printSeq(members, sb.append(",\n")) { m => printIndent(sb, indent + Indent) - printString(m.name, sb) + printString(m._1, sb) sb.append(": ") - print(m.value, sb, indent + Indent) + print(m._2, sb, indent + Indent) } sb.append('\n') printIndent(sb, indent) diff --git a/src/main/scala/cc/spray/json/ProductFormats.scala b/src/main/scala/cc/spray/json/ProductFormats.scala index 90c3349..810a6ec 100644 --- a/src/main/scala/cc/spray/json/ProductFormats.scala +++ b/src/main/scala/cc/spray/json/ProductFormats.scala @@ -401,24 +401,24 @@ trait ProductFormats { val value = p.productElement(ix).asInstanceOf[T] writer match { case _: OptionFormat[_] if (value == None) => rest - case _ => JsField(fieldName, writer.write(value)) :: rest + case _ => (fieldName, writer.write(value)) :: rest } } private def fromField[T](value: JsValue, fieldName: String)(implicit reader: JsonReader[T]) = { - @tailrec - def getFrom(fields: List[JsField]): T = { - if (fields.isEmpty) { - if (reader.isInstanceOf[OptionFormat[_]]) None.asInstanceOf[T] - else throw new DeserializationException("Object is missing required member '" + fieldName + "'") - } else if (fields.head.name == fieldName) { - reader.read(fields.head.value) - } else { - getFrom(fields.tail) - } - } value match { - case x: JsObject => getFrom(x.fields) + case x: JsObject => + var fieldFound = false + try { + val fieldValue = x.fields(fieldName) + fieldFound = true + reader.read(fieldValue) + } + catch { + case e: NoSuchElementException if !fieldFound => + if (reader.isInstanceOf[OptionFormat[_]]) None.asInstanceOf[T] + else throw new DeserializationException("Object is missing required member '" + fieldName + "'", e) + } case _ => throw new DeserializationException("Object expected") } } @@ -437,6 +437,6 @@ trait NullOptions extends ProductFormats { override protected def productElement2Field[T](fieldName: String, p: Product, ix: Int, rest: List[JsField]) (implicit writer: JsonWriter[T]) = { val value = p.productElement(ix).asInstanceOf[T] - JsField(fieldName, writer.write(value)) :: rest + (fieldName, writer.write(value)) :: rest } } \ No newline at end of file diff --git a/src/main/scala/cc/spray/json/package.scala b/src/main/scala/cc/spray/json/package.scala index ee50ec0..f080312 100644 --- a/src/main/scala/cc/spray/json/package.scala +++ b/src/main/scala/cc/spray/json/package.scala @@ -17,6 +17,8 @@ package cc.spray package object json { + + type JsField = (String, JsValue) def jsonReader[T](implicit reader: JsonReader[T]) = reader def jsonWriter[T](implicit writer: JsonWriter[T]) = writer diff --git a/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala b/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala index 1f5362a..a9fb2d1 100644 --- a/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala @@ -10,7 +10,7 @@ class AdditionalFormatsSpec extends Specification { implicit def containerReader[T :JsonFormat] = lift { new JsonReader[Container[T]] { def read(value: JsValue) = value match { - case JsObject(JsField("content", obj: JsValue) :: Nil) => Container(Some(jsonReader[T].read(obj))) + case JsObject(fields) if fields.contains("content") => Container(Some(jsonReader[T].read(fields("content")))) case _ => throw new DeserializationException("Unexpected format: " + value.toString) } } @@ -20,7 +20,7 @@ class AdditionalFormatsSpec extends Specification { object WriterProtocol extends DefaultJsonProtocol { implicit def containerWriter[T :JsonFormat] = lift { new JsonWriter[Container[T]] { - def write(obj: Container[T]) = JsObject(JsField("content", obj.inner.toJson)) + def write(obj: Container[T]) = JsObject("content" -> obj.inner.toJson) } } } diff --git a/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala b/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala index f5eafa8..fe5e4bb 100644 --- a/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala @@ -29,7 +29,7 @@ class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { "The mapFormat" should { val map = Map("a" -> 1, "b" -> 2, "c" -> 3) - val json = JsObject(JsField("a", JsNumber(1)), JsField("b", JsNumber(2)), JsField("c", JsNumber(3))) + val json = JsObject("a" -> JsNumber(1), "b" -> JsNumber(2), "c" -> JsNumber(3)) "convert a Map[String, Long] to a JsObject" in { map.toJson mustEqual json } diff --git a/src/test/scala/cc/spray/json/CompactPrinterSpec.scala b/src/test/scala/cc/spray/json/CompactPrinterSpec.scala index f2d92fc..948ee4d 100644 --- a/src/test/scala/cc/spray/json/CompactPrinterSpec.scala +++ b/src/test/scala/cc/spray/json/CompactPrinterSpec.scala @@ -36,11 +36,11 @@ class CompactPrinterSpec extends Specification { CompactPrinter(JsString("\"\\\b\f\n\r\t\u12AB")) mustEqual """"\"\\\b\f\n\r\t""" + "\\u12ab\"" } "properly print a simple JsObject" in ( - CompactPrinter(JsObject(JsField("key", JsNumber(42)), JsField("key2", JsString("value")))) + CompactPrinter(JsObject("key" -> JsNumber(42), "key2" -> JsString("value"))) mustEqual """{"key":42,"key2":"value"}""" ) "properly print a simple JsArray" in ( - CompactPrinter(JsArray(JsNull, JsNumber(1.23), JsObject(JsField("key", JsBoolean(true))))) + CompactPrinter(JsArray(JsNull, JsNumber(1.23), JsObject("key" -> JsBoolean(true)))) mustEqual """[null,1.23,{"key":true}]""" ) "properly print a JSON padding (JSONP) if requested" in { diff --git a/src/test/scala/cc/spray/json/JsonParserSpec.scala b/src/test/scala/cc/spray/json/JsonParserSpec.scala index f030013..c298617 100644 --- a/src/test/scala/cc/spray/json/JsonParserSpec.scala +++ b/src/test/scala/cc/spray/json/JsonParserSpec.scala @@ -35,16 +35,16 @@ class JsonParserSpec extends Specification { } "properly parse a simple JsObject" in ( JsonParser(""" { "key" :42, "key2": "value" }""") mustEqual - JsObject(JsField("key", JsNumber(42)), JsField("key2", JsString("value"))) + JsObject("key" -> JsNumber(42), "key2" -> JsString("value")) ) "properly parse a simple JsArray" in ( JsonParser("""[null, 1.23 ,{"key":true } ] """) mustEqual - JsArray(JsNull, JsNumber(1.23), JsObject(JsField("key", JsBoolean(true)))) + JsArray(JsNull, JsNumber(1.23), JsObject("key" -> JsBoolean(true))) ) "be reentrant" in { val largeJsonSource = FileUtils.readAllCharsFromResource("test.json") List.fill(20)(largeJsonSource).par.map(JsonParser(_)).toList.map { - _.asInstanceOf[JsObject].asMap("questions").asInstanceOf[JsArray].elements.size + _.asInstanceOf[JsObject].fields("questions").asInstanceOf[JsArray].elements.size } mustEqual List.fill(20)(100) } } diff --git a/src/test/scala/cc/spray/json/ProductFormatsSpec.scala b/src/test/scala/cc/spray/json/ProductFormatsSpec.scala index 059b931..6e389c7 100644 --- a/src/test/scala/cc/spray/json/ProductFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/ProductFormatsSpec.scala @@ -18,7 +18,7 @@ class ProductFormatsSpec extends Specification { "A JsonFormat created with `jsonFormat`, for a case class with 2 elements," should { import TestProtocol1._ val obj = Test2(42, Some(4.2)) - val json = JsObject(JsField("a", JsNumber(42)), JsField("b", JsNumber(4.2))) + val json = JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2)) "convert to a respective JsObject" in { obj.toJson mustEqual json } @@ -26,20 +26,20 @@ class ProductFormatsSpec extends Specification { json.convertTo[Test2] mustEqual obj } "throw a DeserializationException if the JsObject does not all required members" in ( - JsObject(JsField("b", JsNumber(4.2))).convertTo[Test2] must + JsObject("b" -> JsNumber(4.2)).convertTo[Test2] must throwA(new DeserializationException("Object is missing required member 'a'")) ) "not require the presence of optional fields for deserialization" in { - JsObject(JsField("a", JsNumber(42))).convertTo[Test2] mustEqual Test2(42, None) + JsObject("a" -> JsNumber(42)).convertTo[Test2] mustEqual Test2(42, None) } "not render `None` members during serialization" in { - Test2(42, None).toJson mustEqual JsObject(JsField("a", JsNumber(42))) + Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42)) } "ignore additional members during deserialization" in { - JsObject(JsField("a", JsNumber(42)), JsField("b", JsNumber(4.2)), JsField("c", JsString('no))).convertTo[Test2] mustEqual obj + JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2), "c" -> JsString('no)).convertTo[Test2] mustEqual obj } "not depend on any specific member order for deserialization" in { - JsObject(JsField("b", JsNumber(4.2)), JsField("a", JsNumber(42))).convertTo[Test2] mustEqual obj + JsObject("b" -> JsNumber(4.2), "a" -> JsNumber(42)).convertTo[Test2] mustEqual obj } "throw a DeserializationException if the JsValue is not a JsObject" in ( JsNull.convertTo[Test2] must throwA(new DeserializationException("Object expected")) @@ -49,7 +49,7 @@ class ProductFormatsSpec extends Specification { "A JsonProtocol mixing in NullOptions" should { "render `None` members to `null`" in { import TestProtocol2._ - Test2(42, None).toJson mustEqual JsObject(JsField("a", JsNumber(42)), JsField("b", JsNull)) + Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull) } } @@ -57,8 +57,8 @@ class ProductFormatsSpec extends Specification { import TestProtocol1._ val obj = Test3(42 :: 43 :: Nil, "x" :: "y" :: "z" :: Nil) val json = JsObject( - JsField("as", JsArray(JsNumber(42), JsNumber(43))), - JsField("bs", JsArray(JsString("x"), JsString("y"), JsString("z"))) + "as" -> JsArray(JsNumber(42), JsNumber(43)), + "bs" -> JsArray(JsString("x"), JsString("y"), JsString("z")) ) "convert to a respective JsObject" in { obj.toJson mustEqual json diff --git a/src/test/scala/cc/spray/json/ReadmeSpec.scala b/src/test/scala/cc/spray/json/ReadmeSpec.scala index b1c6674..ace94b1 100644 --- a/src/test/scala/cc/spray/json/ReadmeSpec.scala +++ b/src/test/scala/cc/spray/json/ReadmeSpec.scala @@ -10,7 +10,7 @@ class ReadmeSpec extends Specification { val json = """{ "some": "JSON source" }""" val jsonAst = JsonParser(json) - jsonAst mustEqual JsObject(JsField("some", JsString("JSON source"))) + jsonAst mustEqual JsObject("some" -> JsString("JSON source")) val json2 = PrettyPrinter(jsonAst) json2 mustEqual -- cgit v1.2.3