对于编码/解码零元素数量的密封特质实例,是否存在Circe实例?

15

我使用封闭特质作为枚举来进行穷尽模式匹配。在我有使用案例对象而不是继承我的特质的案例类时,我想通过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中可配置,但有没有人可以帮我写点东西来过渡一下直到那个版本发布?

1个回答

31
为了突出问题 - 假设这是一个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":{}}

这是因为通用派生机制建立在Shapeless的LabelledGeneric之上,它将case对象表示为空的HList。这可能始终是默认行为,因为它干净、简单、一致,但这并不总是你想要的(正如你注意到即将推出的配置选项所支持的替代方案)。您可以通过为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 object,它按预期工作。
scala> On.asJson.noSpaces
res2: String = "On"

当值被静态类型定义为基本类型时,情况有些不同:
scala> (On: State).asJson.noSpaces
res3: String = {"On":"On"}

我们得到了一个泛型派生的 State 实例,并且它遵循我们手动定义的 case object 的泛型实例,但它仍然将它们包装在一个对象中。如果你考虑一下,这是有道理的——ADT 可能包含 case class,而 case class 只能合理地表示为 JSON 对象,因此采用带有构造函数名称键的对象包装方法可能是最合理的做法。
不过,这并不是我们唯一可以做的事情,因为我们确实静态地知道 ADT 是否只包含 case object。首先,我们需要一个新的类型类,证明 ADT 仅由 case object 组成(请注意,我假设从头开始,但应该可以使其与泛型推导一起工作):
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)

这正是我们想要的。


2
事实证明,当密封特质包含在对象中时,它会出现问题。我一直在进行实验,但我希望能得到一些指导,以找出解决这个问题的方法。 - Andrew Roberts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接