web-dev-qa-db-ja.com

アリティ0の封印されたトレイトインスタンスをエンコード/デコードするための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で構成可能になることを理解していますが、リリースされるまで誰かが私を乗り越えるために何かを書くのを手伝ってくれるでしょうか?

13
Andrew Roberts

問題を浮き彫りにするために—この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に基づいて構築されているためです。これは、ケースオブジェクトを空のHListsとして表します。これは、クリーンでシンプルで一貫性があるため、おそらく常にデフォルトの動作になりますが、必ずしも必要なものとは限りません(間もなく登場する 構成オプション が代替をサポートすることに注意してください)。

ケースオブジェクトに独自の汎用インスタンスを提供することで、この動作をオーバーライドできます。

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文字列としてエンコードする」ことを意味します。そして、それ自体として静的に型付けされた格オブジェクトに期待するように機能します。

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

値が基本型として静的に型付けされている場合、ストーリーは少し異なります。

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

Stateのジェネリック派生インスタンスを取得し、ケースオブジェクトの手動で定義されたジェネリックインスタンスを尊重しますが、それでもオブジェクトにラップします。これについて考えると、ある程度意味があります。ADTcouldには、JSONオブジェクトとしてのみ合理的に表現できるケースクラスが含まれているため、object-wrapper-with-constructor -名前キーアプローチは、間違いなく最も合理的な方法です。

ただし、ADTにケースクラスが含まれるのか、ケースオブジェクトのみが含まれるのかを静的にdo知っているので、実行できるのはそれだけではありません。最初に、ADTがケースオブジェクトのみで構成されていることを確認する新しい型クラスが必要です(ここでは新たなスタートを想定していますが、一般的な派生と一緒にこれを機能させることができるはずです):

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)

それが私たちが望んでいたことです。

28
Travis Brown