2016-05-03 7 views
8

Używam zapieczętowanych cech jako wyliczeń do wyczerpującego dopasowania wzorców. W przypadkach, w których mam obiekty sprawy, zamiast klas przypadków rozszerzających moją cechę, chciałbym kodować i dekodować (przez Circe) jako zwykły ciąg znaków.Circe instances do kodowania/dekodowania zaplombowanych instancji cechy arity 0?

Na przykład:

sealed trait State 
case object On extends State 
case object Off extends State 

val a: State = State.Off 
a.asJson.noSpaces // trying for "Off" 

decode[State]("On") // should be State.On 

Rozumiem, że to będzie konfigurowany w 0.5.0, ale może ktoś mi pomóc napisać coś do mnie aż przypływ, który jest zwolniony?

Odpowiedz

19

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.

+0

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. –