Класс Decode Case с вложенным Coproduct по дискриминатору

У меня есть следующая настройка

case class A(eventType : String, fieldOne : Int)
case class B(eventType : String, fieldOne : Int, fieldTwo : Int)

type Event = A :+: B :+: CNil

case class X(id :String, events : List[Event])

И я получаю следующее сообщение Json, X с одним событием (экземпляр B)

{
"id" : "id",
"events" : [
    {
      "eventType" : "B",
      "fieldOne": 1,
      "fieldTwo" : 2
    }
]
}

Если я использую circe, я могу декодировать это в экземпляр X, однако в списке событий, поскольку A прибывает первым в копродукте, он декодирует его в A.

val actual = X("id", [A("B", 1)])
val expected = X("id", [B("B", 1, 2)])

Я хочу иметь возможность использовать eventType в качестве дискриминатора конфигурации, чтобы определять, какой тип в Coproduct становится вложенным полем.

Я думаю, что ответ здесь

но я не могу понять это в моем случае.


person John Cragg    schedule 11.04.2019    source источник
comment
Хотя подход сопродуктов - это круто, почему бы не использовать подход тегов? Похоже, это идеально подходит для ваших требований, и вам не нужно использовать какой-либо сложный неявный механизм, который немногие могут поддерживать.   -  person flavian    schedule 11.04.2019
comment
Нам нужны сопродукты для создания схем avro где-нибудь еще   -  person John Cragg    schedule 12.04.2019


Ответы (1)


Самый простой способ сделать это - изменить производные декодеры для A и B так, чтобы они перестали работать, когда eventType не является правильным значением. Это заставит декодер сопродукта естественным образом найти подходящий случай:

import shapeless._
import io.circe.Decoder, io.circe.syntax._
import io.circe.generic.semiauto.deriveDecoder
import io.circe.generic.auto._, io.circe.shapes._

case class A(eventType: String, fieldOne: Int)
case class B(eventType: String, fieldOne: Int, fieldTwo: Int)

type Event = A :+: B :+: CNil

case class X(id: String, events: List[Event])

implicit val decodeA: Decoder[A] = deriveDecoder[A].emap {
  case a @ A("A", _) => Right(a)
  case _ => Left("Invalid eventType")
}

implicit val decodeB: Decoder[B] = deriveDecoder[B].emap {
  case b @ B("B", _, _) => Right(b)
  case _ => Left("Invalid eventType")
}

val doc = """{
  "id" : "id",
  "events" : [
    {
      "eventType" : "B",
      "fieldOne": 1,
      "fieldTwo" : 2
    }
  ]
}"""

А потом:

scala> io.circe.jawn.decode[X](doc)
res0: Either[io.circe.Error,X] = Right(X(id,List(Inr(Inl(B(B,1,2))))))

Обратите внимание, что вы по-прежнему можете использовать автоматически созданные кодировщики - вам просто нужна дополнительная проверка на стороне декодирования. (Это, конечно, предполагает, что вы следите за тем, чтобы не создавать значения A или B с недопустимыми типами событий, но, поскольку вы спрашиваете об использовании этого члена в качестве дискриминатора, это кажется нормальным.)

Обновление: если вы не хотите перечислять декодеры, вы можете сделать что-то вроде этого:

import io.circe.generic.decoding.DerivedDecoder

def checkType[A <: Product { def eventType: String }](a: A): Either[String, A] =
  if (a.productPrefix == a.eventType) Right(a) else Left("Invalid eventType")

implicit def decodeSomeX[A <: Product { def eventType: String }](implicit
  decoder: DerivedDecoder[A]
): Decoder[A] = decoder.emap(checkType)

… И он должен работать точно так же, как и приведенный выше код. Существует небольшая (почти наверняка незначительная) стоимость времени выполнения для материала структурных типов, но это совершенно безопасно и кажется мне разумным способом абстрагироваться от этих типов.

person Travis Brown    schedule 12.04.2019
comment
В любом случае, мы можем сделать это в целом, мой сопродукт в настоящее время имеет около 20 классов и скоро может увеличиться до 100? - person John Cragg; 12.04.2019