diff options
Diffstat (limited to 'shared/src/test/scala/spray')
11 files changed, 956 insertions, 0 deletions
diff --git a/shared/src/test/scala/spray/json/AdditionalFormatsSpec.scala b/shared/src/test/scala/spray/json/AdditionalFormatsSpec.scala new file mode 100644 index 0000000..01127e6 --- /dev/null +++ b/shared/src/test/scala/spray/json/AdditionalFormatsSpec.scala @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable._ + +class AdditionalFormatsSpec extends Specification { + + case class Container[A](inner: Option[A]) + + object ReaderProtocol extends DefaultJsonProtocol { + implicit def containerReader[T :JsonFormat] = lift { + new JsonReader[Container[T]] { + def read(value: JsValue) = value match { + case JsObject(fields) if fields.contains("content") => Container(Some(jsonReader[T].read(fields("content")))) + case _ => deserializationError("Unexpected format: " + value.toString) + } + } + } + } + + object WriterProtocol extends DefaultJsonProtocol { + implicit def containerWriter[T :JsonFormat] = lift { + new JsonWriter[Container[T]] { + def write(obj: Container[T]) = JsObject("content" -> obj.inner.toJson) + } + } + } + + "The liftJsonWriter" should { + val obj = Container(Some(Container(Some(List(1, 2, 3))))) + + "properly write a Container[Container[List[Int]]] to JSON" in { + import WriterProtocol._ + obj.toJson.toString mustEqual """{"content":{"content":[1,2,3]}}""" + } + + "properly read a Container[Container[List[Int]]] from JSON" in { + import ReaderProtocol._ + """{"content":{"content":[1,2,3]}}""".parseJson.convertTo[Container[Container[List[Int]]]] mustEqual obj + } + } + + case class Foo(id: Long, name: String, foos: Option[List[Foo]] = None) + + object FooProtocol extends DefaultJsonProtocol { + implicit val fooProtocol: JsonFormat[Foo] = lazyFormat(jsonFormat(Foo, "id", "name", "foos")) + } + + "The lazyFormat wrapper" should { + "enable recursive format definitions" in { + import FooProtocol._ + val json = Foo(1, "a", Some(Foo(2, "b", Some(Foo(3, "c") :: Nil)) :: Foo(4, "d") :: Nil)).toJson + + json mustEqual + """{"id":1,"name":"a","foos":[{"id":2,"name":"b","foos":[{"id":3,"name":"c"}]},{"id":4,"name":"d"}]}""".parseJson + } + } +}
\ No newline at end of file diff --git a/shared/src/test/scala/spray/json/BasicFormatsSpec.scala b/shared/src/test/scala/spray/json/BasicFormatsSpec.scala new file mode 100644 index 0000000..454e1cc --- /dev/null +++ b/shared/src/test/scala/spray/json/BasicFormatsSpec.scala @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable._ + +class BasicFormatsSpec extends Specification with DefaultJsonProtocol { + + "The IntJsonFormat" should { + "convert an Int to a JsNumber" in { + 42.toJson mustEqual JsNumber(42) + } + "convert a JsNumber to an Int" in { + JsNumber(42).convertTo[Int] mustEqual 42 + } + } + + "The LongJsonFormat" should { + "convert a Long to a JsNumber" in { + 7563661897011259335L.toJson mustEqual JsNumber(7563661897011259335L) + } + "convert a JsNumber to a Long" in { + JsNumber(7563661897011259335L).convertTo[Long] mustEqual 7563661897011259335L + } + } + + "The FloatJsonFormat" should { + "convert a Float to a JsNumber" in { + 4.2f.toJson mustEqual JsNumber(4.2f) + } + "convert a Float.NaN to a JsNull" in { + Float.NaN.toJson mustEqual JsNull + } + "convert a Float.PositiveInfinity to a JsNull" in { + Float.PositiveInfinity.toJson mustEqual JsNull + } + "convert a Float.NegativeInfinity to a JsNull" in { + Float.NegativeInfinity.toJson mustEqual JsNull + } + "convert a JsNumber to a Float" in { + JsNumber(4.2f).convertTo[Float] mustEqual 4.2f + } + "convert a JsNull to a Float" in { + JsNull.convertTo[Float].isNaN mustEqual Float.NaN.isNaN + } + } + + "The DoubleJsonFormat" should { + "convert a Double to a JsNumber" in { + 4.2.toJson mustEqual JsNumber(4.2) + } + "convert a Double.NaN to a JsNull" in { + Double.NaN.toJson mustEqual JsNull + } + "convert a Double.PositiveInfinity to a JsNull" in { + Double.PositiveInfinity.toJson mustEqual JsNull + } + "convert a Double.NegativeInfinity to a JsNull" in { + Double.NegativeInfinity.toJson mustEqual JsNull + } + "convert a JsNumber to a Double" in { + JsNumber(4.2).convertTo[Double] mustEqual 4.2 + } + "convert a JsNull to a Double" in { + JsNull.convertTo[Double].isNaN mustEqual Double.NaN.isNaN + } + } + + "The ByteJsonFormat" should { + "convert a Byte to a JsNumber" in { + 42.asInstanceOf[Byte].toJson mustEqual JsNumber(42) + } + "convert a JsNumber to a Byte" in { + JsNumber(42).convertTo[Byte] mustEqual 42 + } + } + + "The ShortJsonFormat" should { + "convert a Short to a JsNumber" in { + 42.asInstanceOf[Short].toJson mustEqual JsNumber(42) + } + "convert a JsNumber to a Short" in { + JsNumber(42).convertTo[Short] mustEqual 42 + } + } + + "The BigDecimalJsonFormat" should { + "convert a BigDecimal to a JsNumber" in { + BigDecimal(42).toJson mustEqual JsNumber(42) + } + "convert a JsNumber to a BigDecimal" in { + JsNumber(42).convertTo[BigDecimal] mustEqual BigDecimal(42) + } + """convert a JsString to a BigDecimal (to allow the quoted-large-numbers pattern)""" in { + JsString("9223372036854775809").convertTo[BigDecimal] mustEqual BigDecimal("9223372036854775809") + } + } + + "The BigIntJsonFormat" should { + "convert a BigInt to a JsNumber" in { + BigInt(42).toJson mustEqual JsNumber(42) + } + "convert a JsNumber to a BigInt" in { + JsNumber(42).convertTo[BigInt] mustEqual BigInt(42) + } + """convert a JsString to a BigInt (to allow the quoted-large-numbers pattern)""" in { + JsString("9223372036854775809").convertTo[BigInt] mustEqual BigInt("9223372036854775809") + } + } + + "The UnitJsonFormat" should { + "convert Unit to a JsNumber(1)" in { + ().toJson mustEqual JsNumber(1) + } + "convert a JsNumber to Unit" in { + JsNumber(1).convertTo[Unit] mustEqual (()) + } + } + + "The BooleanJsonFormat" should { + "convert true to a JsTrue" in { true.toJson mustEqual JsTrue } + "convert false to a JsFalse" in { false.toJson mustEqual JsFalse } + "convert a JsTrue to true" in { JsTrue.convertTo[Boolean] mustEqual true } + "convert a JsFalse to false" in { JsFalse.convertTo[Boolean] mustEqual false } + } + + "The CharJsonFormat" should { + "convert a Char to a JsString" in { + 'c'.toJson mustEqual JsString("c") + } + "convert a JsString to a Char" in { + JsString("c").convertTo[Char] mustEqual 'c' + } + } + + "The StringJsonFormat" should { + "convert a String to a JsString" in { + "Hello".toJson mustEqual JsString("Hello") + } + "convert a JsString to a String" in { + JsString("Hello").convertTo[String] mustEqual "Hello" + } + } + + "The SymbolJsonFormat" should { + "convert a Symbol to a JsString" in { + Symbol("Hello").toJson mustEqual JsString("Hello") + } + "convert a JsString to a Symbol" in { + JsString("Hello").convertTo[Symbol] mustEqual Symbol("Hello") + } + } + +} diff --git a/shared/src/test/scala/spray/json/CollectionFormatsSpec.scala b/shared/src/test/scala/spray/json/CollectionFormatsSpec.scala new file mode 100644 index 0000000..9d6970b --- /dev/null +++ b/shared/src/test/scala/spray/json/CollectionFormatsSpec.scala @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable._ +import java.util.Arrays + +class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { + + "The listFormat" should { + val list = List(1, 2, 3) + val json = JsArray(JsNumber(1), JsNumber(2), JsNumber(3)) + "convert a List[Int] to a JsArray of JsNumbers" in { + list.toJson mustEqual json + } + "convert a JsArray of JsNumbers to a List[Int]" in { + json.convertTo[List[Int]] mustEqual list + } + } + + "The arrayFormat" should { + val array = Array(1, 2, 3) + val json = JsArray(JsNumber(1), JsNumber(2), JsNumber(3)) + "convert an Array[Int] to a JsArray of JsNumbers" in { + array.toJson mustEqual json + } + "convert a JsArray of JsNumbers to an Array[Int]" in { + Arrays.equals(json.convertTo[Array[Int]], array) must beTrue + } + } + + "The mapFormat" should { + val map = Map("a" -> 1, "b" -> 2, "c" -> 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 + } + "be able to convert a JsObject to a Map[String, Long]" in { + json.convertTo[Map[String, Long]] mustEqual map + } + "throw an Exception when trying to serialize a map whose key are not serialized to JsStrings" in { + Map(1 -> "a").toJson must throwA(new SerializationException("Map key must be formatted as JsString, not '1'")) + } + } + + "The immutableSetFormat" should { + val set = Set(1, 2, 3) + val numbers = Set(JsNumber(1), JsNumber(2), JsNumber(3)) + "convert a Set[Int] to a JsArray of JsNumbers" in { + set.toJson.asInstanceOf[JsArray].elements.toSet mustEqual numbers + } + "convert a JsArray of JsNumbers to a Set[Int]" in { + JsArray(numbers.toVector).convertTo[Set[Int]] mustEqual set + } + } + + "The indexedSeqFormat" should { + val seq = collection.IndexedSeq(1, 2, 3) + val json = JsArray(JsNumber(1), JsNumber(2), JsNumber(3)) + "convert a Set[Int] to a JsArray of JsNumbers" in { + seq.toJson mustEqual json + } + "convert a JsArray of JsNumbers to a IndexedSeq[Int]" in { + json.convertTo[collection.IndexedSeq[Int]] mustEqual seq + } + } + +}
\ No newline at end of file diff --git a/shared/src/test/scala/spray/json/CompactPrinterSpec.scala b/shared/src/test/scala/spray/json/CompactPrinterSpec.scala new file mode 100644 index 0000000..691daa9 --- /dev/null +++ b/shared/src/test/scala/spray/json/CompactPrinterSpec.scala @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable._ + +class CompactPrinterSpec extends Specification { + + "The CompactPrinter" should { + "print JsNull to 'null'" in { + CompactPrinter(JsNull) mustEqual "null" + } + "print JsTrue to 'true'" in { + CompactPrinter(JsTrue) mustEqual "true" + } + "print JsFalse to 'false'" in { + CompactPrinter(JsFalse) mustEqual "false" + } + "print JsNumber(0) to '0'" in { + CompactPrinter(JsNumber(0)) mustEqual "0" + } + "print JsNumber(1.23) to '1.23'" in { + CompactPrinter(JsNumber(1.23)) mustEqual "1.23" + } + "print JsNumber(1.23) to '1.23'" in { + CompactPrinter(JsNumber(1.23)) mustEqual "1.23" + } + "print JsNumber(12.34e-10) to '12.34e-10'" in { + CompactPrinter(JsNumber(12.34e-10)) mustEqual "1.234E-9" + } + "print JsString(\"xyz\") to \"xyz\"" in { + CompactPrinter(JsString("xyz")) mustEqual "\"xyz\"" + } + "properly escape special chars in JsString" in { + CompactPrinter(JsString("\"\\\b\f\n\r\t")) mustEqual """"\"\\\b\f\n\r\t"""" + CompactPrinter(JsString("\u1000")) mustEqual "\"\u1000\"" + CompactPrinter(JsString("\u0100")) mustEqual "\"\u0100\"" + CompactPrinter(JsString("\u0010")) mustEqual "\"\\u0010\"" + CompactPrinter(JsString("\u0001")) mustEqual "\"\\u0001\"" + CompactPrinter(JsString("\u001e")) mustEqual "\"\\u001e\"" + // don't escape as it isn't required by the spec + CompactPrinter(JsString("\u007f")) mustEqual "\"\u007f\"" + CompactPrinter(JsString("飞机因此受到损伤")) mustEqual "\"飞机因此受到损伤\"" + CompactPrinter(JsString("\uD834\uDD1E")) mustEqual "\"\uD834\uDD1E\"" + } + "properly print a simple JsObject" in ( + 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("key" -> JsBoolean(true)))) + mustEqual """[null,1.23,{"key":true}]""" + ) + "properly print a JSON padding (JSONP) if requested" in { + CompactPrinter(JsTrue, Some("customCallback")) mustEqual("customCallback(true)") + } + } + +}
\ No newline at end of file diff --git a/shared/src/test/scala/spray/json/CustomFormatSpec.scala b/shared/src/test/scala/spray/json/CustomFormatSpec.scala new file mode 100644 index 0000000..2397abc --- /dev/null +++ b/shared/src/test/scala/spray/json/CustomFormatSpec.scala @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable.Specification + +class CustomFormatSpec extends Specification with DefaultJsonProtocol { + + case class MyType(name: String, value: Int) + + implicit 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) + case _ => deserializationError("Expected fields: 'name' (JSON string) and 'value' (JSON number)") + } + } + 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 { + """{ "name": "bob", "value": 42 }""".parseJson.convertTo[MyType] mustEqual value + } + "support full round-trip (de)serialization" in { + value.toJson.convertTo[MyType] mustEqual value + } + } + +}
\ No newline at end of file diff --git a/shared/src/test/scala/spray/json/HashCodeCollider.scala b/shared/src/test/scala/spray/json/HashCodeCollider.scala new file mode 100644 index 0000000..57388b9 --- /dev/null +++ b/shared/src/test/scala/spray/json/HashCodeCollider.scala @@ -0,0 +1,26 @@ +package spray.json + +/** + * Helper that creates strings that all share the same hashCode == 0. + * + * Adapted from MIT-licensed code by Andriy Plokhotnyuk + * at https://github.com/plokhotnyuk/jsoniter-scala/blob/26b5ecdd4f8c2ab7e97bd8106cefdda4c1e701ce/jsoniter-scala-benchmark/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/macros/HashCodeCollider.scala#L6. + */ +object HashCodeCollider { + val visibleChars = (33 until 127).filterNot(c => c == '\\' || c == '"') + def asciiChars: Iterator[Int] = visibleChars.toIterator + def asciiCharsAndHash(previousHash: Int): Iterator[(Int, Int)] = visibleChars.toIterator.map(c => c -> (previousHash + c) * 31) + + /** Creates an iterator of Strings that all have hashCode == 0 */ + def zeroHashCodeIterator(): Iterator[String] = + for { + (i0, h0) <- asciiCharsAndHash(0) + (i1, h1) <- asciiCharsAndHash(h0) + (i2, h2) <- asciiCharsAndHash(h1) if (((h2 + 32) * 923521) ^ ((h2 + 127) * 923521)) < 0 + (i3, h3) <- asciiCharsAndHash(h2) if (((h3 + 32) * 29791) ^ ((h3 + 127) * 29791)) < 0 + (i4, h4) <- asciiCharsAndHash(h3) if (((h4 + 32) * 961) ^ ((h4 + 127) * 961)) < 0 + (i5, h5) <- asciiCharsAndHash(h4) if (((h5 + 32) * 31) ^ ((h5 + 127) * 31)) < 0 + (i6, h6) <- asciiCharsAndHash(h5) if ((h6 + 32) ^ (h6 + 127)) < 0 + (i7, h7) <- asciiCharsAndHash(h6) if h6 + i7 == 0 + } yield new String(Array(i0, i1, i2, i3, i4, i5, i6, i7).map(_.toChar)) +} diff --git a/shared/src/test/scala/spray/json/JsonParserSpec.scala b/shared/src/test/scala/spray/json/JsonParserSpec.scala new file mode 100644 index 0000000..0793e66 --- /dev/null +++ b/shared/src/test/scala/spray/json/JsonParserSpec.scala @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable._ + +class JsonParserSpec extends Specification { + + "The JsonParser" should { + "parse 'null' to JsNull" in { + JsonParser("null") === JsNull + } + "parse 'true' to JsTrue" in { + JsonParser("true") === JsTrue + } + "parse 'false' to JsFalse" in { + JsonParser("false") === JsFalse + } + "parse '0' to JsNumber" in { + JsonParser("0") === JsNumber(0) + } + "parse '1.23' to JsNumber" in { + JsonParser("1.23") === JsNumber(1.23) + } + "parse '-1E10' to JsNumber" in { + JsonParser("-1E10") === JsNumber("-1E+10") + } + "parse '12.34e-10' to JsNumber" in { + JsonParser("12.34e-10") === JsNumber("1.234E-9") + } + "parse \"xyz\" to JsString" in { + JsonParser("\"xyz\"") === JsString("xyz") + } + "parse escapes in a JsString" in { + JsonParser(""""\"\\/\b\f\n\r\t"""") === JsString("\"\\/\b\f\n\r\t") + JsonParser("\"L\\" + "u00e4nder\"") === JsString("Länder") + } + "parse all representations of the slash (SOLIDUS) character in a JsString" in { + JsonParser( "\"" + "/\\/\\u002f" + "\"") === JsString("///") + } + "parse a simple JsObject" in ( + JsonParser(""" { "key" :42, "key2": "value" }""") === + JsObject("key" -> JsNumber(42), "key2" -> JsString("value")) + ) + "parse a simple JsArray" in ( + JsonParser("""[null, 1.23 ,{"key":true } ] """) === + JsArray(JsNull, JsNumber(1.23), JsObject("key" -> JsTrue)) + ) + "parse directly from UTF-8 encoded bytes" in { + val json = JsObject( + "7-bit" -> JsString("This is regular 7-bit ASCII text."), + "2-bytes" -> JsString("2-byte UTF-8 chars like £, æ or Ö"), + "3-bytes" -> JsString("3-byte UTF-8 chars like ヨ, ᄅ or ᐁ."), + "4-bytes" -> JsString("4-byte UTF-8 chars like \uD801\uDC37, \uD852\uDF62 or \uD83D\uDE01.")) + JsonParser(json.prettyPrint.getBytes("UTF-8")) === json + } + "parse directly from UTF-8 encoded bytes when string starts with a multi-byte character" in { + val json = JsString("£0.99") + JsonParser(json.prettyPrint.getBytes("UTF-8")) === json + } + "not show bad performance characteristics when object keys' hashCodes collide" in { + val numKeys = 100000 + val value = "null" + + val regularKeys = Iterator.from(1).map(i => s"key_$i").take(numKeys) + val collidingKeys = HashCodeCollider.zeroHashCodeIterator().take(numKeys) + + def createJson(keys: Iterator[String]): String = keys.mkString("""{"""", s"""":$value,"""", s"""":$value}""") + + def nanoBench(block: => Unit): Long = { + // great microbenchmark (the comment must be kept, otherwise it's not true) + val f = block _ + + // warmup + (1 to 10).foreach(_ => f()) + + val start = System.nanoTime() + f() + val end = System.nanoTime() + end - start + } + + val regularJson = createJson(regularKeys) + val collidingJson = createJson(collidingKeys) + + val regularTime = nanoBench { JsonParser(regularJson) } + val collidingTime = nanoBench { JsonParser(collidingJson) } + + collidingTime / regularTime must be < 2L // speed must be in same order of magnitude + } + + "produce proper error messages" in { + def errorMessage(input: String, settings: JsonParserSettings = JsonParserSettings.default) = + try JsonParser(input, settings) catch { case e: JsonParser.ParsingException => e.getMessage } + + errorMessage("""[null, 1.23 {"key":true } ]""") === + """Unexpected character '{' at input index 12 (line 1, position 13), expected ']': + |[null, 1.23 {"key":true } ] + | ^ + |""".stripMargin + + errorMessage("""[null, 1.23, { key":true } ]""") === + """Unexpected character 'k' at input index 16 (line 1, position 17), expected '"': + |[null, 1.23, { key":true } ] + | ^ + |""".stripMargin + + errorMessage("""{"a}""") === + """Unexpected end-of-input at input index 4 (line 1, position 5), expected '"': + |{"a} + | ^ + |""".stripMargin + + errorMessage("""{}x""") === + """Unexpected character 'x' at input index 2 (line 1, position 3), expected end-of-input: + |{}x + | ^ + |""".stripMargin + + "reject numbers which are too big / have too high precision" in { + val settings = JsonParserSettings.default.withMaxNumberCharacters(5) + errorMessage("123.4567890", settings) === + "Number too long:The number starting with '123.4567890' had 11 characters which is more than the allowed limit " + + "maxNumberCharacters = 5. If this is legit input consider increasing the limit." + } + } + + "parse multiple values when allowTrailingInput" in { + val parser = new JsonParser("""{"key":1}{"key":2}""") + parser.parseJsValue(true) === JsObject("key" -> JsNumber(1)) + parser.parseJsValue(true) === JsObject("key" -> JsNumber(2)) + } + "reject trailing input when !allowTrailingInput" in { + def parser = JsonParser("""{"key":1}x""") + parser must throwA[JsonParser.ParsingException].like { + case e: JsonParser.ParsingException => e.getMessage must contain("expected end-of-input") + } + } + + } +} diff --git a/shared/src/test/scala/spray/json/PrettyPrinterSpec.scala b/shared/src/test/scala/spray/json/PrettyPrinterSpec.scala new file mode 100644 index 0000000..b547f59 --- /dev/null +++ b/shared/src/test/scala/spray/json/PrettyPrinterSpec.scala @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import scala.collection.immutable.ListMap +import org.specs2.mutable._ + +class PrettyPrinterSpec extends Specification { + + "The PrettyPrinter" should { + "print a more complicated JsObject nicely aligned" in { + val js = JsonParser { + """{ + | "Boolean no": false, + | "Boolean yes":true, + | "Unic\u00f8de" : "Long string with newline\nescape", + | "key with \"quotes\"" : "string", + | "key with spaces": null, + | "number": -1.2323424E-5, + | "simpleKey" : "some value", + | "sub object" : { + | "sub key": 26.5, + | "a": "b", + | "array": [1, 2, { "yes":1, "no":0 }, ["a", "b", null], false] + | }, + | "zero": 0 + |}""".stripMargin + } + def fixedFieldOrder(js: JsValue): JsValue = js match { + case JsObject(fields) => JsObject(ListMap(fields.toSeq.sortBy(_._1).map { case (k, v) => (k, fixedFieldOrder(v)) }:_*)) + case x => x + } + + PrettyPrinter(fixedFieldOrder(js)) mustEqual { + """{ + | "Boolean no": false, + | "Boolean yes": true, + | "Unic\u00f8de": "Long string with newline\nescape", + | "key with \"quotes\"": "string", + | "key with spaces": null, + | "number": -0.000012323424, + | "simpleKey": "some value", + | "sub object": { + | "a": "b", + | "array": [1, 2, { + | "no": 0, + | "yes": 1 + | }, ["a", "b", null], false], + | "sub key": 26.5 + | }, + | "zero": 0 + |}""".stripMargin + } + } + } + +}
\ No newline at end of file diff --git a/shared/src/test/scala/spray/json/RoundTripSpecs.scala b/shared/src/test/scala/spray/json/RoundTripSpecs.scala new file mode 100644 index 0000000..6bee7b4 --- /dev/null +++ b/shared/src/test/scala/spray/json/RoundTripSpecs.scala @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable.Specification +import org.scalacheck._ +import org.specs2.ScalaCheck + +object JsValueGenerators { + import Gen._ + import Arbitrary.arbitrary + + val parseableString: Gen[String] = Gen.someOf(('\u0020' to '\u007E').toVector).map(_.mkString) + val genString: Gen[JsString] = parseableString.map(JsString(_)) + val genBoolean: Gen[JsBoolean] = oneOf(JsFalse, JsTrue) + val genLongNumber: Gen[JsNumber] = arbitrary[Long].map(JsNumber(_)) + val genIntNumber: Gen[JsNumber] = arbitrary[Long].map(JsNumber(_)) + val genDoubleNumber: Gen[JsNumber] = arbitrary[Long].map(JsNumber(_)) + def genArray(depth: Int): Gen[JsArray] = + if (depth == 0) JsArray() + else + for { + n <- choose(0, 15) + els <- Gen.containerOfN[List, JsValue](n, genValue(depth - 1)) + } yield JsArray(els.toVector) + def genField(depth: Int): Gen[(String, JsValue)] = + for { + key <- parseableString + value <- genValue(depth) + } yield key -> value + def genObject(depth: Int): Gen[JsObject] = + if (depth == 0) JsObject() + else + for { + n <- choose(0, 15) + fields <- Gen.containerOfN[List, (String, JsValue)](n, genField(depth - 1)) + } yield JsObject(fields: _*) + + def genValue(depth: Int): Gen[JsValue] = + oneOf( + JsNull: Gen[JsValue], + genString, + genBoolean, + genLongNumber, + genDoubleNumber, + genIntNumber, + genArray(depth), + genObject(depth)) + implicit val arbitraryValue: Arbitrary[JsValue] = Arbitrary(genValue(5)) +} + +class RoundTripSpecs extends Specification with ScalaCheck { + import JsValueGenerators.arbitraryValue + + "Parsing / Printing round-trip" should { + "starting from JSON using compactPrint" in prop { (json: JsValue) => + json.compactPrint.parseJson must_== json + } + "starting from JSON using prettyPrint" in prop { (json: JsValue) => + json.prettyPrint.parseJson must_== json + } + } +} diff --git a/shared/src/test/scala/spray/json/SortedPrinterSpec.scala b/shared/src/test/scala/spray/json/SortedPrinterSpec.scala new file mode 100644 index 0000000..f91640e --- /dev/null +++ b/shared/src/test/scala/spray/json/SortedPrinterSpec.scala @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable._ + +class SortedPrinterSpec extends Specification { + + "The SortedPrinter" should { + "print a more complicated JsObject nicely aligned with fields sorted" in { + val obj = JsonParser { + """{ + | "Unic\u00f8de" : "Long string with newline\nescape", + | "Boolean no": false, + | "number": -1.2323424E-5, + | "key with \"quotes\"" : "string", + | "key with spaces": null, + | "simpleKey" : "some value", + | "zero": 0, + | "sub object" : { + | "sub key": 26.5, + | "a": "b", + | "array": [1, 2, { "yes":1, "no":0 }, ["a", "b", null], false] + | }, + | "Boolean yes":true + |}""".stripMargin + } + SortedPrinter(obj) mustEqual { + """{ + | "Boolean no": false, + | "Boolean yes": true, + | "Unic\u00f8de": "Long string with newline\nescape", + | "key with \"quotes\"": "string", + | "key with spaces": null, + | "number": -0.000012323424, + | "simpleKey": "some value", + | "sub object": { + | "a": "b", + | "array": [1, 2, { + | "no": 0, + | "yes": 1 + | }, ["a", "b", null], false], + | "sub key": 26.5 + | }, + | "zero": 0 + |}""".stripMargin + } + } + } + +} diff --git a/shared/src/test/scala/spray/json/StandardFormatsSpec.scala b/shared/src/test/scala/spray/json/StandardFormatsSpec.scala new file mode 100644 index 0000000..833f06a --- /dev/null +++ b/shared/src/test/scala/spray/json/StandardFormatsSpec.scala @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 Mathias Doenitz + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spray.json + +import org.specs2.mutable._ +import scala.Right + +class StandardFormatsSpec extends Specification with DefaultJsonProtocol { + + "The optionFormat" should { + "convert None to JsNull" in { + None.asInstanceOf[Option[Int]].toJson mustEqual JsNull + } + "convert JsNull to None" in { + JsNull.convertTo[Option[Int]] mustEqual None + } + "convert Some(Hello) to JsString(Hello)" in { + Some("Hello").asInstanceOf[Option[String]].toJson mustEqual JsString("Hello") + } + "convert JsString(Hello) to Some(Hello)" in { + JsString("Hello").convertTo[Option[String]] mustEqual Some("Hello") + } + } + + "The eitherFormat" should { + val a: Either[Int, String] = Left(42) + val b: Either[Int, String] = Right("Hello") + + "convert the left side of an Either value to Json" in { + a.toJson mustEqual JsNumber(42) + } + "convert the right side of an Either value to Json" in { + b.toJson mustEqual JsString("Hello") + } + "convert the left side of an Either value from Json" in { + JsNumber(42).convertTo[Either[Int, String]] mustEqual Left(42) + } + "convert the right side of an Either value from Json" in { + JsString("Hello").convertTo[Either[Int, String]] mustEqual Right("Hello") + } + } + + "The tuple1Format" should { + "convert (42) to a JsNumber" in { + Tuple1(42).toJson mustEqual JsNumber(42) + } + "be able to convert a JsNumber to a Tuple1[Int]" in { + JsNumber(42).convertTo[Tuple1[Int]] mustEqual Tuple1(42) + } + } + + "The tuple2Format" should { + val json = JsArray(JsNumber(42), JsNumber(4.2)) + "convert (42, 4.2) to a JsArray" in { + (42, 4.2).toJson mustEqual json + } + "be able to convert a JsArray to a (Int, Double)]" in { + json.convertTo[(Int, Double)] mustEqual ((42, 4.2)) + } + } + + "The tuple3Format" should { + val json = JsArray(JsNumber(42), JsNumber(4.2), JsNumber(3)) + "convert (42, 4.2, 3) to a JsArray" in { + (42, 4.2, 3).toJson mustEqual json + } + "be able to convert a JsArray to a (Int, Double, Int)]" in { + json.convertTo[(Int, Double, Int)] mustEqual ((42, 4.2, 3)) + } + } + "The tuple4Format" should { + val json = JsArray(JsNumber(42), JsNumber(4.2), JsNumber(3), JsNumber(4)) + "convert (42, 4.2, 3, 4) to a JsArray" in { + (42, 4.2, 3, 4).toJson mustEqual json + } + "be able to convert a JsArray to a (Int, Double, Int, Int)]" in { + json.convertTo[(Int, Double, Int, Int)] mustEqual ((42, 4.2, 3, 4)) + } + } + "The tuple5Format" should { + val json = JsArray(JsNumber(42), JsNumber(4.2), JsNumber(3), JsNumber(4), JsNumber(5)) + "convert (42, 4.2, 3, 4, 5) to a JsArray" in { + (42, 4.2, 3, 4, 5).toJson mustEqual json + } + "be able to convert a JsArray to a (Int, Double, Int, Int, Int)]" in { + json.convertTo[(Int, Double, Int, Int, Int)] mustEqual ((42, 4.2, 3, 4, 5)) + } + } + "The tuple6Format" should { + val json = JsArray(JsNumber(42), JsNumber(4.2), JsNumber(3), JsNumber(4), JsNumber(5), JsNumber(6)) + "convert (42, 4.2, 3, 4, 5, 6) to a JsArray" in { + (42, 4.2, 3, 4, 5, 6).toJson mustEqual json + } + "be able to convert a JsArray to a (Int, Double, Int, Int, Int, Int)]" in { + json.convertTo[(Int, Double, Int, Int, Int, Int)] mustEqual ((42, 4.2, 3, 4, 5, 6)) + } + } + "The tuple7Format" should { + val json = JsArray(JsNumber(42), JsNumber(4.2), JsNumber(3), JsNumber(4), JsNumber(5), JsNumber(6), JsNumber(7)) + "convert (42, 4.2, 3, 4, 5, 6, 7) to a JsArray" in { + (42, 4.2, 3, 4, 5, 6, 7).toJson mustEqual json + } + "be able to convert a JsArray to a (Int, Double, Int, Int, Int, Int, Int)]" in { + json.convertTo[(Int, Double, Int, Int, Int, Int, Int)] mustEqual ((42, 4.2, 3, 4, 5, 6, 7)) + } + } +} |