Экземпляры Circe для кодирования / декодирования экземпляров закрытых признаков с арностью 0?

Я использую запечатанные черты как перечисления для исчерпывающего сопоставления с образцом. В случаях, когда у меня есть объекты case вместо классов case, расширяющих мою черту, я хотел бы кодировать и декодировать (через Circe) как простую строку .

Например:

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

Я понимаю, что это будет настраиваться в 0.5.0, но может ли кто-нибудь помочь мне написать что-нибудь, чтобы поддержать меня, пока это не будет выпущено?


person Andrew Roberts    schedule 03.05.2016    source источник


Ответы (1)


Чтобы выделить проблему - предполагая, что ADT:

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

Общий вывод circe будет (в настоящее время) производить следующие кодировки:

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":{}}

Это связано с тем, что общий механизм деривации построен на LabelledGeneric Shapeless, который представляет объекты case как пустые HLists. Это, вероятно, всегда будет поведением по умолчанию, поскольку оно чистое, простое и последовательное, но не всегда то, что вы хотите (как вы заметили, параметры конфигурации, которые скоро появятся, будут поддерживать альтернативы).

Вы можете переопределить это поведение, предоставив свои собственные универсальные экземпляры для объектов case:

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)

Здесь говорится: «Если общее представление A является пустым HList, закодируйте его как его имя как строку JSON». И это работает, как и следовало ожидать, для объектов case, статически типизированных как они сами:

scala> On.asJson.noSpaces
res2: String = "On"

Когда значение статически типизировано как базовый тип, история немного отличается:

scala> (On: State).asJson.noSpaces
res3: String = {"On":"On"}

Мы получаем универсально производный экземпляр для State, и он учитывает наш вручную определенный универсальный экземпляр для объектов case, но по-прежнему оборачивает их в объект. Если задуматься, в этом есть смысл - ADT может содержать классы case, которые могут быть разумно представлены только как объект JSON, и поэтому подход «объект-оболочка-с-именем-конструктором-ключом» пожалуй, самый разумный поступок.

Однако это не единственное, что мы можем сделать, поскольку мы знаем статически, содержит ли ADT классы case или только объекты case. Сначала нам нужен новый класс типа, который свидетельствует о том, что ADT состоит только из объектов case (обратите внимание, что я предполагаю начать все сначала, но должно быть возможно, чтобы эта работа работала вместе с общим производным):

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(_))
  }
}

А затем наши общие Encoder экземпляры:

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)))

Можно также пойти дальше и написать декодер.

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")
}

А потом:

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)

Что мы и хотели.

person Travis Brown    schedule 03.05.2016
comment
Оказывается, это не работает, когда запечатанная характеристика содержится в объекте. Я экспериментировал, но мне хотелось бы получить советы по поиску способа решения этой проблемы. - person Andrew Roberts; 10.05.2016