aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/DerivedFormats.scala
blob: 79c1e4defb548c1dd077e2b895b6790f262b3a8e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package xyz.driver.json

import magnolia._
import spray.json._

import scala.language.experimental.macros

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 =
        if (ctx.isObject) {
          JsString(ctx.typeName.short)
        } else {
          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 str: JsString if ctx.isObject && str.value == ctx.typeName.short =>
          ctx.rawConstruct(Seq.empty)

        case js =>
          deserializationError(
            s"Cannot read JSON '$js' as a ${ctx.typeName.full}")
      }
    }

  def dispatch[T](ctx: SealedTrait[JsonFormat, T]): JsonFormat[T] =
    new JsonFormat[T] {
      def tpe =
        ctx.annotations
          .find(_.isInstanceOf[JsonAnnotation])
          .getOrElse(new gadt("type"))

      override def write(value: T): JsValue = tpe match {
        case _: enum =>
          ctx.dispatch(value) { sub =>
            JsString(sub.typeName.short)
          }

        case g: gadt =>
          ctx.dispatch(value) { sub =>
            val obj = sub.typeclass.write(sub.cast(value)).asJsObject
            JsObject(
              (Map(g.typeFieldName -> JsString(sub.typeName.short)) ++
                obj.fields).toSeq: _*)
          }
      }

      override def read(value: JsValue): T = tpe match {
        case _: enum =>
          value match {
            case str: JsString =>
              ctx.subtypes
                .find(_.typeName.short == str.value)
                .getOrElse(deserializationError(
                  s"Cannot deserialize JSON to ${ctx.typeName.full} because " +
                    "type '${str}' has an unsupported value."))
                .typeclass
                .read(str)
            case js =>
              deserializationError(
                s"Cannot read JSON '$js' as a ${ctx.typeName.full}")
          }

        case g: gadt =>
          value match {
            case obj: JsObject if obj.fields.contains(g.typeFieldName) =>
              val fieldName = obj.fields(g.typeFieldName).convertTo[String]

              ctx.subtypes.find(_.typeName.short == fieldName) match {
                case Some(tpe) => tpe.typeclass.read(obj)
                case None =>
                  deserializationError(
                    s"Cannot deserialize JSON to ${ctx.typeName.full} " +
                      s"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 DerivedFormats extends DerivedFormats with BasicFormats