Skip to content

Commit

Permalink
Add deriveEnumerationCodec
Browse files Browse the repository at this point in the history
  • Loading branch information
travisbrown committed Aug 13, 2019
1 parent 9898e4b commit c39c10f
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.circe.generic.extras.codec

import io.circe.{ Codec, Decoder, DecodingFailure, HCursor, Json }
import io.circe.generic.extras.Configuration
import shapeless.{ :+:, CNil, Coproduct, HNil, Inl, Inr, LabelledGeneric, Witness }
import shapeless.labelled.{ FieldType, field }

abstract class EnumerationCodec[A] extends Codec[A]

object EnumerationCodec {
implicit val codecForEnumerationCNil: EnumerationCodec[CNil] = new EnumerationCodec[CNil] {
def apply(c: HCursor): Decoder.Result[CNil] = Left(DecodingFailure("Enumeration", c.history))
def apply(a: CNil): Json = sys.error("Cannot encode CNil")
}

implicit def decodeEnumerationCCons[K <: Symbol, V, R <: Coproduct](
implicit
witK: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, HNil],
codecForR: EnumerationCodec[R],
config: Configuration = Configuration.default
): EnumerationCodec[FieldType[K, V] :+: R] = new EnumerationCodec[FieldType[K, V] :+: R] {
def apply(c: HCursor): Decoder.Result[FieldType[K, V] :+: R] =
c.as[String] match {
case Right(s) if s == config.transformConstructorNames(witK.value.name) =>
Right(Inl(field[K](gen.from(HNil))))
case Right(_) =>
codecForR.apply(c) match {
case Right(v) => Right(Inr(v))
case Left(err) => Left(err)
}
case Left(err) => Left(DecodingFailure("Enumeration", c.history))
}
def apply(a: FieldType[K, V] :+: R): Json = a match {
case Inl(l) => Json.fromString(config.transformConstructorNames(witK.value.name))
case Inr(r) => codecForR(r)
}
}

implicit def decodeEnumeration[A, R <: Coproduct](
implicit
gen: LabelledGeneric.Aux[A, R],
codecForR: EnumerationCodec[R]
): EnumerationCodec[A] =
new EnumerationCodec[A] {
def apply(c: HCursor): Decoder.Result[A] = codecForR(c) match {
case Right(v) => Right(gen.from(v))
case Left(err) => Left(err)
}
def apply(a: A): Json = codecForR(gen.to(a))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ object EnumerationDecoder {

implicit def decodeEnumerationCCons[K <: Symbol, V, R <: Coproduct](
implicit
wit: Witness.Aux[K],
gv: LabelledGeneric.Aux[V, HNil],
dr: EnumerationDecoder[R],
witK: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, HNil],
decodeR: EnumerationDecoder[R],
config: Configuration = Configuration.default
): EnumerationDecoder[FieldType[K, V] :+: R] = new EnumerationDecoder[FieldType[K, V] :+: R] {
def apply(c: HCursor): Decoder.Result[FieldType[K, V] :+: R] =
c.as[String] match {
case Right(s) if s == config.transformConstructorNames(wit.value.name) =>
Right(Inl(field[K](gv.from(HNil))))
case Right(s) if s == config.transformConstructorNames(witK.value.name) =>
Right(Inl(field[K](gen.from(HNil))))
case Right(_) =>
dr.apply(c) match {
decodeR.apply(c) match {
case Right(v) => Right(Inr(v))
case Left(err) => Left(err)
}
Expand All @@ -35,10 +35,10 @@ object EnumerationDecoder {
implicit def decodeEnumeration[A, Repr <: Coproduct](
implicit
gen: LabelledGeneric.Aux[A, Repr],
rr: EnumerationDecoder[Repr]
decodeR: EnumerationDecoder[Repr]
): EnumerationDecoder[A] =
new EnumerationDecoder[A] {
def apply(c: HCursor): Decoder.Result[A] = rr(c) match {
def apply(c: HCursor): Decoder.Result[A] = decodeR(c) match {
case Right(v) => Right(gen.from(v))
case Left(err) => Left(err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ object EnumerationEncoder {

implicit def encodeEnumerationCCons[K <: Symbol, V, R <: Coproduct](
implicit
wit: Witness.Aux[K],
gv: LabelledGeneric.Aux[V, HNil],
dr: EnumerationEncoder[R],
witK: Witness.Aux[K],
gen: LabelledGeneric.Aux[V, HNil],
encodeR: EnumerationEncoder[R],
config: Configuration = Configuration.default
): EnumerationEncoder[FieldType[K, V] :+: R] = new EnumerationEncoder[FieldType[K, V] :+: R] {
def apply(a: FieldType[K, V] :+: R): Json = a match {
case Inl(l) => Json.fromString(config.transformConstructorNames(wit.value.name))
case Inr(r) => dr(r)
case Inl(l) => Json.fromString(config.transformConstructorNames(witK.value.name))
case Inr(r) => encodeR(r)
}
}

implicit def encodeEnumeration[A, Repr <: Coproduct](
implicit
gen: LabelledGeneric.Aux[A, Repr],
rr: EnumerationEncoder[Repr]
encodeR: EnumerationEncoder[Repr]
): EnumerationEncoder[A] =
new EnumerationEncoder[A] {
def apply(a: A): Json = rr(gen.to(a))
def apply(a: A): Json = encodeR(gen.to(a))
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.circe.generic.extras

import io.circe.{ Codec, Decoder, Encoder }
import io.circe.generic.extras.codec.{ ConfiguredAsObjectCodec, UnwrappedCodec }
import io.circe.generic.extras.codec.{ ConfiguredAsObjectCodec, EnumerationCodec, UnwrappedCodec }
import io.circe.generic.extras.decoding.{ ConfiguredDecoder, EnumerationDecoder, ReprDecoder, UnwrappedDecoder }
import io.circe.generic.extras.encoding.{ ConfiguredAsObjectEncoder, EnumerationEncoder, UnwrappedEncoder }
import io.circe.generic.extras.util.RecordToMap
Expand Down Expand Up @@ -52,6 +52,14 @@ object semiauto {
*/
def deriveEnumerationEncoder[A](implicit encode: Lazy[EnumerationEncoder[A]]): Encoder[A] = encode.value

/**
* Derive a codec for a sealed trait hierarchy made up of case objects.
*
* Note that this differs from the usual derived encoder in that the leaves of
* the ADT are represented as JSON strings.
*/
def deriveEnumerationCodec[A](implicit codec: Lazy[EnumerationCodec[A]]): Codec[A] = codec.value

/**
* Derive a decoder for a value class.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.circe.generic.extras

import io.circe.{ Decoder, Encoder }
import io.circe.{ Codec, Decoder, Encoder }
import io.circe.generic.extras.semiauto._
import io.circe.literal._
import io.circe.testing.CodecTests
Expand All @@ -11,8 +11,21 @@ import shapeless.test.illTyped
class EnumerationSemiautoDerivedSuite extends CirceSuite {
implicit val decodeCardinalDirection: Decoder[CardinalDirection] = deriveEnumerationDecoder
implicit val encodeCardinalDirection: Encoder[CardinalDirection] = deriveEnumerationEncoder
val codecForCardinalDirection: Codec[CardinalDirection] = deriveEnumerationCodec

checkLaws("Codec[CardinalDirection]", CodecTests[CardinalDirection].codec)
checkLaws(
"Codec[CardinalDirection] via Codec",
CodecTests[CardinalDirection](codecForCardinalDirection, codecForCardinalDirection).codec
)
checkLaws(
"Codec[CardinalDirection] via Decoder and Codec",
CodecTests[CardinalDirection](implicitly, codecForCardinalDirection).codec
)
checkLaws(
"Codec[CardinalDirection] via Encoder and Codec",
CodecTests[CardinalDirection](codecForCardinalDirection, implicitly).codec
)

"deriveEnumerationDecoder" should "not compile on an ADT with case classes" in {
implicit val config: Configuration = Configuration.default
Expand Down

0 comments on commit c39c10f

Please sign in to comment.