scala - 用于编码/解码 arity 0 的密封特征实例的 Circe 实例?

标签 scala circe

我使用密封特征作为详尽模式匹配的枚举。如果我有案例对象而不是扩展我的特征的案例类,我想编码和解码(通过 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 中进行配置,但是谁能帮我写一些东西来帮助我度过难关,直到它发布?

最佳答案

为了突出这个问题——假设这个 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 之上。 ,将案例对象表示为空 HList s。这可能始终是默认行为,因为它干净、简单且一致,但并不总是您想要的(正如您所注意到的,即将推出的 configuration options 将支持替代方案)。

您可以通过为案例对象提供您自己的通用实例来覆盖此行为:
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 对象,因此 object-wrapper-with-constructor-name-key 方法可以说是最合理的方法做。

然而,这并不是我们唯一能做的事情,因为我们确实静态地知道 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)

这就是我们想要的。

关于scala - 用于编码/解码 arity 0 的密封特征实例的 Circe 实例?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37011894/

相关文章:

scala - 使用 Http4s 的 Circe 编码器和解码器

json - 使用 circe 对无形记录进行编码/解码

scala - 如何防止喷涂应用过载?

scala - 为任意 JSON 创建 `Decoder`

scala - 从Scala并行收集转换为常规收集

java - 如何在 Akka 2.4.7 中将 Scala HTTP 路由转换为 Java HTTP 路由?

java.lang.NoSuchMethodError : scala. Predef$.ArrowAssoc(Ljava/lang/Object;)Ljava/lang/Object

json - 使用 circe 将 Scala None 编码为 JSON 值

scala - 有没有办法在Circe解码器中具有可选字段?

json - Scala circe中按字段值将json列表解析为两种列表类型