aboutsummaryrefslogtreecommitdiff
path: root/shared/src/main/scala
diff options
context:
space:
mode:
Diffstat (limited to 'shared/src/main/scala')
-rw-r--r--shared/src/main/scala/DerivedFormats.scala108
-rw-r--r--shared/src/main/scala/annotations.scala22
2 files changed, 130 insertions, 0 deletions
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