From c5fe23594f66225eb18bf4e67472cd80023eb448 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 8 Mar 2018 16:53:44 -0800 Subject: Refactor build to use sbt-crossproject --- build.sbt | 56 +++++++---- project/plugins.sbt | 4 + shared/src/main/scala/DerivedFormats.scala | 108 +++++++++++++++++++++ shared/src/main/scala/annotations.scala | 22 +++++ .../src/test/scala/CoproductTypeFormatTests.scala | 79 +++++++++++++++ shared/src/test/scala/FormatTests.scala | 19 ++++ .../test/scala/ImplicitDerivedFormatTests.scala | 24 +++++ shared/src/test/scala/ProductTypeFormatTests.scala | 77 +++++++++++++++ src/main/scala/DerivedFormats.scala | 108 --------------------- src/main/scala/annotations.scala | 22 ----- src/test/scala/CoproductTypeFormatTests.scala | 79 --------------- src/test/scala/FormatTests.scala | 19 ---- src/test/scala/ImplicitDerivedFormatTests.scala | 24 ----- src/test/scala/ProductTypeFormatTests.scala | 77 --------------- 14 files changed, 368 insertions(+), 350 deletions(-) create mode 100644 shared/src/main/scala/DerivedFormats.scala create mode 100644 shared/src/main/scala/annotations.scala create mode 100644 shared/src/test/scala/CoproductTypeFormatTests.scala create mode 100644 shared/src/test/scala/FormatTests.scala create mode 100644 shared/src/test/scala/ImplicitDerivedFormatTests.scala create mode 100644 shared/src/test/scala/ProductTypeFormatTests.scala delete mode 100644 src/main/scala/DerivedFormats.scala delete mode 100644 src/main/scala/annotations.scala delete mode 100644 src/test/scala/CoproductTypeFormatTests.scala delete mode 100644 src/test/scala/FormatTests.scala delete mode 100644 src/test/scala/ImplicitDerivedFormatTests.scala delete mode 100644 src/test/scala/ProductTypeFormatTests.scala diff --git a/build.sbt b/build.sbt index 23c577f..ff91284 100644 --- a/build.sbt +++ b/build.sbt @@ -1,24 +1,38 @@ -name := "spray-json-derivation" +// shadow sbt-scalajs' crossProject and CrossType until Scala.js 1.0.0 is released +import sbtcrossproject.{crossProject, CrossType} -version in ThisBuild := { - import sys.process._ - ("git describe --always --dirty=-SNAPSHOT --match v[0-9].*" !!).tail.trim -} +lazy val sprayJsonDerivation = crossProject(JVMPlatform) + .crossType(CrossType.Full) + .in(file(".")) + .settings( + name := "spray-json-derivation", + version in ThisBuild := { + import sys.process._ + ("git describe --always --dirty=-SNAPSHOT --match v[0-9].*" !!).tail.trim + }, + crossScalaVersions := "2.12.4" :: "2.11.12" :: Nil, + scalaVersion := crossScalaVersions.value.head, + scalacOptions ++= Seq( + "-feature", + "-deprecation", + "-Xlint", + "-Xfatal-warnings" + ), + libraryDependencies ++= Seq( + "io.spray" %%% "spray-json" % "1.3.4", + "com.propensive" %%% "magnolia" % "0.7.1", + "org.scalatest" %%% "scalatest" % "3.0.2" % "test" + ) + ) + .jvmSettings( + mimaPreviousArtifacts := Set("xyz.driver" %% "spray-json-derivation" % "0.3.1") + ) -crossScalaVersions := "2.12.4" :: "2.11.12" :: Nil -scalaVersion := crossScalaVersions.value.head +lazy val sprayJsonDerivationJVM = sprayJsonDerivation.jvm -scalacOptions ++= Seq( - "-feature", - "-deprecation", - "-Xlint", - "-Xfatal-warnings" -) - -libraryDependencies ++= Seq( - "io.spray" %% "spray-json" % "1.3.4", - "com.propensive" %% "magnolia" % "0.7.1", - "org.scalatest" %% "scalatest" % "3.0.2" % "test" -) - -mimaPreviousArtifacts := Set("xyz.driver" %% "spray-json-derivation" % "0.3.1") +lazy val root = (project in file(".")) + .aggregate(sprayJsonDerivationJVM) + .settings( + publish := {}, + publishLocal := {} + ) diff --git a/project/plugins.sbt b/project/plugins.sbt index 17dccdc..dffcdb8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,3 +2,7 @@ ivyLoggingLevel := UpdateLogging.Quiet addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.2.0") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.18") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22") +addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.3.1") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.3.1") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.6") diff --git a/shared/src/main/scala/DerivedFormats.scala b/shared/src/main/scala/DerivedFormats.scala new file mode 100644 index 0000000..d0cac38 --- /dev/null +++ b/shared/src/main/scala/DerivedFormats.scala @@ -0,0 +1,108 @@ +package spray.json + +import magnolia._ + +import scala.language.experimental.macros + +/** 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] + + def combine[T](ctx: CaseClass[JsonFormat, T]): JsonFormat[T] = + new JsonFormat[T] { + override def write(value: T): JsValue = { + val fields: Seq[(String, JsValue)] = ctx.parameters.map { param => + param.label -> param.typeclass.write(param.dereference(value)) + } + JsObject(fields: _*) + } + + override def read(value: JsValue): T = value match { + case obj: JsObject => + if (ctx.isObject) { + ctx.rawConstruct(Seq.empty) + } else { + ctx.construct { param => + param.typeclass.read(obj.fields(param.label)) + } + } + case js => + deserializationError( + s"Cannot read JSON '$js' as a ${ctx.typeName.full}") + } + } + + def dispatch[T](ctx: SealedTrait[JsonFormat, T]): JsonFormat[T] = { + val typeFieldName = ctx.annotations + .collectFirst { + case g: gadt => g.typeFieldName + } + .getOrElse("type") + + new JsonFormat[T] { + override def write(value: T): JsValue = ctx.dispatch(value) { sub => + sub.typeclass.write(sub.cast(value)) match { + case obj: JsObject => + JsObject( + (Map(typeFieldName -> JsString(sub.typeName.short)) ++ + obj.fields).toSeq: _*) + case js => js + } + } + + override def read(js: JsValue): T = { + val typeName: String = js match { + case obj: JsObject if obj.fields.contains(typeFieldName) => + obj.fields(typeFieldName).convertTo[String] + case JsString(str) => + str + case _ => + deserializationError( + s"Cannot deserialize JSON to ${ctx.typeName.full} " + + "because serialized type cannot be determined.") + } + + ctx.subtypes.find(_.typeName.short == typeName) match { + case Some(tpe) => tpe.typeclass.read(js) + case None => + deserializationError( + s"Cannot deserialize JSON to ${ctx.typeName.full} " + + s"because type '${typeName}' is unsupported.") + } + } + } + } + + def jsonFormat[T]: RootJsonFormat[T] = + macro DerivedFormatMacros.derivedFormat[T] + +} +object DerivedFormats extends DerivedFormats with DefaultJsonProtocol + +trait ImplicitDerivedFormats extends DerivedFormats { self: BasicFormats => + implicit def implicitJsonFormat[T]: RootJsonFormat[T] = + macro DerivedFormatMacros.derivedFormat[T] +} +object ImplicitDerivedFormats + extends ImplicitDerivedFormats + with DefaultJsonProtocol + +object DerivedFormatMacros { + import scala.reflect.macros.whitebox._ + + /** Utility that converts a magnolia-generated JsonFormat to a RootJsonFormat. */ + def derivedFormat[T: c.WeakTypeTag](c: Context): c.Tree = { + import c.universe._ + val tpe = weakTypeOf[T].typeSymbol.asType + val sprayPkg = c.mirror.staticPackage("spray.json") + val valName = TermName(c.freshName("format")) + q"""{ + val $valName = ${Magnolia.gen[T](c)} + new $sprayPkg.RootJsonFormat[$tpe] { + def write(value: $tpe) = $valName.write(value) + def read(value: $sprayPkg.JsValue) = $valName.read(value) + } + }""" + } +} diff --git a/shared/src/main/scala/annotations.scala b/shared/src/main/scala/annotations.scala new file mode 100644 index 0000000..9d35d16 --- /dev/null +++ b/shared/src/main/scala/annotations.scala @@ -0,0 +1,22 @@ +package spray.json + +import scala.annotation.StaticAnnotation + +/** An annotation that designates that a sealed trait is a generalized algebraic + * datatype (GADT), and that a type field containing the serialized childrens' + * types should be added to the final JSON objects. + * + * Note that by default all sealed traits are treated as GADTs, with a type + * field called `type`. This annotation enables overriding the name of that + * field and is really only useful if a child itself has a field called `type` + * that would result in a conflict. + * + * Example: + * {{{ + * // the JSON field "kind" will contain the actual type of the serialized child + * @gadt("kind") sealed abstract class Keyword(`type`: String) + * case class If(`type`: String) extends Keyword(`type`) + * }}} + * @param typeFieldName the name of the field to inject into a serialized JSON + * object */ +final class gadt(val typeFieldName: String = "type") extends StaticAnnotation diff --git a/shared/src/test/scala/CoproductTypeFormatTests.scala b/shared/src/test/scala/CoproductTypeFormatTests.scala new file mode 100644 index 0000000..de73967 --- /dev/null +++ b/shared/src/test/scala/CoproductTypeFormatTests.scala @@ -0,0 +1,79 @@ +package 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/shared/src/test/scala/FormatTests.scala b/shared/src/test/scala/FormatTests.scala new file mode 100644 index 0000000..0f56091 --- /dev/null +++ b/shared/src/test/scala/FormatTests.scala @@ -0,0 +1,19 @@ +package spray.json + +import org.scalatest._ + +trait FormatTests { self: FlatSpec => + + def checkRoundtrip[A: RootJsonFormat](a: A, expectedJson: String) = { + it should "serialize to the expected JSON value" in { + val expected: JsValue = expectedJson.parseJson + assert(a.toJson == expected) + } + + it should "serialize then deserialize back to itself" in { + val reread = a.toJson.convertTo[A] + assert(reread == a) + } + } + +} diff --git a/shared/src/test/scala/ImplicitDerivedFormatTests.scala b/shared/src/test/scala/ImplicitDerivedFormatTests.scala new file mode 100644 index 0000000..1d54ae9 --- /dev/null +++ b/shared/src/test/scala/ImplicitDerivedFormatTests.scala @@ -0,0 +1,24 @@ +package 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/shared/src/test/scala/ProductTypeFormatTests.scala b/shared/src/test/scala/ProductTypeFormatTests.scala new file mode 100644 index 0000000..ce05000 --- /dev/null +++ b/shared/src/test/scala/ProductTypeFormatTests.scala @@ -0,0 +1,77 @@ +package 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/main/scala/DerivedFormats.scala b/src/main/scala/DerivedFormats.scala deleted file mode 100644 index d0cac38..0000000 --- a/src/main/scala/DerivedFormats.scala +++ /dev/null @@ -1,108 +0,0 @@ -package spray.json - -import magnolia._ - -import scala.language.experimental.macros - -/** 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] - - def combine[T](ctx: CaseClass[JsonFormat, T]): JsonFormat[T] = - new JsonFormat[T] { - override def write(value: T): JsValue = { - val fields: Seq[(String, JsValue)] = ctx.parameters.map { param => - param.label -> param.typeclass.write(param.dereference(value)) - } - JsObject(fields: _*) - } - - override def read(value: JsValue): T = value match { - case obj: JsObject => - if (ctx.isObject) { - ctx.rawConstruct(Seq.empty) - } else { - ctx.construct { param => - param.typeclass.read(obj.fields(param.label)) - } - } - case js => - deserializationError( - s"Cannot read JSON '$js' as a ${ctx.typeName.full}") - } - } - - def dispatch[T](ctx: SealedTrait[JsonFormat, T]): JsonFormat[T] = { - val typeFieldName = ctx.annotations - .collectFirst { - case g: gadt => g.typeFieldName - } - .getOrElse("type") - - new JsonFormat[T] { - override def write(value: T): JsValue = ctx.dispatch(value) { sub => - sub.typeclass.write(sub.cast(value)) match { - case obj: JsObject => - JsObject( - (Map(typeFieldName -> JsString(sub.typeName.short)) ++ - obj.fields).toSeq: _*) - case js => js - } - } - - override def read(js: JsValue): T = { - val typeName: String = js match { - case obj: JsObject if obj.fields.contains(typeFieldName) => - obj.fields(typeFieldName).convertTo[String] - case JsString(str) => - str - case _ => - deserializationError( - s"Cannot deserialize JSON to ${ctx.typeName.full} " + - "because serialized type cannot be determined.") - } - - ctx.subtypes.find(_.typeName.short == typeName) match { - case Some(tpe) => tpe.typeclass.read(js) - case None => - deserializationError( - s"Cannot deserialize JSON to ${ctx.typeName.full} " + - s"because type '${typeName}' is unsupported.") - } - } - } - } - - def jsonFormat[T]: RootJsonFormat[T] = - macro DerivedFormatMacros.derivedFormat[T] - -} -object DerivedFormats extends DerivedFormats with DefaultJsonProtocol - -trait ImplicitDerivedFormats extends DerivedFormats { self: BasicFormats => - implicit def implicitJsonFormat[T]: RootJsonFormat[T] = - macro DerivedFormatMacros.derivedFormat[T] -} -object ImplicitDerivedFormats - extends ImplicitDerivedFormats - with DefaultJsonProtocol - -object DerivedFormatMacros { - import scala.reflect.macros.whitebox._ - - /** Utility that converts a magnolia-generated JsonFormat to a RootJsonFormat. */ - def derivedFormat[T: c.WeakTypeTag](c: Context): c.Tree = { - import c.universe._ - val tpe = weakTypeOf[T].typeSymbol.asType - val sprayPkg = c.mirror.staticPackage("spray.json") - val valName = TermName(c.freshName("format")) - q"""{ - val $valName = ${Magnolia.gen[T](c)} - new $sprayPkg.RootJsonFormat[$tpe] { - def write(value: $tpe) = $valName.write(value) - def read(value: $sprayPkg.JsValue) = $valName.read(value) - } - }""" - } -} diff --git a/src/main/scala/annotations.scala b/src/main/scala/annotations.scala deleted file mode 100644 index 9d35d16..0000000 --- a/src/main/scala/annotations.scala +++ /dev/null @@ -1,22 +0,0 @@ -package spray.json - -import scala.annotation.StaticAnnotation - -/** An annotation that designates that a sealed trait is a generalized algebraic - * datatype (GADT), and that a type field containing the serialized childrens' - * types should be added to the final JSON objects. - * - * Note that by default all sealed traits are treated as GADTs, with a type - * field called `type`. This annotation enables overriding the name of that - * field and is really only useful if a child itself has a field called `type` - * that would result in a conflict. - * - * Example: - * {{{ - * // the JSON field "kind" will contain the actual type of the serialized child - * @gadt("kind") sealed abstract class Keyword(`type`: String) - * case class If(`type`: String) extends Keyword(`type`) - * }}} - * @param typeFieldName the name of the field to inject into a serialized JSON - * object */ -final class gadt(val typeFieldName: String = "type") extends StaticAnnotation diff --git a/src/test/scala/CoproductTypeFormatTests.scala b/src/test/scala/CoproductTypeFormatTests.scala deleted file mode 100644 index de73967..0000000 --- a/src/test/scala/CoproductTypeFormatTests.scala +++ /dev/null @@ -1,79 +0,0 @@ -package 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/FormatTests.scala b/src/test/scala/FormatTests.scala deleted file mode 100644 index 0f56091..0000000 --- a/src/test/scala/FormatTests.scala +++ /dev/null @@ -1,19 +0,0 @@ -package spray.json - -import org.scalatest._ - -trait FormatTests { self: FlatSpec => - - def checkRoundtrip[A: RootJsonFormat](a: A, expectedJson: String) = { - it should "serialize to the expected JSON value" in { - val expected: JsValue = expectedJson.parseJson - assert(a.toJson == expected) - } - - it should "serialize then deserialize back to itself" in { - val reread = a.toJson.convertTo[A] - assert(reread == a) - } - } - -} diff --git a/src/test/scala/ImplicitDerivedFormatTests.scala b/src/test/scala/ImplicitDerivedFormatTests.scala deleted file mode 100644 index 1d54ae9..0000000 --- a/src/test/scala/ImplicitDerivedFormatTests.scala +++ /dev/null @@ -1,24 +0,0 @@ -package 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 deleted file mode 100644 index ce05000..0000000 --- a/src/test/scala/ProductTypeFormatTests.scala +++ /dev/null @@ -1,77 +0,0 @@ -package 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}}""" - ) - -} -- cgit v1.2.3