From 2bc75a80f2bb9fa7d660f1338eb1b2dd7e34aeab Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 09:22:57 -0600 Subject: [PATCH 01/17] Adding Tription class --- src/main/scala/spray/json/Tription.scala | 47 ++++++++++ src/test/scala/spray/json/TriptionSpec.scala | 92 ++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/main/scala/spray/json/Tription.scala create mode 100644 src/test/scala/spray/json/TriptionSpec.scala diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala new file mode 100644 index 00000000..7fec2df2 --- /dev/null +++ b/src/main/scala/spray/json/Tription.scala @@ -0,0 +1,47 @@ +package spray.json + +/** + * Created by bathalh on 2/19/16. + */ +abstract class Tription[+T] +{ + def isDefined: Boolean + def isNull: Boolean + def hasValue = isDefined && !isNull + def get: T + + final def getOrElse[N >: T](default: => N): N = + if( !hasValue ) default else this.get + + final def map[N]( f: T => N ): Tription[N] = + if( !isDefined ) Undefined + else if( isNull ) Null + else Value( f( get ) ) + + final def flatMap[N](f: T => Tription[N]): Tription[N] = + if( !isDefined ) Undefined + else if( isNull ) Null + else f( get ) + + final def foreach[U](f: T => U): Unit = + if( hasValue ) f( this.get ) +} + +case class Value[+T](x: T) extends Tription[T] { + override def isDefined: Boolean = true + override def isNull: Boolean = false + override def get: T = x +} + +case object Null extends Tription[Nothing] { + override def isDefined: Boolean = true + override def isNull: Boolean = true + override def get = throw new NoSuchElementException("Null.get") +} + +case object Undefined extends Tription[Nothing] { + override def isDefined: Boolean = false + override def isNull: Boolean = false + override def get = throw new NoSuchElementException("Undefined.get") +} + diff --git a/src/test/scala/spray/json/TriptionSpec.scala b/src/test/scala/spray/json/TriptionSpec.scala new file mode 100644 index 00000000..92d795ea --- /dev/null +++ b/src/test/scala/spray/json/TriptionSpec.scala @@ -0,0 +1,92 @@ +package spray.json + +import java.util.NoSuchElementException + +import org.apache.commons.lang3.RandomStringUtils._ +import org.specs2.mutable._ + +import scala.util.Random._ + +/** + * Created by bathalh on 2/22/16. + */ +class TriptionSpec extends Specification +{ + def nextString = randomAlphanumeric( nextInt( 16 ) + 1 ) + + "Basic monadic and helper function should work work:" should + { + "Undefined.isDefined is false; Null and Value are true" in { + Undefined.isDefined mustEqual false + Null.isDefined mustEqual true + Value(nextString).isDefined mustEqual true + } + "Null.isNull is true; Null and Value are false" in { + Undefined.isNull mustEqual false + Null.isNull mustEqual true + Value(nextString).isNull mustEqual false + } + "Value.hasValue is true; Null and Undefined are false" in { + Undefined.hasValue mustEqual false + Null.hasValue mustEqual false + Value(nextString).hasValue mustEqual true + } + "Value.get retrieves the value" in { + val x = nextString + Value(x).get mustEqual x + } + "Null.get throws NoSuchElementException" in { + try { + Null.get + "" mustEqual "Expected NoSuchElementException" + } catch { + case nsee: NoSuchElementException => nsee.getMessage mustEqual "Null.get" + } + } + "Undefined.get throws NoSuchElementException" in { + try { + Undefined.get + "" mustEqual "Expected NoSuchElementException" + } catch { + case nsee: NoSuchElementException => nsee.getMessage mustEqual "Undefined.get" + } + } + "getOrElse gets the value if it exists; otherwise executes the else" in { + val value = nextString + val alt = nextString + Undefined.getOrElse( alt ) mustEqual alt + Null.getOrElse( alt ) mustEqual alt + Value(value).getOrElse( alt ) mustEqual value + } + "map translates a value to another value or returns original Tription if value is not there" in { + val value = nextString + val append = nextString + def mapFunction( s: String ) = s + append + + Undefined.map( mapFunction ) mustEqual Undefined + Null.map( mapFunction ) mustEqual Null + Value(value).map( mapFunction ) mustEqual Value(value + append) + } + "flatMap translates a value to another value or returns original Tription if value is not there" in { + val value = nextString + val append = nextString + def mapFunction( s: String ) = Value(s + append) + + Undefined.flatMap( mapFunction ) mustEqual Undefined + Null.flatMap( mapFunction ) mustEqual Null + Value(value).flatMap( mapFunction ) mustEqual Value(value + append) + } + "foreach executes for value in a Value and does nothing otherwise" in { + val sb = new StringBuilder + def foreachFunction( x: Any ) = sb.append( x.toString ) + + Undefined foreach foreachFunction + sb.toString mustEqual "" + Undefined foreach foreachFunction + sb.toString mustEqual "" + val v = nextString + Value(v) foreach foreachFunction + sb.toString mustEqual v + } + } +} From 1c3d5226c15f482a17f1f95aaeaba4e0b5d9115a Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 09:47:59 -0600 Subject: [PATCH 02/17] Adding implicit format for converting Tription --- src/main/scala/spray/json/JsValue.scala | 5 +++- .../scala/spray/json/StandardFormats.scala | 17 ++++++++++++ .../spray/json/StandardFormatsSpec.scala | 27 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/main/scala/spray/json/JsValue.scala b/src/main/scala/spray/json/JsValue.scala index 08a673b4..12490875 100644 --- a/src/main/scala/spray/json/JsValue.scala +++ b/src/main/scala/spray/json/JsValue.scala @@ -120,5 +120,8 @@ case object JsFalse extends JsBoolean { /** * The representation for JSON null. - */ + */ case object JsNull extends JsValue + +/** The representation for JSON undefined. **/ +case object JsUndefined extends JsValue diff --git a/src/main/scala/spray/json/StandardFormats.scala b/src/main/scala/spray/json/StandardFormats.scala index e59de646..c2edf34a 100644 --- a/src/main/scala/spray/json/StandardFormats.scala +++ b/src/main/scala/spray/json/StandardFormats.scala @@ -42,6 +42,23 @@ trait StandardFormats { def readSome(value: JsValue) = Some(value.convertTo[T]) } + implicit def triptionFormat[T :JF]: JF[Tription[T]] = new TriptionFormat[T] + + class TriptionFormat[T :JF] extends JF[Tription[T]] { + def write(tription: Tription[T]) = tription match { + case Value(x) => x.toJson + case Null => JsNull + case Undefined => JsUndefined + } + def read(value: JsValue) = value match { + case JsUndefined => Undefined + case JsNull => Null + case x => Value(x.convertTo[T]) + } + // allows reading the JSON as a Value (useful in container formats) + def readSome(value: JsValue) = Value(value.convertTo[T]) + } + implicit def eitherFormat[A :JF, B :JF] = new JF[Either[A, B]] { def write(either: Either[A, B]) = either match { case Right(a) => a.toJson diff --git a/src/test/scala/spray/json/StandardFormatsSpec.scala b/src/test/scala/spray/json/StandardFormatsSpec.scala index 833f06a7..018dcd06 100644 --- a/src/test/scala/spray/json/StandardFormatsSpec.scala +++ b/src/test/scala/spray/json/StandardFormatsSpec.scala @@ -16,11 +16,15 @@ package spray.json +import org.apache.commons.lang3.RandomStringUtils._ import org.specs2.mutable._ import scala.Right +import scala.util.Random._ class StandardFormatsSpec extends Specification with DefaultJsonProtocol { + def nextString = randomAlphanumeric( nextInt( 16 ) + 1 ) + "The optionFormat" should { "convert None to JsNull" in { None.asInstanceOf[Option[Int]].toJson mustEqual JsNull @@ -36,6 +40,29 @@ class StandardFormatsSpec extends Specification with DefaultJsonProtocol { } } + "The triptionFormat" should { + "convert Undefined to JsUndefined" in { + Undefined.asInstanceOf[Tription[Int]].toJson mustEqual JsUndefined + } + "convert JsUndefined to Undefined" in { + JsUndefined.convertTo[Tription[Int]] mustEqual Undefined + } + "convert Null to JsNull" in { + Null.asInstanceOf[Tription[Int]].toJson mustEqual JsNull + } + "convert JsNull to Null" in { + JsNull.convertTo[Tription[Int]] mustEqual Null + } + "convert Value(x) to JsString(x)" in { + val x = nextString + Value(x).asInstanceOf[Tription[String]].toJson mustEqual JsString(x) + } + "convert JsString(x) to Value(x)" in { + val x = nextString + JsString(x).convertTo[Tription[String]] mustEqual Value(x) + } + } + "The eitherFormat" should { val a: Either[Int, String] = Left(42) val b: Either[Int, String] = Right("Hello") From 323b820d130c566736eac507745060c675e578fa Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 10:11:56 -0600 Subject: [PATCH 03/17] Map JSON keyword "undefined" to JsUndefined --- src/main/scala/spray/json/JsonParser.scala | 2 ++ src/main/scala/spray/json/JsonPrinter.scala | 1 + src/test/scala/spray/json/CompactPrinterSpec.scala | 7 +++++-- src/test/scala/spray/json/JsonParserSpec.scala | 7 +++++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/scala/spray/json/JsonParser.scala b/src/main/scala/spray/json/JsonParser.scala index 71c4c119..a279ad51 100644 --- a/src/main/scala/spray/json/JsonParser.scala +++ b/src/main/scala/spray/json/JsonParser.scala @@ -60,6 +60,7 @@ class JsonParser(input: ParserInput) { (cursorChar: @switch) match { case 'f' => simpleValue(`false`(), JsFalse) case 'n' => simpleValue(`null`(), JsNull) + case 'u' => simpleValue(`undefined`(), JsUndefined) case 't' => simpleValue(`true`(), JsTrue) case '{' => advance(); `object`() case '[' => advance(); `array`() @@ -71,6 +72,7 @@ class JsonParser(input: ParserInput) { private def `false`() = advance() && ch('a') && ch('l') && ch('s') && ws('e') private def `null`() = advance() && ch('u') && ch('l') && ws('l') + private def `undefined`() = advance() && ch('n') && ch('d') && ch('e') && ch('f') && ch('i') && ch('n') && ch('e') && ws('d') private def `true`() = advance() && ch('r') && ch('u') && ws('e') // http://tools.ietf.org/html/rfc4627#section-2.2 diff --git a/src/main/scala/spray/json/JsonPrinter.scala b/src/main/scala/spray/json/JsonPrinter.scala index 258fc5aa..bca0894a 100644 --- a/src/main/scala/spray/json/JsonPrinter.scala +++ b/src/main/scala/spray/json/JsonPrinter.scala @@ -44,6 +44,7 @@ trait JsonPrinter extends (JsValue => String) { protected def printLeaf(x: JsValue, sb: JStringBuilder) { x match { case JsNull => sb.append("null") + case JsUndefined => sb.append("undefined") case JsTrue => sb.append("true") case JsFalse => sb.append("false") case JsNumber(x) => sb.append(x) diff --git a/src/test/scala/spray/json/CompactPrinterSpec.scala b/src/test/scala/spray/json/CompactPrinterSpec.scala index 6a9560b7..7baae41e 100644 --- a/src/test/scala/spray/json/CompactPrinterSpec.scala +++ b/src/test/scala/spray/json/CompactPrinterSpec.scala @@ -24,6 +24,9 @@ class CompactPrinterSpec extends Specification { "print JsNull to 'null'" in { CompactPrinter(JsNull) mustEqual "null" } + "print JsUndefined to 'undefined'" in { + CompactPrinter(JsUndefined) mustEqual "undefined" + } "print JsTrue to 'true'" in { CompactPrinter(JsTrue) mustEqual "true" } @@ -65,8 +68,8 @@ class CompactPrinterSpec extends Specification { 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}]""" + CompactPrinter(JsArray(JsNull, JsUndefined, JsNumber(1.23), JsObject("key" -> JsBoolean(true)))) + mustEqual """[null,undefined,1.23,{"key":true}]""" ) "properly print a JSON padding (JSONP) if requested" in { CompactPrinter(JsTrue, Some("customCallback")) mustEqual("customCallback(true)") diff --git a/src/test/scala/spray/json/JsonParserSpec.scala b/src/test/scala/spray/json/JsonParserSpec.scala index a97f0214..b4745b5f 100644 --- a/src/test/scala/spray/json/JsonParserSpec.scala +++ b/src/test/scala/spray/json/JsonParserSpec.scala @@ -24,6 +24,9 @@ class JsonParserSpec extends Specification { "parse 'null' to JsNull" in { JsonParser("null") === JsNull } + "parse 'undefined' to JsUndefined" in { + JsonParser("undefined") === JsUndefined + } "parse 'true' to JsTrue" in { JsonParser("true") === JsTrue } @@ -57,8 +60,8 @@ class JsonParserSpec extends Specification { 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)) + JsonParser("""[null, undefined, 1.23 ,{"key":true } ] """) === + JsArray(JsNull, JsUndefined, JsNumber(1.23), JsObject("key" -> JsTrue)) ) "parse directly from UTF-8 encoded bytes" in { val json = JsObject( From 342eefab80d92d61c650b81d755d2dfca497d0c5 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 11:30:37 -0600 Subject: [PATCH 04/17] working towards serializing and deserializing Triptions --- .../scala/spray/json/ProductFormats.scala | 8 ++++++-- .../scala/spray/json/ProductFormatsSpec.scala | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/scala/spray/json/ProductFormats.scala b/src/main/scala/spray/json/ProductFormats.scala index 7d6c63e2..a4ad6b90 100644 --- a/src/main/scala/spray/json/ProductFormats.scala +++ b/src/main/scala/spray/json/ProductFormats.scala @@ -50,13 +50,17 @@ trait ProductFormats extends ProductFormatsInstances { protected def fromField[T](value: JsValue, fieldName: String) (implicit reader: JsonReader[T]) = value match { case x: JsObject if - (reader.isInstanceOf[OptionFormat[_]] & + (reader.isInstanceOf[OptionFormat[_]] & !x.fields.contains(fieldName)) => None.asInstanceOf[T] + case x: JsObject if + (reader.isInstanceOf[TriptionFormat[_]] & + !x.fields.contains(fieldName)) => + Undefined.asInstanceOf[T] case x: JsObject => try reader.read(x.fields(fieldName)) catch { - case e: NoSuchElementException => + case e: NoSuchElementException => Undefined deserializationError("Object is missing required member '" + fieldName + "'", e, fieldName :: Nil) case DeserializationException(msg, cause, fieldNames) => deserializationError(msg, cause, fieldName :: fieldNames) diff --git a/src/test/scala/spray/json/ProductFormatsSpec.scala b/src/test/scala/spray/json/ProductFormatsSpec.scala index 30582a8f..6fde27d4 100644 --- a/src/test/scala/spray/json/ProductFormatsSpec.scala +++ b/src/test/scala/spray/json/ProductFormatsSpec.scala @@ -24,6 +24,7 @@ class ProductFormatsSpec extends Specification { case class Test2(a: Int, b: Option[Double]) case class Test3[A, B](as: List[A], bs: List[B]) case class Test4(t2: Test2) + case class Test5(a: Int, b: Tription[Double]) case class TestTransient(a: Int, b: Option[Double]) { @transient var c = false } @@ -37,6 +38,7 @@ class ProductFormatsSpec extends Specification { implicit val test2Format = jsonFormat2(Test2) implicit def test3Format[A: JsonFormat, B: JsonFormat] = jsonFormat2(Test3.apply[A, B]) implicit def test4Format = jsonFormat1(Test4) + implicit val test5Format = jsonFormat2(Test5) implicit def testTransientFormat = jsonFormat2(TestTransient) implicit def testStaticFormat = jsonFormat2(TestStatic) implicit def testMangledFormat = jsonFormat5(TestMangled) @@ -58,12 +60,27 @@ class ProductFormatsSpec extends Specification { 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 { + "not require the presence of Option fields for deserialization" in { JsObject("a" -> JsNumber(42)).convertTo[Test2] mustEqual Test2(42, None) } + "not require the presence of Tription fields for deserialization" in { + JsObject("a" -> JsNumber(42)).convertTo[Test5] mustEqual Test5(42, Undefined) + } + "deserialize null to Tription `Null`" in { + JsObject("a" -> JsNumber(42), "b" -> JsNull).convertTo[Test5] mustEqual Test5(42, Null) + } + "deserialize undefined to Tription `Undefined`" in { + JsObject("a" -> JsNumber(42), "b" -> JsUndefined).convertTo[Test5] mustEqual Test5(42, Undefined) + } "not render `None` members during serialization" in { Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42)) } +// "render `Null` members during serialization" in { +// Test5(42, Null).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull) +// } +// "not render `Undefined` members during serialization" in { +// Test5(42, Undefined).toJson mustEqual JsObject("a" -> JsNumber(42)) +// } "ignore additional members during deserialization" in { JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2), "c" -> JsString('no)).convertTo[Test2] mustEqual obj } From 8a2ecfe311513e5cdb97299a2eaf1b61ce135b2f Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 11:45:02 -0600 Subject: [PATCH 05/17] allowing serialization and deserialization of Triptions --- .../scala/spray/json/ProductFormats.scala | 1 + .../scala/spray/json/ProductFormatsSpec.scala | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/scala/spray/json/ProductFormats.scala b/src/main/scala/spray/json/ProductFormats.scala index a4ad6b90..445f3185 100644 --- a/src/main/scala/spray/json/ProductFormats.scala +++ b/src/main/scala/spray/json/ProductFormats.scala @@ -43,6 +43,7 @@ trait ProductFormats extends ProductFormatsInstances { val value = p.productElement(ix).asInstanceOf[T] writer match { case _: OptionFormat[_] if (value == None) => rest + case _: TriptionFormat[_] if (value == Undefined) => rest case _ => (fieldName, writer.write(value)) :: rest } } diff --git a/src/test/scala/spray/json/ProductFormatsSpec.scala b/src/test/scala/spray/json/ProductFormatsSpec.scala index 6fde27d4..fbf02ef2 100644 --- a/src/test/scala/spray/json/ProductFormatsSpec.scala +++ b/src/test/scala/spray/json/ProductFormatsSpec.scala @@ -24,7 +24,7 @@ class ProductFormatsSpec extends Specification { case class Test2(a: Int, b: Option[Double]) case class Test3[A, B](as: List[A], bs: List[B]) case class Test4(t2: Test2) - case class Test5(a: Int, b: Tription[Double]) + case class TestTription(a: Int, b: Tription[Double]) case class TestTransient(a: Int, b: Option[Double]) { @transient var c = false } @@ -38,7 +38,7 @@ class ProductFormatsSpec extends Specification { implicit val test2Format = jsonFormat2(Test2) implicit def test3Format[A: JsonFormat, B: JsonFormat] = jsonFormat2(Test3.apply[A, B]) implicit def test4Format = jsonFormat1(Test4) - implicit val test5Format = jsonFormat2(Test5) + implicit val test5Format = jsonFormat2(TestTription) implicit def testTransientFormat = jsonFormat2(TestTransient) implicit def testStaticFormat = jsonFormat2(TestStatic) implicit def testMangledFormat = jsonFormat5(TestMangled) @@ -64,23 +64,23 @@ class ProductFormatsSpec extends Specification { JsObject("a" -> JsNumber(42)).convertTo[Test2] mustEqual Test2(42, None) } "not require the presence of Tription fields for deserialization" in { - JsObject("a" -> JsNumber(42)).convertTo[Test5] mustEqual Test5(42, Undefined) + JsObject("a" -> JsNumber(42)).convertTo[TestTription] mustEqual TestTription(42, Undefined) } "deserialize null to Tription `Null`" in { - JsObject("a" -> JsNumber(42), "b" -> JsNull).convertTo[Test5] mustEqual Test5(42, Null) + JsObject("a" -> JsNumber(42), "b" -> JsNull).convertTo[TestTription] mustEqual TestTription(42, Null) } "deserialize undefined to Tription `Undefined`" in { - JsObject("a" -> JsNumber(42), "b" -> JsUndefined).convertTo[Test5] mustEqual Test5(42, Undefined) + JsObject("a" -> JsNumber(42), "b" -> JsUndefined).convertTo[TestTription] mustEqual TestTription(42, Undefined) } "not render `None` members during serialization" in { Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42)) } -// "render `Null` members during serialization" in { -// Test5(42, Null).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull) -// } -// "not render `Undefined` members during serialization" in { -// Test5(42, Undefined).toJson mustEqual JsObject("a" -> JsNumber(42)) -// } + "render `Null` members during serialization" in { + TestTription(42, Null).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull) + } + "not render `Undefined` members during serialization" in { + TestTription(42, Undefined).toJson mustEqual JsObject("a" -> JsNumber(42)) + } "ignore additional members during deserialization" in { JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2), "c" -> JsString('no)).convertTo[Test2] mustEqual obj } @@ -103,10 +103,13 @@ class ProductFormatsSpec extends Specification { } "A JsonProtocol mixing in NullOptions" should { + import TestProtocol2._ "render `None` members to `null`" in { - import TestProtocol2._ Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull) } + "render `Undefined` members to `undefined`" in { + TestTription(42, Undefined).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsUndefined) + } } "A JsonFormat for a generic case class and created with `jsonFormat`" should { From 551b3a94362c878c26ffea35aff68abafc302d18 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 11:56:00 -0600 Subject: [PATCH 06/17] Adding Tription JavaDoc --- src/main/scala/spray/json/Tription.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index 7fec2df2..3ccce96a 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -1,6 +1,23 @@ package spray.json /** + * A Triple-Option + * + * JavaScript (and JSON), unlike Java/Scala, allow undefined values, which are distinct from null values. + * For example, a PUT request may have a payload like this: + * + * { "id":"234565434567898789098765", + * "field1": 7, + * "field3: null, + * "field4": undefined } + * + * which would tell the server to update field1 to 7, set field3 to null, and leave field2 and field4 alone. + * With a standard scala `Option`, it is impossible to tell whether the payload of the request had field2, field3, + * and field4 null or undefined since any missing values translate to `None`. + * + * The Tription solves that problem by defining `Value` for present values, `Null` for null values, and + * `Undefined` for values which are missing or explicitly marked as undefined. + * * Created by bathalh on 2/19/16. */ abstract class Tription[+T] From 484ee1c6fb7dbd895a77732cf776a6ab4fd065f6 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 12:01:20 -0600 Subject: [PATCH 07/17] Documenting why filter is not implemented --- src/main/scala/spray/json/Tription.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index 3ccce96a..efc92500 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -20,7 +20,7 @@ package spray.json * * Created by bathalh on 2/19/16. */ -abstract class Tription[+T] +sealed abstract class Tription[+T] extends Product { def isDefined: Boolean def isNull: Boolean @@ -42,6 +42,10 @@ abstract class Tription[+T] final def foreach[U](f: T => U): Unit = if( hasValue ) f( this.get ) + + // not sure whether to return Null or Undefined if the filter criteria are not met +// final def filter(p: T => Boolean): Tription[T] = +// if (!hasValue || p(this.get)) this else (Undefined/Null) } case class Value[+T](x: T) extends Tription[T] { From fa7c8752b7645c4a7300e3e455e9278fa42383ff Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 12:21:29 -0600 Subject: [PATCH 08/17] Moving Triptoin comments to README --- README.markdown | 31 +++++++++++++++++++++--- src/main/scala/spray/json/Tription.scala | 17 +------------ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/README.markdown b/README.markdown index 30ca8ed5..3be51a24 100644 --- a/README.markdown +++ b/README.markdown @@ -96,6 +96,7 @@ important reference and collection types. As long as your code uses nothing more * String, Symbol * BigInt, BigDecimal * Option, Either, Tuple1 - Tuple7 +* Tription * List, Array * immutable.{Map, Iterable, Seq, IndexedSeq, LinearSeq, Set, Vector} * collection.{Iterable, Seq, IndexedSeq, LinearSeq, Set} @@ -104,6 +105,30 @@ important reference and collection types. As long as your code uses nothing more In most cases however you'll also want to convert types not covered by the `DefaultJsonProtocol`. In these cases you need to provide `JsonFormat[T]`s for your custom types. This is not hard at all. +### Triptions + +`Tription`s are "triple options": values that can either exist, be null, or be undefined. + +JavaScript (and JSON), unlike Java/Scala, allow `undefined` values, which are distinct from `null` values. +For example, a PUT request may have a payload like this: +```json + { "id":"234565434567898789098765", + "field1": "new value", + "field3": null, + "field4": undefined } +``` +which would tell the server to update field1 to "new value", set field3 to null, and leave field2 and field4 +unchanged. With a standard scala `Option`, it is impossible to tell whether the values of field2, field3, +and field4 in the original payload were `null` or `undefined` since any missing values translate to `None`. + +The `Tription` solves that problem by defining `Value` for present values, `Null` for null values, and +`Undefined` for values which are missing or explicitly marked as undefined. + +`Tription`s can be used just like `Option`s: +```scala +case class RequestObject( id: String, field1: Tription[String], field2: Tription[Int], + field3: Tription[String], field4: Tription[SubResource] ) +``` ### Providing JsonFormats for Case Classes @@ -159,10 +184,10 @@ object MyJsonProtocol extends DefaultJsonProtocol { #### NullOptions The `NullOptions` trait supplies an alternative rendering mode for optional case class members. Normally optional -members that are undefined (`None`) are not rendered at all. By mixing in this trait into your custom JsonProtocol you +members that are undefined (`None`/`Undefined`) are not rendered at all. By mixing in this trait into your custom JsonProtocol you can enforce the rendering of undefined members as `null`. -(Note that this only affect JSON writing, spray-json will always read missing optional members as well as `null` -optional members as `None`.) +(Note that this only affect JSON writing, spray-json will always read missing `Option` members as well as `null` +`Option` members as `None` and missing `Tription` members as `Undefined`.) ### Providing JsonFormats for other Types diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index efc92500..466441dc 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -1,22 +1,7 @@ package spray.json /** - * A Triple-Option - * - * JavaScript (and JSON), unlike Java/Scala, allow undefined values, which are distinct from null values. - * For example, a PUT request may have a payload like this: - * - * { "id":"234565434567898789098765", - * "field1": 7, - * "field3: null, - * "field4": undefined } - * - * which would tell the server to update field1 to 7, set field3 to null, and leave field2 and field4 alone. - * With a standard scala `Option`, it is impossible to tell whether the payload of the request had field2, field3, - * and field4 null or undefined since any missing values translate to `None`. - * - * The Tription solves that problem by defining `Value` for present values, `Null` for null values, and - * `Undefined` for values which are missing or explicitly marked as undefined. + * A Triple-Option for JSON values: `Undefined`, `Null`, or `Value(x)` See readme for more details * * Created by bathalh on 2/19/16. */ From f1ded95d450209df1663e63ae988fad02f147299 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 7 Mar 2016 13:38:11 -0600 Subject: [PATCH 09/17] adding filter to Tription --- src/main/scala/spray/json/Tription.scala | 8 ++++---- src/test/scala/spray/json/TriptionSpec.scala | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index 466441dc..1471aa5b 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -25,12 +25,12 @@ sealed abstract class Tription[+T] extends Product else if( isNull ) Null else f( get ) + /** return `Null` (not `Undefined`) if filter criteria don't match **/ + final def filter(p: T => Boolean): Tription[T] = + if (!hasValue || p(this.get)) this else Null + final def foreach[U](f: T => U): Unit = if( hasValue ) f( this.get ) - - // not sure whether to return Null or Undefined if the filter criteria are not met -// final def filter(p: T => Boolean): Tription[T] = -// if (!hasValue || p(this.get)) this else (Undefined/Null) } case class Value[+T](x: T) extends Tription[T] { diff --git a/src/test/scala/spray/json/TriptionSpec.scala b/src/test/scala/spray/json/TriptionSpec.scala index 92d795ea..3d76d791 100644 --- a/src/test/scala/spray/json/TriptionSpec.scala +++ b/src/test/scala/spray/json/TriptionSpec.scala @@ -76,6 +76,19 @@ class TriptionSpec extends Specification Null.flatMap( mapFunction ) mustEqual Null Value(value).flatMap( mapFunction ) mustEqual Value(value + append) } + "filter returns itself for Null and Undefined" in { + Undefined filter { _ => false } mustEqual Undefined + Null filter { _ => false } mustEqual Null + Undefined filter { _ => true } mustEqual Undefined + Null filter { _ => true } mustEqual Null + } + "filter returns Null if value does not match criteria" in { + Value(nextString) filter { _ => false } mustEqual Null + } + "filter returns itself if value matches criteria" in { + val value = nextString + Value(value) filter { _ => true } mustEqual Value(value) + } "foreach executes for value in a Value and does nothing otherwise" in { val sb = new StringBuilder def foreachFunction( x: Any ) = sb.append( x.toString ) From 828f04d451d89a76e206f8d4cb79655279be67d9 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 7 Mar 2016 14:47:20 -0600 Subject: [PATCH 10/17] Removing apache commons dependency in an attempt to resolve the major.minor version error. --- src/test/scala/spray/json/StandardFormatsSpec.scala | 4 +--- src/test/scala/spray/json/TriptionSpec.scala | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/test/scala/spray/json/StandardFormatsSpec.scala b/src/test/scala/spray/json/StandardFormatsSpec.scala index 018dcd06..7a81e20b 100644 --- a/src/test/scala/spray/json/StandardFormatsSpec.scala +++ b/src/test/scala/spray/json/StandardFormatsSpec.scala @@ -16,14 +16,12 @@ package spray.json -import org.apache.commons.lang3.RandomStringUtils._ import org.specs2.mutable._ -import scala.Right import scala.util.Random._ class StandardFormatsSpec extends Specification with DefaultJsonProtocol { - def nextString = randomAlphanumeric( nextInt( 16 ) + 1 ) + def nextString = new String( (alphanumeric take (nextInt( 16 ) + 1)).toArray ) "The optionFormat" should { "convert None to JsNull" in { diff --git a/src/test/scala/spray/json/TriptionSpec.scala b/src/test/scala/spray/json/TriptionSpec.scala index 3d76d791..2e66c7d7 100644 --- a/src/test/scala/spray/json/TriptionSpec.scala +++ b/src/test/scala/spray/json/TriptionSpec.scala @@ -2,7 +2,6 @@ package spray.json import java.util.NoSuchElementException -import org.apache.commons.lang3.RandomStringUtils._ import org.specs2.mutable._ import scala.util.Random._ @@ -12,7 +11,7 @@ import scala.util.Random._ */ class TriptionSpec extends Specification { - def nextString = randomAlphanumeric( nextInt( 16 ) + 1 ) + def nextString = new String( (alphanumeric take (nextInt( 16 ) + 1)).toArray ) "Basic monadic and helper function should work work:" should { From ede92f7c3733edb116c00d041adc6038c9e19736 Mon Sep 17 00:00:00 2001 From: bathalh Date: Thu, 18 May 2017 10:55:55 -0500 Subject: [PATCH 11/17] Actually, we're talking PATCH here. --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 3be51a24..bba104cf 100644 --- a/README.markdown +++ b/README.markdown @@ -110,7 +110,7 @@ need to provide `JsonFormat[T]`s for your custom types. This is not hard at all. `Tription`s are "triple options": values that can either exist, be null, or be undefined. JavaScript (and JSON), unlike Java/Scala, allow `undefined` values, which are distinct from `null` values. -For example, a PUT request may have a payload like this: +For example, a PATCH request may have a payload like this: ```json { "id":"234565434567898789098765", "field1": "new value", From b49545e6ecdb717b1e22e15a1cc61c745b5b70cb Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 09:22:57 -0600 Subject: [PATCH 12/17] Adding Tription class --- src/test/scala/spray/json/TriptionSpec.scala | 149 ++++++++++--------- 1 file changed, 81 insertions(+), 68 deletions(-) diff --git a/src/test/scala/spray/json/TriptionSpec.scala b/src/test/scala/spray/json/TriptionSpec.scala index 2e66c7d7..8218665a 100644 --- a/src/test/scala/spray/json/TriptionSpec.scala +++ b/src/test/scala/spray/json/TriptionSpec.scala @@ -14,67 +14,80 @@ class TriptionSpec extends Specification def nextString = new String( (alphanumeric take (nextInt( 16 ) + 1)).toArray ) "Basic monadic and helper function should work work:" should - { - "Undefined.isDefined is false; Null and Value are true" in { - Undefined.isDefined mustEqual false - Null.isDefined mustEqual true - Value(nextString).isDefined mustEqual true - } - "Null.isNull is true; Null and Value are false" in { - Undefined.isNull mustEqual false - Null.isNull mustEqual true - Value(nextString).isNull mustEqual false - } - "Value.hasValue is true; Null and Undefined are false" in { - Undefined.hasValue mustEqual false - Null.hasValue mustEqual false - Value(nextString).hasValue mustEqual true - } - "Value.get retrieves the value" in { - val x = nextString - Value(x).get mustEqual x - } - "Null.get throws NoSuchElementException" in { - try { - Null.get - "" mustEqual "Expected NoSuchElementException" - } catch { - case nsee: NoSuchElementException => nsee.getMessage mustEqual "Null.get" + { + "Undefined.isDefined is false; Null and Value are true" in { + Undefined.isDefined mustEqual false + Null.isDefined mustEqual true + Value(nextString).isDefined mustEqual true } - } - "Undefined.get throws NoSuchElementException" in { - try { - Undefined.get - "" mustEqual "Expected NoSuchElementException" - } catch { - case nsee: NoSuchElementException => nsee.getMessage mustEqual "Undefined.get" + "Null.isNull is true; Null and Value are false" in { + Undefined.isNull mustEqual false + Null.isNull mustEqual true + Value(nextString).isNull mustEqual false } - } - "getOrElse gets the value if it exists; otherwise executes the else" in { - val value = nextString - val alt = nextString - Undefined.getOrElse( alt ) mustEqual alt - Null.getOrElse( alt ) mustEqual alt - Value(value).getOrElse( alt ) mustEqual value - } - "map translates a value to another value or returns original Tription if value is not there" in { - val value = nextString - val append = nextString - def mapFunction( s: String ) = s + append + "Value.hasValue is true; Null and Undefined are false" in { + Undefined.hasValue mustEqual false + Null.hasValue mustEqual false + Value(nextString).hasValue mustEqual true + } + "Value.get retrieves the value" in { + val x = nextString + Value(x).get mustEqual x + } + "Null.get throws NoSuchElementException" in { + try { + Null.get + "" mustEqual "Expected NoSuchElementException" + } catch { + case nsee: NoSuchElementException => nsee.getMessage mustEqual "Null.get" + } + } + "Undefined.get throws NoSuchElementException" in { + try { + Undefined.get + "" mustEqual "Expected NoSuchElementException" + } catch { + case nsee: NoSuchElementException => nsee.getMessage mustEqual "Undefined.get" + } + } + "getOrElse gets the value if it exists; otherwise executes the else" in { + val value = nextString + val alt = nextString + Undefined.getOrElse( alt ) mustEqual alt + Null.getOrElse( alt ) mustEqual alt + Value(value).getOrElse( alt ) mustEqual value + } + "map translates a value to another value or returns original Tription if value is not there" in { + val value = nextString + val append = nextString + def mapFunction( s: String ) = s + append - Undefined.map( mapFunction ) mustEqual Undefined - Null.map( mapFunction ) mustEqual Null - Value(value).map( mapFunction ) mustEqual Value(value + append) - } - "flatMap translates a value to another value or returns original Tription if value is not there" in { - val value = nextString - val append = nextString - def mapFunction( s: String ) = Value(s + append) + Undefined.map( mapFunction ) mustEqual Undefined + Null.map( mapFunction ) mustEqual Null + Value(value).map( mapFunction ) mustEqual Value(value + append) + } + "flatMap translates a value to another value or returns original Tription if value is not there" in { + val value = nextString + val append = nextString + def mapFunction( s: String ) = Value(s + append) - Undefined.flatMap( mapFunction ) mustEqual Undefined - Null.flatMap( mapFunction ) mustEqual Null - Value(value).flatMap( mapFunction ) mustEqual Value(value + append) - } + Undefined.flatMap( mapFunction ) mustEqual Undefined + Null.flatMap( mapFunction ) mustEqual Null + Value(value).flatMap( mapFunction ) mustEqual Value(value + append) + } + "filter returns itself for Null and Undefined" in { + Undefined filter { _ => false } mustEqual Undefined + Null filter { _ => false } mustEqual Null + Undefined filter { _ => true } mustEqual Undefined + Null filter { _ => true } mustEqual Null + } + "filter returns Null if value does not match criteria" in { + Value(nextString) filter { _ => false } mustEqual Null + } + "filter returns itself if value matches criteria" in { + val value = nextString + Value(value) filter { _ => true } mustEqual Value(value) + } "filter returns itself for Null and Undefined" in { Undefined filter { _ => false } mustEqual Undefined Null filter { _ => false } mustEqual Null @@ -88,17 +101,17 @@ class TriptionSpec extends Specification val value = nextString Value(value) filter { _ => true } mustEqual Value(value) } - "foreach executes for value in a Value and does nothing otherwise" in { - val sb = new StringBuilder - def foreachFunction( x: Any ) = sb.append( x.toString ) + "foreach executes for value in a Value and does nothing otherwise" in { + val sb = new StringBuilder + def foreachFunction( x: Any ) = sb.append( x.toString ) - Undefined foreach foreachFunction - sb.toString mustEqual "" - Undefined foreach foreachFunction - sb.toString mustEqual "" - val v = nextString - Value(v) foreach foreachFunction - sb.toString mustEqual v + Undefined foreach foreachFunction + sb.toString mustEqual "" + Undefined foreach foreachFunction + sb.toString mustEqual "" + val v = nextString + Value(v) foreach foreachFunction + sb.toString mustEqual v + } } - } } From e71ac5f7b06c862f223d274eafc4c148032a5962 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 11:56:00 -0600 Subject: [PATCH 13/17] Adding Tription JavaDoc --- src/main/scala/spray/json/Tription.scala | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index 1471aa5b..06482dfd 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -1,7 +1,22 @@ package spray.json /** - * A Triple-Option for JSON values: `Undefined`, `Null`, or `Value(x)` See readme for more details + * A Triple-Option + * + * JavaScript (and JSON), unlike Java/Scala, allow undefined values, which are distinct from null values. + * For example, a PUT request may have a payload like this: + * + * { "id":"234565434567898789098765", + * "field1": 7, + * "field3: null, + * "field4": undefined } + * + * which would tell the server to update field1 to 7, set field3 to null, and leave field2 and field4 alone. + * With a standard scala `Option`, it is impossible to tell whether the payload of the request had field2, field3, + * and field4 null or undefined since any missing values translate to `None`. + * + * The Tription solves that problem by defining `Value` for present values, `Null` for null values, and + * `Undefined` for values which are missing or explicitly marked as undefined. * * Created by bathalh on 2/19/16. */ From 3b0b55bad5e92ded3b9a0d8736c278018c808dc9 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 22 Feb 2016 12:01:20 -0600 Subject: [PATCH 14/17] Documenting why filter is not implemented --- src/main/scala/spray/json/Tription.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index 06482dfd..503543a3 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -46,6 +46,10 @@ sealed abstract class Tription[+T] extends Product final def foreach[U](f: T => U): Unit = if( hasValue ) f( this.get ) + + // not sure whether to return Null or Undefined if the filter criteria are not met +// final def filter(p: T => Boolean): Tription[T] = +// if (!hasValue || p(this.get)) this else (Undefined/Null) } case class Value[+T](x: T) extends Tription[T] { From 51e3f32e68af719f4f00aa84777188686a0d5d23 Mon Sep 17 00:00:00 2001 From: Andrew Thalheimer Date: Mon, 23 Oct 2017 09:42:15 -0500 Subject: [PATCH 15/17] resolving merge conflicts --- src/main/scala/spray/json/Tription.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index 503543a3..f2c45d72 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -8,12 +8,11 @@ package spray.json * * { "id":"234565434567898789098765", * "field1": 7, - * "field3: null, - * "field4": undefined } + * "field3: null } * - * which would tell the server to update field1 to 7, set field3 to null, and leave field2 and field4 alone. - * With a standard scala `Option`, it is impossible to tell whether the payload of the request had field2, field3, - * and field4 null or undefined since any missing values translate to `None`. + * which would tell the server to update field1 to 7, set field3 to null, and leave field2 alone. + * With a standard scala `Option`, it is impossible to tell whether the payload of the request had field2 and field3 + * null or undefined since any missing values translate to `None`. * * The Tription solves that problem by defining `Value` for present values, `Null` for null values, and * `Undefined` for values which are missing or explicitly marked as undefined. From 90908f4fb3636b032ee89fc6deb3f223fef02b97 Mon Sep 17 00:00:00 2001 From: bathalh Date: Mon, 7 Mar 2016 13:38:11 -0600 Subject: [PATCH 16/17] adding filter to Tription --- src/main/scala/spray/json/Tription.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index f2c45d72..00f7b70b 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -45,10 +45,6 @@ sealed abstract class Tription[+T] extends Product final def foreach[U](f: T => U): Unit = if( hasValue ) f( this.get ) - - // not sure whether to return Null or Undefined if the filter criteria are not met -// final def filter(p: T => Boolean): Tription[T] = -// if (!hasValue || p(this.get)) this else (Undefined/Null) } case class Value[+T](x: T) extends Tription[T] { From 60f26878dc8018c475137070264c18213bb25946 Mon Sep 17 00:00:00 2001 From: Andrew Thalheimer Date: Wed, 1 Nov 2017 16:09:26 -0500 Subject: [PATCH 17/17] Removing support for the `undefined` keyword, which is part of JavaScript but not JSON itself. --- README.markdown | 17 ++++---- .../scala/spray/json/CompactPrinter.scala | 9 ++-- src/main/scala/spray/json/JsValue.scala | 5 ++- src/main/scala/spray/json/JsonParser.scala | 2 - src/main/scala/spray/json/JsonPrinter.scala | 2 +- src/main/scala/spray/json/PrettyPrinter.scala | 5 ++- .../scala/spray/json/ProductFormats.scala | 6 +-- src/main/scala/spray/json/Tription.scala | 6 +-- .../scala/spray/json/CompactPrinterSpec.scala | 21 +++++++-- .../scala/spray/json/JsonParserSpec.scala | 7 +-- .../scala/spray/json/PrettyPrinterSpec.scala | 43 +++++++++++++++---- .../scala/spray/json/ProductFormatsSpec.scala | 6 +++ 12 files changed, 85 insertions(+), 44 deletions(-) diff --git a/README.markdown b/README.markdown index bba104cf..6258912c 100644 --- a/README.markdown +++ b/README.markdown @@ -114,15 +114,14 @@ For example, a PATCH request may have a payload like this: ```json { "id":"234565434567898789098765", "field1": "new value", - "field3": null, - "field4": undefined } + "field3": null } ``` -which would tell the server to update field1 to "new value", set field3 to null, and leave field2 and field4 -unchanged. With a standard scala `Option`, it is impossible to tell whether the values of field2, field3, -and field4 in the original payload were `null` or `undefined` since any missing values translate to `None`. +which would tell the server to update field1 to "new value", set field3 to null, and leave field2 +unchanged. With a standard scala `Option`, it is impossible to tell whether the values of field2 and field3 +in the original payload were `null` or undefined since any missing values translate to `None`. -The `Tription` solves that problem by defining `Value` for present values, `Null` for null values, and -`Undefined` for values which are missing or explicitly marked as undefined. +The `Tription` solves that problem by defining `Value` for present values, `Null` for values explicitly marked +null, and `Undefined` for values which are missing. `Tription`s can be used just like `Option`s: ```scala @@ -184,8 +183,8 @@ object MyJsonProtocol extends DefaultJsonProtocol { #### NullOptions The `NullOptions` trait supplies an alternative rendering mode for optional case class members. Normally optional -members that are undefined (`None`/`Undefined`) are not rendered at all. By mixing in this trait into your custom JsonProtocol you -can enforce the rendering of undefined members as `null`. +members that are undefined (`None`/`Undefined`) are not rendered at all. By mixing in this trait into your custom +JsonProtocol you can enforce the rendering of undefined members as `null`. (Note that this only affect JSON writing, spray-json will always read missing `Option` members as well as `null` `Option` members as `None` and missing `Tription` members as `Undefined`.) diff --git a/src/main/scala/spray/json/CompactPrinter.scala b/src/main/scala/spray/json/CompactPrinter.scala index a51583d3..06b3f7ff 100644 --- a/src/main/scala/spray/json/CompactPrinter.scala +++ b/src/main/scala/spray/json/CompactPrinter.scala @@ -33,10 +33,11 @@ trait CompactPrinter extends JsonPrinter { protected def printObject(members: Map[String, JsValue], sb: StringBuilder) { sb.append('{') - printSeq(members, sb.append(',')) { m => - printString(m._1, sb) - sb.append(':') - print(m._2, sb) + val definedMembers = members filter { case (_, v) => v != JsUndefined } + printSeq(definedMembers, sb.append(',')) { m => + printString( m._1, sb ) + sb.append( ':' ) + print( m._2, sb ) } sb.append('}') } diff --git a/src/main/scala/spray/json/JsValue.scala b/src/main/scala/spray/json/JsValue.scala index 12490875..1de810e4 100644 --- a/src/main/scala/spray/json/JsValue.scala +++ b/src/main/scala/spray/json/JsValue.scala @@ -68,7 +68,8 @@ case class JsArray(elements: Vector[JsValue]) extends JsValue { } object JsArray { val empty = JsArray(Vector.empty) - def apply(elements: JsValue*) = new JsArray(elements.toVector) + def apply(elements: JsValue*) = if( elements contains JsUndefined ) throw new IllegalStateException( "JSON arrays cannot contain undefined values" ) + else new JsArray(elements.toVector) @deprecated("Use JsArray(Vector[JsValue]) instead", "1.3.0") def apply(elements: List[JsValue]) = new JsArray(elements.toVector) } @@ -123,5 +124,5 @@ case object JsFalse extends JsBoolean { */ case object JsNull extends JsValue -/** The representation for JSON undefined. **/ +/** The representation for JSON missing value. **/ case object JsUndefined extends JsValue diff --git a/src/main/scala/spray/json/JsonParser.scala b/src/main/scala/spray/json/JsonParser.scala index a279ad51..71c4c119 100644 --- a/src/main/scala/spray/json/JsonParser.scala +++ b/src/main/scala/spray/json/JsonParser.scala @@ -60,7 +60,6 @@ class JsonParser(input: ParserInput) { (cursorChar: @switch) match { case 'f' => simpleValue(`false`(), JsFalse) case 'n' => simpleValue(`null`(), JsNull) - case 'u' => simpleValue(`undefined`(), JsUndefined) case 't' => simpleValue(`true`(), JsTrue) case '{' => advance(); `object`() case '[' => advance(); `array`() @@ -72,7 +71,6 @@ class JsonParser(input: ParserInput) { private def `false`() = advance() && ch('a') && ch('l') && ch('s') && ws('e') private def `null`() = advance() && ch('u') && ch('l') && ws('l') - private def `undefined`() = advance() && ch('n') && ch('d') && ch('e') && ch('f') && ch('i') && ch('n') && ch('e') && ws('d') private def `true`() = advance() && ch('r') && ch('u') && ws('e') // http://tools.ietf.org/html/rfc4627#section-2.2 diff --git a/src/main/scala/spray/json/JsonPrinter.scala b/src/main/scala/spray/json/JsonPrinter.scala index bca0894a..8c62cf97 100644 --- a/src/main/scala/spray/json/JsonPrinter.scala +++ b/src/main/scala/spray/json/JsonPrinter.scala @@ -44,11 +44,11 @@ trait JsonPrinter extends (JsValue => String) { protected def printLeaf(x: JsValue, sb: JStringBuilder) { x match { case JsNull => sb.append("null") - case JsUndefined => sb.append("undefined") case JsTrue => sb.append("true") case JsFalse => sb.append("false") case JsNumber(x) => sb.append(x) case JsString(x) => printString(x, sb) + case JsUndefined => throw new IllegalStateException( "Cannot display JsUndefined" ) case _ => throw new IllegalStateException } } diff --git a/src/main/scala/spray/json/PrettyPrinter.scala b/src/main/scala/spray/json/PrettyPrinter.scala index 6af54433..7568df3c 100644 --- a/src/main/scala/spray/json/PrettyPrinter.scala +++ b/src/main/scala/spray/json/PrettyPrinter.scala @@ -40,8 +40,9 @@ trait PrettyPrinter extends JsonPrinter { protected def organiseMembers(members: Map[String, JsValue]): Seq[(String, JsValue)] = members.toSeq protected def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int) { - sb.append("{\n") - printSeq(organiseMembers(members), sb.append(",\n")) { m => + sb.append("{\n") + val definedMembers = members filter { case (_, v) => v != JsUndefined } + printSeq(organiseMembers(definedMembers), sb.append(",\n")) { m => printIndent(sb, indent + Indent) printString(m._1, sb) sb.append(": ") diff --git a/src/main/scala/spray/json/ProductFormats.scala b/src/main/scala/spray/json/ProductFormats.scala index 445f3185..8d7aaac4 100644 --- a/src/main/scala/spray/json/ProductFormats.scala +++ b/src/main/scala/spray/json/ProductFormats.scala @@ -51,17 +51,17 @@ trait ProductFormats extends ProductFormatsInstances { protected def fromField[T](value: JsValue, fieldName: String) (implicit reader: JsonReader[T]) = value match { case x: JsObject if - (reader.isInstanceOf[OptionFormat[_]] & + (reader.isInstanceOf[OptionFormat[_]] & !x.fields.contains(fieldName)) => None.asInstanceOf[T] case x: JsObject if - (reader.isInstanceOf[TriptionFormat[_]] & + (reader.isInstanceOf[TriptionFormat[_]] & !x.fields.contains(fieldName)) => Undefined.asInstanceOf[T] case x: JsObject => try reader.read(x.fields(fieldName)) catch { - case e: NoSuchElementException => Undefined + case e: NoSuchElementException => deserializationError("Object is missing required member '" + fieldName + "'", e, fieldName :: Nil) case DeserializationException(msg, cause, fieldNames) => deserializationError(msg, cause, fieldName :: fieldNames) diff --git a/src/main/scala/spray/json/Tription.scala b/src/main/scala/spray/json/Tription.scala index 00f7b70b..905963c5 100644 --- a/src/main/scala/spray/json/Tription.scala +++ b/src/main/scala/spray/json/Tription.scala @@ -11,11 +11,11 @@ package spray.json * "field3: null } * * which would tell the server to update field1 to 7, set field3 to null, and leave field2 alone. - * With a standard scala `Option`, it is impossible to tell whether the payload of the request had field2 and field3 + * With a standard scala `Option`, it is impossible to tell whether field2 and field3 were * null or undefined since any missing values translate to `None`. * - * The Tription solves that problem by defining `Value` for present values, `Null` for null values, and - * `Undefined` for values which are missing or explicitly marked as undefined. + * The Tription solves that problem by defining `Value` for present values, `Null` for values + * explicitly set to null, and `Undefined` for values which are not there at all. * * Created by bathalh on 2/19/16. */ diff --git a/src/test/scala/spray/json/CompactPrinterSpec.scala b/src/test/scala/spray/json/CompactPrinterSpec.scala index 7baae41e..b804d2ef 100644 --- a/src/test/scala/spray/json/CompactPrinterSpec.scala +++ b/src/test/scala/spray/json/CompactPrinterSpec.scala @@ -24,8 +24,13 @@ class CompactPrinterSpec extends Specification { "print JsNull to 'null'" in { CompactPrinter(JsNull) mustEqual "null" } - "print JsUndefined to 'undefined'" in { - CompactPrinter(JsUndefined) mustEqual "undefined" + "throw exception when printing JsUndefined" in { + try { + CompactPrinter(JsUndefined) mustEqual "undefined" + } catch { + case ise: IllegalStateException => + ise.getMessage mustEqual "Cannot display JsUndefined" + } } "print JsTrue to 'true'" in { CompactPrinter(JsTrue) mustEqual "true" @@ -67,9 +72,17 @@ class CompactPrinterSpec extends Specification { CompactPrinter(JsObject("key" -> JsNumber(42), "key2" -> JsString("value"))) mustEqual """{"key":42,"key2":"value"}""" ) + "properly print a simple JsObject with undefined values" in ( + CompactPrinter(JsObject("key" -> JsNumber(42), "key2" -> JsString("value"), "key3" -> JsUndefined)) + mustEqual """{"key":42,"key2":"value"}""" + ) + "properly print a simple JsObject with only undefined values" in ( + CompactPrinter(JsObject("key" -> JsUndefined, "key2" -> JsUndefined)) + mustEqual "{}" + ) "properly print a simple JsArray" in ( - CompactPrinter(JsArray(JsNull, JsUndefined, JsNumber(1.23), JsObject("key" -> JsBoolean(true)))) - mustEqual """[null,undefined,1.23,{"key":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 { CompactPrinter(JsTrue, Some("customCallback")) mustEqual("customCallback(true)") diff --git a/src/test/scala/spray/json/JsonParserSpec.scala b/src/test/scala/spray/json/JsonParserSpec.scala index b4745b5f..a97f0214 100644 --- a/src/test/scala/spray/json/JsonParserSpec.scala +++ b/src/test/scala/spray/json/JsonParserSpec.scala @@ -24,9 +24,6 @@ class JsonParserSpec extends Specification { "parse 'null' to JsNull" in { JsonParser("null") === JsNull } - "parse 'undefined' to JsUndefined" in { - JsonParser("undefined") === JsUndefined - } "parse 'true' to JsTrue" in { JsonParser("true") === JsTrue } @@ -60,8 +57,8 @@ class JsonParserSpec extends Specification { JsObject("key" -> JsNumber(42), "key2" -> JsString("value")) ) "parse a simple JsArray" in ( - JsonParser("""[null, undefined, 1.23 ,{"key":true } ] """) === - JsArray(JsNull, JsUndefined, JsNumber(1.23), JsObject("key" -> JsTrue)) + 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( diff --git a/src/test/scala/spray/json/PrettyPrinterSpec.scala b/src/test/scala/spray/json/PrettyPrinterSpec.scala index 6354ef0b..737607d4 100644 --- a/src/test/scala/spray/json/PrettyPrinterSpec.scala +++ b/src/test/scala/spray/json/PrettyPrinterSpec.scala @@ -22,25 +22,50 @@ import org.specs2.mutable._ class PrettyPrinterSpec extends Specification { "The PrettyPrinter" should { + val JsObject(fields) = 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 + } + "print a more complicated JsObject nicely aligned" in { - val JsObject(fields) = JsonParser { + PrettyPrinter(JsObject(ListMap(fields.toSeq.sortBy(_._1):_*))) mustEqual { """{ | "Boolean no": false, - | "Boolean yes":true, - | "Unic\u00f8de" : "Long string with newline\nescape", - | "key with \"quotes\"" : "string", + | "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" : { + | "number": -0.000012323424, + | "simpleKey": "some value", + | "sub object": { | "sub key": 26.5, | "a": "b", - | "array": [1, 2, { "yes":1, "no":0 }, ["a", "b", null], false] + | "array": [1, 2, { + | "yes": 1, + | "no": 0 + | }, ["a", "b", null], false] | }, | "zero": 0 |}""".stripMargin } - PrettyPrinter(JsObject(ListMap(fields.toSeq.sortBy(_._1):_*))) mustEqual { + } + + "ignore undefined fields" in { + val fieldsWithUndefined = fields + ("notthere" -> JsUndefined) + PrettyPrinter(JsObject(ListMap(fieldsWithUndefined.toSeq.sortBy(_._1):_*))) mustEqual { """{ | "Boolean no": false, | "Boolean yes": true, diff --git a/src/test/scala/spray/json/ProductFormatsSpec.scala b/src/test/scala/spray/json/ProductFormatsSpec.scala index fbf02ef2..be3a13e3 100644 --- a/src/test/scala/spray/json/ProductFormatsSpec.scala +++ b/src/test/scala/spray/json/ProductFormatsSpec.scala @@ -72,12 +72,18 @@ class ProductFormatsSpec extends Specification { "deserialize undefined to Tription `Undefined`" in { JsObject("a" -> JsNumber(42), "b" -> JsUndefined).convertTo[TestTription] mustEqual TestTription(42, Undefined) } + "deserialize valued to Tription the value" in { + JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2D)).convertTo[TestTription] mustEqual TestTription(42, Value(4.2D)) + } "not render `None` members during serialization" in { Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42)) } "render `Null` members during serialization" in { TestTription(42, Null).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull) } + "render `Value` members during serialization" in { + TestTription(42, Value(4.2D)).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2D)) + } "not render `Undefined` members during serialization" in { TestTription(42, Undefined).toJson mustEqual JsObject("a" -> JsNumber(42)) }