From 5ef502abc058358ec3a329c774bb42b9a7bd106f Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 8 Mar 2018 16:13:03 -0800 Subject: Don't ake derived formats implicit by default --- README.md | 25 ++++++-- src/main/scala/DerivedFormats.scala | 13 ++-- src/test/scala/CoproductTypeFormatTests.scala | 81 +++++++++++++++++++++++++ src/test/scala/CoproductTypeFormats.scala | 72 ---------------------- src/test/scala/ImplicitDerivedFormatTests.scala | 26 ++++++++ src/test/scala/ProductTypeFormatTests.scala | 79 ++++++++++++++++++++++++ src/test/scala/ProductTypeFormats.scala | 54 ----------------- 7 files changed, 215 insertions(+), 135 deletions(-) create mode 100644 src/test/scala/CoproductTypeFormatTests.scala delete mode 100644 src/test/scala/CoproductTypeFormats.scala create mode 100644 src/test/scala/ImplicitDerivedFormatTests.scala create mode 100644 src/test/scala/ProductTypeFormatTests.scala delete mode 100644 src/test/scala/ProductTypeFormats.scala diff --git a/README.md b/README.md index 3c15698..f7c2070 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ build.sbt: libraryDependencies += "xyz.driver" %% "spray-json-derivation" % "" ``` -Define some case classes and mix `DerivedFormats` into your JSON +Define some case classes and mix `ImplicitDerivedFormats` into your JSON protocol stack. That's it. ```scala import spray.json._ -import xyz.driver.json.DerivedFormats +import xyz.driver.json.ImplicitDerivedFormats -object Main extends App with DefaultJsonProtocol with DerivedFormats { +object Main extends App with DefaultJsonProtocol with ImplicitDerivedFormats { // Simple case classes @@ -70,10 +70,27 @@ object Main extends App with DefaultJsonProtocol with DerivedFormats { } ``` +It is also possible to summon derived formats explicitly by mixing in `DerivedFormats`instead of `ImplicitDerivedFormats`: +```scala +import spray.json._ +import xyz.driver.json.DerivedFormats + +object Main extends App with DefaultJsonProtocol with DerivedFormats { + + case class A(x: Int) + case class B(a: A, str: String) + + implicit val bFormat: RootJsonFormat[B] = jsonFormat[B] + + println(B(A(42), "hello world").toJson.prettyPrint) +``` +This will have the additional benefit of outputting a stacktrace in case a format cannot be derived, hence making debugging +much easier. + ## Documentation Check out the main file [DerivedFormats.scala](src/main/scala/DerivedFormats.scala) and the -[test suite](src/test/scala/ProductTypeFormats.scala) for a complete +[test suite](src/test/scala/ProductTypeFormatTests.scala) for a complete overview of the project. ## Development diff --git a/src/main/scala/DerivedFormats.scala b/src/main/scala/DerivedFormats.scala index cbfbd61..4339e5c 100644 --- a/src/main/scala/DerivedFormats.scala +++ b/src/main/scala/DerivedFormats.scala @@ -5,7 +5,7 @@ import spray.json._ import scala.language.experimental.macros -/** Mixin that enables automatic derivation of JSON formats for any product +/** Mixin that enables derivation of JSON formats for any product * (case classes) or coproduct (sealed traits) types. */ trait DerivedFormats { self: BasicFormats => type Typeclass[T] = JsonFormat[T] @@ -75,14 +75,17 @@ trait DerivedFormats { self: BasicFormats => } } - implicit def derivedFormat[T]: RootJsonFormat[T] = - macro DerivedFormatHelper.derivedFormat[T] + def jsonFormat[T]: RootJsonFormat[T] = + macro DerivedFormatMacros.derivedFormat[T] } -object DerivedFormats extends DerivedFormats with BasicFormats +trait ImplicitDerivedFormats extends DerivedFormats { self: BasicFormats => + implicit def implicitJsonFormat[T]: RootJsonFormat[T] = + macro DerivedFormatMacros.derivedFormat[T] +} -object DerivedFormatHelper { +object DerivedFormatMacros { import scala.reflect.macros.whitebox._ /** Utility that converts a magnolia-generated JsonFormat to a RootJsonFormat. */ diff --git a/src/test/scala/CoproductTypeFormatTests.scala b/src/test/scala/CoproductTypeFormatTests.scala new file mode 100644 index 0000000..ec9b3f5 --- /dev/null +++ b/src/test/scala/CoproductTypeFormatTests.scala @@ -0,0 +1,81 @@ +package xyz.driver.json + +import spray.json._ + +import org.scalatest._ + +class CoproductTypeFormatTests + extends FlatSpec + with FormatTests + with DefaultJsonProtocol + with DerivedFormats { + + sealed trait Expr + case class Zero() extends Expr + case class Value(x: Int) extends Expr + case class Plus(lhs: Expr, rhs: Expr) extends Expr + case object One extends Expr + + implicit val exprFormat: RootJsonFormat[Expr] = jsonFormat[Expr] + + "No-parameter case class child" should behave like checkRoundtrip[Expr]( + Zero(), + """{"type":"Zero"}""" + ) + + "Simple parameter case class child" should behave like checkRoundtrip[Expr]( + Value(42), + """{"type":"Value","x":42}""" + ) + + "Nested parameter case class child" should behave like checkRoundtrip[Expr]( + Plus(Value(42), One), + """{"type":"Plus","lhs":{"type":"Value","x":42},"rhs":{"type":"One"}}""" + ) + + "Case object child" should behave like checkRoundtrip[Expr]( + One, + """{"type": "One"}""" + ) + + @gadt("kind") + sealed abstract class Keyword(`type`: String) + case class If(`type`: String) extends Keyword(`type`) + + implicit val keywordFormat: RootJsonFormat[Keyword] = jsonFormat[Keyword] + + "GADT with type field alias" should behave like checkRoundtrip[Keyword]( + If("class"), + """{"kind":"If","type":"class"}""" + ) + + @gadt("""_`crazy type!`"""") + sealed abstract trait Crazy + case class CrazyType() extends Crazy + + implicit val crazyFormat: RootJsonFormat[Crazy] = jsonFormat[Crazy] + + "GADT with special characters in type field" should behave like checkRoundtrip[ + Crazy]( + CrazyType(), + """{"_`crazy type!`\"": "CrazyType"}""" + ) + + sealed trait Enum + case object A extends Enum + case object B extends Enum + + implicit val enumFormat: RootJsonFormat[Enum] = jsonFormat[Enum] + + "Enum" should behave like checkRoundtrip[List[Enum]]( + A :: B :: Nil, + """[{"type":"A"}, {"type":"B"}]""" + ) + + "Serializing as sealed trait and deserializing as child" should "work" in { + implicit val plusFormat: RootJsonFormat[Plus] = jsonFormat[Plus] + val expr: Expr = Plus(Value(42), Plus(Zero(), One)) + assert(expr.toJson.convertTo[Plus] == expr) + } + +} diff --git a/src/test/scala/CoproductTypeFormats.scala b/src/test/scala/CoproductTypeFormats.scala deleted file mode 100644 index 6496e00..0000000 --- a/src/test/scala/CoproductTypeFormats.scala +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.driver.json - -import spray.json._ - -import org.scalatest._ - -class CoproductTypeFormats - extends FlatSpec - with FormatTests - with DefaultJsonProtocol - with DerivedFormats { - - sealed trait Expr - case class Zero() extends Expr - case class Value(x: Int) extends Expr - case class Plus(lhs: Expr, rhs: Expr) extends Expr - case object One extends Expr - - "No-parameter case class child" should behave like checkRoundtrip[Expr]( - Zero(), - """{"type":"Zero"}""" - ) - - "Simple parameter case class child" should behave like checkRoundtrip[Expr]( - Value(42), - """{"type":"Value","x":42}""" - ) - - "Nested parameter case class child" should behave like checkRoundtrip[Expr]( - Plus(Value(42), One), - """{"type":"Plus","lhs":{"type":"Value","x":42},"rhs":{"type":"One"}}""" - ) - - "Case object child" should behave like checkRoundtrip[Expr]( - One, - """{"type": "One"}""" - ) - - @gadt("kind") - sealed abstract class Keyword(`type`: String) - case class If(`type`: String) extends Keyword(`type`) - - "GADT with type field alias" should behave like checkRoundtrip[Keyword]( - If("class"), - """{"kind":"If","type":"class"}""" - ) - - @gadt("""_`crazy type!`"""") - sealed abstract trait Crazy - case class CrazyType() extends Crazy - - "GADT with special characters in type field" should behave like checkRoundtrip[ - Crazy]( - CrazyType(), - """{"_`crazy type!`\"": "CrazyType"}""" - ) - - sealed trait Enum - case object A extends Enum - case object B extends Enum - - "Enum" should behave like checkRoundtrip[List[Enum]]( - A :: B :: Nil, - """[{"type":"A"}, {"type":"B"}]""" - ) - - "Serializing as sealed trait an deserializing as child" should "work" in { - val expr: Expr = Plus(Value(42), Plus(Zero(), One)) - assert(expr.toJson.convertTo[Plus] == expr) - } - -} diff --git a/src/test/scala/ImplicitDerivedFormatTests.scala b/src/test/scala/ImplicitDerivedFormatTests.scala new file mode 100644 index 0000000..9723cb0 --- /dev/null +++ b/src/test/scala/ImplicitDerivedFormatTests.scala @@ -0,0 +1,26 @@ +package xyz.driver.json + +import spray.json._ + +import org.scalatest._ + +class ImplicitDerivedFormatTests + extends FlatSpec + with FormatTests + with ImplicitDerivedFormats + with DefaultJsonProtocol { + + case class B(x: Int, b: String, mp: Map[String, Int]) + case class C(b: B) + + "Simple parameter product" should behave like checkRoundtrip( + B(42, "Hello World", Map("a" -> 1, "b" -> -1024)), + """{ "x": 42, "b": "Hello World", "mp": { "a": 1, "b": -1024 } }""" + ) + + "Nested parameter product with custom child format" should behave like checkRoundtrip( + C(B(42, "Hello World", Map("a" -> 1, "b" -> -1024))), + """{"b" :{ "x": 42, "b": "Hello World", "mp": { "a": 1, "b": -1024 } } }""" + ) + +} diff --git a/src/test/scala/ProductTypeFormatTests.scala b/src/test/scala/ProductTypeFormatTests.scala new file mode 100644 index 0000000..b7d8d27 --- /dev/null +++ b/src/test/scala/ProductTypeFormatTests.scala @@ -0,0 +1,79 @@ +package xyz.driver.json + +import spray.json._ + +import org.scalatest._ + +class ProductTypeFormatTests + extends FlatSpec + with FormatTests + with DerivedFormats + with DefaultJsonProtocol { + + case class A() + case class B(x: Int, b: String, mp: Map[String, Int]) + case class C(b: B) + case object D + case class E(d: D.type) + case class F(x: Int) + case class G(f: F) + + implicit val aFormat: RootJsonFormat[A] = jsonFormat[A] + implicit val bFormat: RootJsonFormat[B] = jsonFormat[B] + implicit val cFormat: RootJsonFormat[C] = jsonFormat[C] + implicit val dFormat: RootJsonFormat[D.type] = jsonFormat[D.type] + implicit val eFormat: RootJsonFormat[E] = jsonFormat[E] + + "No-parameter product" should behave like checkRoundtrip(A(), "{}") + + "Simple parameter product" should behave like checkRoundtrip( + B(42, "Hello World", Map("a" -> 1, "b" -> -1024)), + """{ "x": 42, "b": "Hello World", "mp": { "a": 1, "b": -1024 } }""" + ) + + "Nested parameter product" should behave like checkRoundtrip( + C(B(42, "Hello World", Map("a" -> 1, "b" -> -1024))), + """{"b" :{ "x": 42, "b": "Hello World", "mp": { "a": 1, "b": -1024 } } }""" + ) + + "Case object" should behave like checkRoundtrip( + D, + "{}" + ) + + "Case object as parameter" should behave like checkRoundtrip( + E(D), + """{"d":{}}""" + ) + + // custom format for F, that inverts the value of parameter x + implicit val fFormat: RootJsonFormat[F] = new RootJsonFormat[F] { + override def write(f: F): JsValue = JsObject("y" -> f.x.toJson) + override def read(js: JsValue): F = + F(js.asJsObject.fields("y").convertTo[Int]) + } + + "Overriding with a custom format" should behave like checkRoundtrip( + F(2), + """{"y":2}""" + ) + + implicit val gFormat: RootJsonFormat[G] = jsonFormat[G] + + "Derving a format with a custom child format" should behave like checkRoundtrip( + G(F(2)), + """{"f": {"y":2}}""" + ) + + case class H(x: Boolean) + case class I(h: H) + + // there is no format defined for H, Magnolia will generate one automatically + implicit val iFormat: RootJsonFormat[I] = jsonFormat[I] + + "Deriving a format that has no implicit child formats available" should behave like checkRoundtrip( + I(H(true)), + """{"h": {"x":true}}""" + ) + +} diff --git a/src/test/scala/ProductTypeFormats.scala b/src/test/scala/ProductTypeFormats.scala deleted file mode 100644 index 02fb554..0000000 --- a/src/test/scala/ProductTypeFormats.scala +++ /dev/null @@ -1,54 +0,0 @@ -package xyz.driver.json - -import spray.json._ - -import org.scalatest._ - -class ProductTypeFormats - extends FlatSpec - with FormatTests - with DerivedFormats - with DefaultJsonProtocol { - - case class A() - case class B(x: Int, b: String, mp: Map[String, Int]) - case class C(b: B) - case object D - case class E(d: D.type) - case class F(x: Int) - - "No-parameter product" should behave like checkRoundtrip(A(), "{}") - - "Simple parameter product" should behave like checkRoundtrip( - B(42, "Hello World", Map("a" -> 1, "b" -> -1024)), - """{ "x": 42, "b": "Hello World", "mp": { "a": 1, "b": -1024 } }""" - ) - - "Nested parameter product" should behave like checkRoundtrip( - C(B(42, "Hello World", Map("a" -> 1, "b" -> -1024))), - """{"b" :{ "x": 42, "b": "Hello World", "mp": { "a": 1, "b": -1024 } } }""" - ) - - "Case object" should behave like checkRoundtrip( - D, - "{}" - ) - - "Case object as parameter" should behave like checkRoundtrip( - E(D), - """{"d":{}}""" - ) - - // custom format for F, that inverts the value of parameter x - implicit val fFormat: RootJsonFormat[F] = new RootJsonFormat[F] { - override def write(f: F): JsValue = JsObject("x" -> JsNumber(-f.x)) - override def read(js: JsValue): F = - F(-js.asJsObject.fields("x").convertTo[Int]) - } - - "Overriding with a custom format" should behave like checkRoundtrip( - F(2), - """{"x":-2}""" - ) - -} -- cgit v1.2.3