encoding/decoding 元数 0 的密封特征实例的 Circe 实例?
Circe instances for encoding/decoding sealed trait instances of arity 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 中是可配置的,但是谁能帮我写一些东西来帮助我渡过难关,直到它发布?
突出问题——假设这个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
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)
这表示,"if the generic representation of A
is an empty HList
, encode it as its name as a JSON string"。对于静态类型化为自身的 case 对象,它的工作方式与我们预期的一样:
scala> On.asJson.noSpaces
res2: String = "On"
当值被静态类型化为基类型时,情况有点不同:
scala> (On: State).asJson.noSpaces
res3: String = {"On":"On"}
我们得到了 State
的通用派生实例,它尊重我们为案例对象手动定义的通用实例,但它仍然将它们包装在一个对象中。如果您考虑一下,这是有道理的——ADT 可以 包含 case classes,它只能合理地表示为 JSON 对象,因此object-wrapper-with-constructor-name-key 方法可以说是最合理的做法。
不过,这不是我们唯一能做的事情,因为我们确实 静态地知道 ADT 是否包含 case classes 或仅包含 case 对象。首先,我们需要一个新类型 class 来证明 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)
这就是我们想要的。
我使用密封特征作为详尽模式匹配的枚举。在我有 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 中是可配置的,但是谁能帮我写一些东西来帮助我渡过难关,直到它发布?
突出问题——假设这个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
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)
这表示,"if the generic representation of A
is an empty HList
, encode it as its name as a JSON string"。对于静态类型化为自身的 case 对象,它的工作方式与我们预期的一样:
scala> On.asJson.noSpaces
res2: String = "On"
当值被静态类型化为基类型时,情况有点不同:
scala> (On: State).asJson.noSpaces
res3: String = {"On":"On"}
我们得到了 State
的通用派生实例,它尊重我们为案例对象手动定义的通用实例,但它仍然将它们包装在一个对象中。如果您考虑一下,这是有道理的——ADT 可以 包含 case classes,它只能合理地表示为 JSON 对象,因此object-wrapper-with-constructor-name-key 方法可以说是最合理的做法。
不过,这不是我们唯一能做的事情,因为我们确实 静态地知道 ADT 是否包含 case classes 或仅包含 case 对象。首先,我们需要一个新类型 class 来证明 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)
这就是我们想要的。