Представление классов дел в виде массивов JSON
Первое, на что нужно обратить внимание, это то, что модуль circe-shape предоставляет экземпляры для HList
Shapeless, которые используют представление массива, подобное тому, которое мы хотим для наших классов case. Например:
scala> import io.circe.shapes._
import io.circe.shapes._
scala> import shapeless._
import shapeless._
scala> ("foo" :: 1 :: List(true, false) :: HNil).asJson.noSpaces
res4: String = ["foo",1,[true,false]]
… а сама Shapeless обеспечивает общее отображение между классами case и HList
s. Мы можем объединить эти два, чтобы получить универсальные экземпляры, которые мы хотим для case-классов:
import io.circe.{ Decoder, Encoder }
import io.circe.shapes.HListInstances
import shapeless.{ Generic, HList }
trait FlatCaseClassCodecs extends HListInstances {
implicit def encodeCaseClassFlat[A, Repr <: HList](implicit
gen: Generic.Aux[A, Repr],
encodeRepr: Encoder[Repr]
): Encoder[A] = encodeRepr.contramap(gen.to)
implicit def decodeCaseClassFlat[A, Repr <: HList](implicit
gen: Generic.Aux[A, Repr],
decodeRepr: Decoder[Repr]
): Decoder[A] = decodeRepr.map(gen.from)
}
object FlatCaseClassCodecs extends FlatCaseClassCodecs
А потом:
scala> import FlatCaseClassCodecs._
import FlatCaseClassCodecs._
scala> Cake("cherry", 100).asJson.noSpaces
res5: String = ["cherry",100]
scala> Hat("cowboy", "felt", "brown").asJson.noSpaces
res6: String = ["cowboy","felt","brown"]
Обратите внимание, что я использую io.circe.shapes.HListInstances
, чтобы связать только те экземпляры, которые нам нужны, из круговых фигур вместе с нашими экземплярами классов пользовательских падежей, чтобы минимизировать количество вещей, которые должны импортировать наши пользователи (оба по сути). эргономики и для экономии времени компиляции).
Кодирование общего представления наших ADT
Это хороший первый шаг, но он не дает нам представление, которое мы хотим для Item
. Для этого нам понадобится более сложное оборудование:
import io.circe.{ JsonObject, ObjectEncoder }
import shapeless.{ :+:, CNil, Coproduct, Inl, Inr, Witness }
import shapeless.labelled.FieldType
trait ReprEncoder[C <: Coproduct] extends ObjectEncoder[C]
object ReprEncoder {
def wrap[A <: Coproduct](encodeA: ObjectEncoder[A]): ReprEncoder[A] =
new ReprEncoder[A] {
def encodeObject(a: A): JsonObject = encodeA.encodeObject(a)
}
implicit val encodeCNil: ReprEncoder[CNil] = wrap(
ObjectEncoder.instance[CNil](_ => sys.error("Cannot encode CNil"))
)
implicit def encodeCCons[K <: Symbol, L, R <: Coproduct](implicit
witK: Witness.Aux[K],
encodeL: Encoder[L],
encodeR: ReprEncoder[R]
): ReprEncoder[FieldType[K, L] :+: R] = wrap[FieldType[K, L] :+: R](
ObjectEncoder.instance {
case Inl(l) => JsonObject("tag" := witK.value.name, "contents" := (l: L))
case Inr(r) => encodeR.encodeObject(r)
}
)
}
Это говорит нам о том, как кодировать экземпляры Coproduct
, которые Shapeless использует в качестве общего представления иерархий запечатанных признаков в Scala. Поначалу код может быть пугающим, но это очень распространенный паттерн, и если вы потратите много времени на работу с Shapeless, вы поймете, что 90% этого кода по сути являются шаблонными, что вы видите каждый раз, когда вы создаете экземпляры индуктивно, как это.
Расшифровка этих копроизведений
Реализация декодирования даже немного хуже, но следует той же схеме:
import io.circe.{ DecodingFailure, HCursor }
import shapeless.labelled.field
trait ReprDecoder[C <: Coproduct] extends Decoder[C]
object ReprDecoder {
def wrap[A <: Coproduct](decodeA: Decoder[A]): ReprDecoder[A] =
new ReprDecoder[A] {
def apply(c: HCursor): Decoder.Result[A] = decodeA(c)
}
implicit val decodeCNil: ReprDecoder[CNil] = wrap(
Decoder.failed(DecodingFailure("CNil", Nil))
)
implicit def decodeCCons[K <: Symbol, L, R <: Coproduct](implicit
witK: Witness.Aux[K],
decodeL: Decoder[L],
decodeR: ReprDecoder[R]
): ReprDecoder[FieldType[K, L] :+: R] = wrap(
decodeL.prepare(_.downField("contents")).validate(
_.downField("tag").focus
.flatMap(_.as[String].right.toOption)
.contains(witK.value.name),
witK.value.name
)
.map(l => Inl[FieldType[K, L], R](field[K](l)))
.or(decodeR.map[FieldType[K, L] :+: R](Inr(_)))
)
}
В общем случае в наших реализациях Decoder
будет немного больше логики, поскольку каждый шаг декодирования может завершиться неудачей.
Наше представительство ADT
Теперь мы можем обернуть все это вместе:
import shapeless.{ LabelledGeneric, Lazy }
object Derivation extends FlatCaseClassCodecs {
implicit def encodeAdt[A, Repr <: Coproduct](implicit
gen: LabelledGeneric.Aux[A, Repr],
encodeRepr: Lazy[ReprEncoder[Repr]]
): ObjectEncoder[A] = encodeRepr.value.contramapObject(gen.to)
implicit def decodeAdt[A, Repr <: Coproduct](implicit
gen: LabelledGeneric.Aux[A, Repr],
decodeRepr: Lazy[ReprDecoder[Repr]]
): Decoder[A] = decodeRepr.value.map(gen.from)
}
Это выглядит очень похоже на определения в нашем FlatCaseClassCodecs
выше, и идея та же: мы определяем экземпляры для нашего типа данных (классы случаев или ADT), основываясь на экземплярах для обобщенных представлений эти типы данных. Обратите внимание, что я расширяю FlatCaseClassCodecs
, чтобы минимизировать импорт для пользователя.
В действии
Теперь мы можем использовать эти экземпляры так:
scala> import Derivation._
import Derivation._
scala> item1.asJson.noSpaces
res7: String = {"tag":"Cake","contents":["cherry",100]}
scala> item2.asJson.noSpaces
res8: String = {"tag":"Hat","contents":["cowboy","felt","brown"]}
... это именно то, что мы хотели. И самое приятное то, что это будет работать для любой иерархии запечатанных черт в Scala, независимо от того, сколько у нее классов дел или сколько у них членов этих классов дел (хотя время компиляции начнет ухудшаться, как только вы попадете в десятки ), предполагая, что все типы элементов имеют представления JSON.