aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2018-02-13 15:09:31 -0800
committerJakob Odersky <jakob@odersky.com>2018-02-13 15:35:12 -0800
commitaf6845cc08ae223ad67ebad61559d8264fd3346b (patch)
tree0983ef274c10eaacb0304db65e5217ec3fd00816
downloadspray-json-derivation-af6845cc08ae223ad67ebad61559d8264fd3346b.tar.gz
spray-json-derivation-af6845cc08ae223ad67ebad61559d8264fd3346b.tar.bz2
spray-json-derivation-af6845cc08ae223ad67ebad61559d8264fd3346b.zip
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--build.sbt4
-rw-r--r--project/build.properties1
-rw-r--r--src/main/scala/CustomFormats.scala19
-rw-r--r--src/main/scala/DerivedFormats.scala55
-rw-r--r--src/main/scala/main.scala58
6 files changed, 138 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9f97022
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/ \ No newline at end of file
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..750289a
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,4 @@
+
+libraryDependencies += "io.spray" %% "spray-json" % "1.3.4"
+
+libraryDependencies += "com.propensive" %% "magnolia" % "0.6.1"
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..31334bb
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.1.1
diff --git a/src/main/scala/CustomFormats.scala b/src/main/scala/CustomFormats.scala
new file mode 100644
index 0000000..3656e3b
--- /dev/null
+++ b/src/main/scala/CustomFormats.scala
@@ -0,0 +1,19 @@
+
+import spray.json._
+
+trait CustomFormats extends DefaultJsonProtocol {
+
+ implicit val fooFormat: JsonFormat[Foo] = new JsonFormat[Foo] {
+ def read(number: JsValue) = number match {
+ case JsNumber(x) => Foo(-x.toInt)
+ case tpe => sys.error(s"no way I'm reading that type $tpe!")
+ }
+ def write(number: Foo) = JsNumber(-number.x)
+ }
+
+ implicit val z: JsonFormat[B] = new JsonFormat[B] {
+ def read(x: JsValue) = B("gone")
+ def write(x: B) = JsObject("a" -> JsString("A"))
+ }
+
+}
diff --git a/src/main/scala/DerivedFormats.scala b/src/main/scala/DerivedFormats.scala
new file mode 100644
index 0000000..6c2396e
--- /dev/null
+++ b/src/main/scala/DerivedFormats.scala
@@ -0,0 +1,55 @@
+import magnolia._
+import spray.json._
+
+import scala.language.experimental.macros
+
+trait JsonFormatDerivation extends DefaultJsonProtocol {
+ 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 =>
+ ctx.construct { param =>
+ param.typeclass.read(obj.fields(param.label))
+ }
+ case js =>
+ deserializationError(s"Cannot read JSON '$js' as a ${ctx.typeName}")
+ }
+ }
+
+ def dispatch[T](ctx: SealedTrait[JsonFormat, T]): JsonFormat[T] = new JsonFormat[T] {
+ override def write(value: T): JsValue = {
+ ctx.dispatch(value) { sub =>
+ val obj = sub.typeclass.write(sub.cast(value)).asJsObject
+ JsObject((obj.fields ++ Map("type" -> JsString(sub.label))).toSeq: _*)
+ }
+ }
+ override def read(value: JsValue): T = value match {
+ case obj: JsObject if obj.fields.contains("type") =>
+ val fieldName = obj.fields("type").convertTo[String]
+
+ ctx.subtypes.find(_.label == fieldName) match {
+ case Some(tpe) => tpe.typeclass.read(obj)
+ case None =>
+ deserializationError(
+ s"Cannot deserialize JSON to ${ctx.typeName} because type field '${fieldName}' has an unsupported value.")
+ }
+
+ case js =>
+ deserializationError(s"Cannot read JSON '$js' as a ${ctx.typeName}")
+ }
+
+ }
+
+ implicit def gen[T]: JsonFormat[T] = macro Magnolia.gen[T]
+
+}
+object JsonFormatDerivation extends JsonFormatDerivation
+
+trait DerivedFormats extends JsonFormatDerivation
diff --git a/src/main/scala/main.scala b/src/main/scala/main.scala
new file mode 100644
index 0000000..223ac1c
--- /dev/null
+++ b/src/main/scala/main.scala
@@ -0,0 +1,58 @@
+import spray.json._
+
+// product type
+case class Foo(x: Int)
+case class Bar(foo: Foo, str: String)
+
+// coproduct
+sealed trait T
+case object A extends T
+case class B(a: String) extends T
+case class C(x: T) extends T // inception!
+
+object Main extends App with DefaultJsonProtocol with DerivedFormats {
+
+ println("//////////\nProducts:")
+
+ {
+ val product = Bar(Foo(42), "hello world")
+ val js = product.toJson
+ println(js.prettyPrint)
+ println(js.convertTo[Bar])
+ }
+
+ println("//////////\nCoproducts:")
+
+ {
+ val coproduct: T = B("hello wordld") //Seq(C(B("What's up?")), B("a"), A)
+ val js = coproduct.toJson
+ println(js.prettyPrint)
+ println(js.convertTo[T])
+ }
+
+}
+
+/*
+A potentital danger:
+
+Overriding generated formats is possible (see CustomFormats), however it can be
+easy to forget to include the custom formats.
+=> In that case, the program will still compile, however it won't use the
+ correct format!
+
+Possible workarounds?
+
+ - Require explicit format declarations, i.e. remove implicit from `implicit def
+ gen[T] = macro Magnolia.gen[T]` and add `def myFormat = gen[Foo]` to every
+ format trait.
+ => requires manual code, thereby mostly defeats the advantages of automatic derivation
+ => (advantage, no more code duplication since macro is expanded only once)
+
+ - Avoid custom formats.
+ => entities become "API objects", which will be hard to upgrade in a backwards-compatible, yet idiomatic way
+ (E.g. new fields could be made optional so that they won't be required in json, however the business logic
+ may not require them to be optional. We lose some typesafety.)
+ => we'd likely have an additional layer of indirection, that will convert "api objects" to "business objects"
+ implemented by services
+ => Is that a good or bad thing?
+*/