Aby podkreślić problem zakładając ten ADT:
sealed trait State
case object On extends State
case object Off extends State
Circe generic wyprowadzenie zostanie (obecnie) wytwarzają następujące kodowanie:
scala> import io.circe.generic.auto._, io.circe.syntax._
import io.circe.generic.auto._
import io.circe.syntax._
scala> On.asJson.noSpaces
res0: String = {}
scala> (On: State).asJson.noSpaces
res1: String = {"On":{}}
To dlatego, że generyczny mechanizm wyprowadzenie jest zbudowany na Shapeless użytkownika LabelledGeneric
, który reprezentuje obiekty sprawy jako puste HList
s. Prawdopodobnie będzie to zawsze zachowanie domyślne, ponieważ jest czyste, proste i spójne, ale nie zawsze jest to, co chcesz (jak zauważysz, configuration options, które wkrótce będą wspierać alternatywne rozwiązania).
Można to zmienić poprzez dostarczanie własnych wystąpień generycznych dla przypadku obiektów:
import io.circe.Encoder
import shapeless.{ Generic, HNil }
implicit def encodeCaseObject[A <: Product](implicit
gen: Generic.Aux[A, HNil]
): Encoder[A] = Encoder[String].contramap[A](_.productPrefix)
mówi to, „czy generyczny reprezentacja A
jest pusty HList
, zakodować go jako swoją nazwę jako ciąg JSON ". I działa jak my oczekujemy dla obiektów przypadków, które są statycznie typowanych jako siebie:
scala> On.asJson.noSpaces
res2: String = "On"
Gdy wartość jest statycznie wpisane jako typ bazy, historia jest nieco inna:
scala> (On: State).asJson.noSpaces
res3: String = {"On":"On"}
Otrzymujemy generycznie wywodzącą się instancję dla State
i respektujemy naszą ręcznie określoną ogólną instancję dla obiektów sprawy, ale nadal opakowuje je w obiekt. Ma to jakiś sens, jeśli się nad tym zastanowisz - ADT może zawierać klasy spraw, które mogą być w uzasadniony sposób reprezentowane jako obiekt JSON, a więc podejście obiektowo-opakowujące-z-konstruktorem-kluczem jest prawdopodobnie najbardziej uzasadnione rzecz do zrobienia.
Nie jest to jedyna rzecz, którą możemy zrobić, ponieważ od do wiemy statycznie, czy ADT zawiera klasy sprawy, czy tylko obiekty sprawy. Najpierw musimy nową klasę typu, że świadkowie, że ADT składa się tylko z obiektami przypadków (zauważ, że jestem zakładając nowy początek tutaj, ale powinno być możliwe do tej pracy obok generycznych wyprowadzenia):
import shapeless._
import shapeless.labelled.{ FieldType, field }
trait IsEnum[C <: Coproduct] {
def to(c: C): String
def from(s: String): Option[C]
}
object IsEnum {
implicit val cnilIsEnum: IsEnum[CNil] = new IsEnum[CNil] {
def to(c: CNil): String = sys.error("Impossible")
def from(s: String): Option[CNil] = None
}
implicit def cconsIsEnum[K <: Symbol, H <: Product, T <: Coproduct](implicit
witK: Witness.Aux[K],
witH: Witness.Aux[H],
gen: Generic.Aux[H, HNil],
tie: IsEnum[T]
): IsEnum[FieldType[K, H] :+: T] = new IsEnum[FieldType[K, H] :+: T] {
def to(c: FieldType[K, H] :+: T): String = c match {
case Inl(h) => witK.value.name
case Inr(t) => tie.to(t)
}
def from(s: String): Option[FieldType[K, H] :+: T] =
if (s == witK.value.name) Some(Inl(field[K](witH.value)))
else tie.from(s).map(Inr(_))
}
}
a następnie nasze ogólne Encoder
przypadki:
import io.circe.Encoder
implicit def encodeEnum[A, C <: Coproduct](implicit
gen: LabelledGeneric.Aux[A, C],
rie: IsEnum[C]
): Encoder[A] = Encoder[String].contramap[A](a => rie.to(gen.to(a)))
równie dobrze można śmiało napisać dekoder też.
import cats.data.Xor, io.circe.Decoder
implicit def decodeEnum[A, C <: Coproduct](implicit
gen: LabelledGeneric.Aux[A, C],
rie: IsEnum[C]
): Decoder[A] = Decoder[String].emap { s =>
Xor.fromOption(rie.from(s).map(gen.from), "enum")
}
A potem:
scala> import io.circe.jawn.decode
import io.circe.jawn.decode
scala> import io.circe.syntax._
import io.circe.syntax._
scala> (On: State).asJson.noSpaces
res0: String = "On"
scala> (Off: State).asJson.noSpaces
res1: String = "Off"
scala> decode[State](""""On"""")
res2: cats.data.Xor[io.circe.Error,State] = Right(On)
scala> decode[State](""""Off"""")
res3: cats.data.Xor[io.circe.Error,State] = Right(Off)
który jest co chcieliśmy.
Okazuje się, że rozkłada się, gdy zamknięta cecha jest zawarta w obiekcie. Eksperymentowałem, ale chciałbym wskazówek, jak znaleźć sposób, aby podejść do tego problemu. –