Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension of JsonCodec to have parity features with generic.extra #91

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ target/
.project
.classpath
tmp/
.bloop
.metals
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ val compilerOptions = Seq(
)

val catsVersion = "2.0.0-RC1"
val circeVersion = "0.12.0-RC1"
val circeVersion = "0.12.0-RC2"
val paradiseVersion = "2.1.1"
val previousCirceDerivationVersion = "0.12.0-M4"
val previousCirceDerivationVersion = "0.12.0-RC1"
val scalaCheckVersion = "1.14.0"
val scalaJavaTimeVersion = "2.0.0-RC3"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.circe.derivation.renaming
* derived - or if only one of them will be.
*/
sealed trait Configuration {
import io.circe.derivation.Discriminator

type Config <: Configuration

Expand All @@ -19,32 +20,52 @@ sealed trait Configuration {
*/
def transformMemberNames: String => String

def useDefaults: Boolean

def discriminator: Discriminator

protected def getA(transformMemberNames: String => String): Config

protected def applyDiscriminator(discriminator: Discriminator): Config

/** Creates a configuration which produces snake cased member names */
final def withSnakeCaseMemberNames: Config =
getA(renaming.snakeCase)

/** Creates a configuration which produces kebab cased member names */
final def withKebabCaseMemberNames: Config =
getA(renaming.kebabCase)

final def withDiscriminatorName(name: String): Config =
applyDiscriminator(Discriminator.Embedded(name))

final def withTypeDiscriminator: Config =
applyDiscriminator(Discriminator.TypeDiscriminator)
}

object Configuration {
import io.circe.derivation.Discriminator

/** Configuration allowing customisation of JSON produced when encoding or
* decoding.
*
* This configuration creates *both* encoder and decoder.
*/
final case class Codec(
transformMemberNames: String => String
transformMemberNames: String => String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we support ADTs we should probably have transformConstructorNames as well, but I can add that in a follow-up.

useDefaults: Boolean,
discriminator: Discriminator
) extends Configuration {

type Config = Codec

protected final def getA(transformMemberNames: String => String) =
Codec(transformMemberNames)
Codec(transformMemberNames, useDefaults, discriminator)

protected final def applyDiscriminator(
discriminator: Discriminator
): Codec =
Codec(transformMemberNames, useDefaults, discriminator)
}

/** Configuration allowing customisation of JSON produced when encoding or
Expand All @@ -53,13 +74,18 @@ object Configuration {
* This configuration **only** creates decoder.
*/
final case class DecodeOnly(
transformMemberNames: String => String
transformMemberNames: String => String,
useDefaults: Boolean,
discriminator: Discriminator
) extends Configuration {

type Config = DecodeOnly

protected final def getA(transformMemberNames: String => String) =
DecodeOnly(transformMemberNames)
DecodeOnly(transformMemberNames, useDefaults, discriminator)

protected final def applyDiscriminator(discriminator: Discriminator) =
DecodeOnly(transformMemberNames, useDefaults, discriminator)
}

/** Configuration allowing customisation of JSON produced when encoding or
Expand All @@ -68,24 +94,30 @@ object Configuration {
* This configuration **only** creates encoder.
*/
final case class EncodeOnly(
transformMemberNames: String => String
transformMemberNames: String => String,
useDefaults: Boolean,
discriminator: Discriminator
) extends Configuration {

type Config = EncodeOnly

protected final def getA(transformMemberNames: String => String) =
EncodeOnly(transformMemberNames)
EncodeOnly(transformMemberNames, useDefaults, discriminator)

protected final def applyDiscriminator(discriminator: Discriminator) =
EncodeOnly(transformMemberNames, useDefaults, discriminator)

}

/** Create a default configuration with both decoder and encoder */
val default: Codec =
Codec(identity)
Codec(identity, true, Discriminator.default)

/** Create a default configuration with **only** encoder */
val encodeOnly: EncodeOnly =
EncodeOnly(identity)
EncodeOnly(identity, true, Discriminator.default)

/** Create a default configuration with **only** decoder */
val decodeOnly: DecodeOnly =
DecodeOnly(identity)
DecodeOnly(identity, true, Discriminator.default)
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context)

private[this] def codecFrom(tree: Tree): JsonCodecType =
tree.tpe.dealias match {
case t if t <:< typeOf[Configuration.Codec] =>
case t if t == typeOf[Configuration.Codec] =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

equals is not guaranteed to work correctly on scala-reflect types. Maybe =:=?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I hadn't noticed these changes. @aparo, is there a reason to change these? I fixed this a few weeks ago because I was getting inaccurate warnings in some contexts.

JsonCodecType.Both
case t if t <:< typeOf[Configuration.DecodeOnly] =>
case t if t == typeOf[Configuration.DecodeOnly] =>
JsonCodecType.DecodeOnly
case t if t <:< typeOf[Configuration.EncodeOnly] =>
case t if t == typeOf[Configuration.EncodeOnly] =>
JsonCodecType.EncodeOnly
case t =>
c.warning(
Expand All @@ -101,8 +101,15 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context)
JsonCodecType.Both
}

private[this] val cfgNameTransformation =
private[this] val cfgTransformMemberNames =
q"$config.transformMemberNames"
private[this] val cfgUseDefaults =
q"$config.useDefaults"
private[this] val cfgDiscriminator =
q"$config.discriminator"

private[this] val defaultDiscriminator: Tree =
q"_root_.io.circe.derivation.Discriminator.default"

private[this] def codec(clsDef: ClassDef): Tree = {
val tpname = clsDef.name
Expand All @@ -114,11 +121,11 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context)
val Type = tpname
(
q"""implicit val $decoderName: $DecoderClass[$Type] =
_root_.io.circe.derivation.deriveDecoder[$Type]($cfgNameTransformation)""",
_root_.io.circe.derivation.deriveDecoder[$Type]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)""",
q"""implicit val $encoderName: $AsObjectEncoderClass[$Type] =
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgNameTransformation)""",
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgTransformMemberNames, $cfgDiscriminator)""",
q"""implicit val $codecName: $AsObjectCodecClass[$Type] =
_root_.io.circe.derivation.deriveCodec[$Type]($cfgNameTransformation)"""
_root_.io.circe.derivation.deriveCodec[$Type]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)"""
)
} else {
val tparamNames = tparams.map(_.name)
Expand All @@ -134,13 +141,13 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context)
val Type = tq"$tpname[..$tparamNames]"
(
q"""implicit def $decoderName[..$tparams](implicit ..$decodeParams): $DecoderClass[$Type] =
_root_.io.circe.derivation.deriveDecoder[$Type]($cfgNameTransformation)""",
_root_.io.circe.derivation.deriveDecoder[$Type]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)""",
q"""implicit def $encoderName[..$tparams](implicit ..$encodeParams): $AsObjectEncoderClass[$Type] =
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgNameTransformation)""",
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgTransformMemberNames, $cfgDiscriminator)""",
q"""implicit def $codecName[..$tparams](implicit
..${decodeParams ++ encodeParams}
): $AsObjectCodecClass[$Type] =
_root_.io.circe.derivation.deriveCodec[$Type]($cfgNameTransformation)"""
_root_.io.circe.derivation.deriveCodec[$Type]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)"""
)
}
codecType match {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.circe.derivation.annotations

import scala.annotation.StaticAnnotation

final case class JsonKey(value: String) extends StaticAnnotation

final case class JsonNoDefault() extends StaticAnnotation
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.circe.derivation.annotations

import io.circe._
import io.circe.parser._
import io.circe.syntax._
import org.scalatest._

object JsonCodecADTSpecSamples {

@JsonCodec
sealed trait ADT1

@JsonCodec case class ADT1A(a: Int) extends ADT1
@JsonCodec case class ADT1B(b: Int) extends ADT1

@JsonCodec(Configuration.default.withDiscriminatorName("_type"))
sealed trait ADT1Custom

@JsonCodec case class ADT1CustomA(a: Int) extends ADT1Custom
@JsonCodec case class ADT1CustomB(b: Int) extends ADT1Custom

@JsonCodec(Configuration.default.withTypeDiscriminator)
sealed trait ADTTyped

@JsonCodec case class ADTTypedA(a: Int) extends ADTTyped
@JsonCodec case class ADTTypedB(b: Int) extends ADTTyped
}

class JsonCodecADTSpec extends WordSpec with Matchers {

import JsonCodecADTSpecSamples._

implicit val printer = Printer.noSpaces.copy(dropNullValues = true)

"JsonCodecADTSpec" should {

"serialize default" in {
val a1: ADT1 = ADT1A(1)

a1.asJson.pretty(printer) should be("""{"a":1,"type":"adt1a"}""")
parse("""{"a":1,"type":"adt1a"}""").right.get.as[ADT1] should be(
Right(a1)
)

val b1: ADT1 = ADT1B(1)

b1.asJson.pretty(printer) should be("""{"b":1,"type":"adt1b"}""")
parse("""{"b":1,"type":"adt1b"}""").right.get.as[ADT1] should be(
Right(b1)
)
}

"serialize discriminator custom fieldname" in {
val a1: ADT1Custom = ADT1CustomA(1)

a1.asJson.pretty(printer) should be("""{"a":1,"_type":"adt1customa"}""")
parse("""{"a":1,"_type":"adt1customa"}""").right.get.as[ADT1Custom] should be(Right(a1))

val b1: ADT1Custom = ADT1CustomB(1)

b1.asJson.pretty(printer) should be("""{"b":1,"_type":"adt1customb"}""")
parse("""{"b":1,"_type":"adt1customb"}""").right.get.as[ADT1Custom] should be(Right(b1))
}

"serialize discriminator typed" in {
val a1: ADTTyped = ADTTypedA(1)

a1.asJson.pretty(printer) should be("""{"adttypeda":{"a":1}}""")
parse("""{"adttypeda":{"a":1}}""").right.get.as[ADTTyped] should be(
Right(a1)
)

val b1: ADTTyped = ADTTypedB(1)

b1.asJson.pretty(printer) should be("""{"adttypedb":{"b":1}}""")
parse("""{"adttypedb":{"b":1}}""").right.get.as[ADTTyped] should be(
Right(b1)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.circe.derivation.annotations

import io.circe._
import io.circe.parser._
import io.circe.syntax._
import org.scalatest._

object CustomJsonCodecSpecSamples {

@JsonCodec
case class Person(
@JsonKey("n") name: String,
@JsonNoDefault @JsonKey("a") age: Int = 0,
optional: Option[String] = None
)

@JsonCodec
case class RemoveEmptyPerson(
optional: Option[String] = None,
values: List[String] = Nil,
valuesSet: Set[String] = Set.empty[String],
obj: Json = Json.obj()
)

@JsonCodec
case class Group(name: String, team: Map[String, Person])

}

class CustomJsonCodecSpec extends WordSpec with Matchers {

import CustomJsonCodecSpecSamples._

implicit val printer = Printer.noSpaces.copy(dropNullValues = true)

"CustomJsonCodec" should {

io.circe.Decoder
"correct generate json" in {
val p1 = Person("andrea")

p1.asJson.pretty(printer) should be("{\"n\":\"andrea\"}")
parse("""{"n":"andrea"}""").right.get.as[Person] should be(Right(p1))
}

"remove empty values" in {
val p1 = RemoveEmptyPerson()

p1.asJson.pretty(printer) should be(
"""{"values":[],"valuesSet":[],"obj":{}}"""
)

val p2 = RemoveEmptyPerson(values = List("a"))

p2.asJson.pretty(printer) should be(
"""{"values":["a"],"valuesSet":[],"obj":{}}"""
)
parse("""{}""").right.get.as[RemoveEmptyPerson] should be(Right(p1))
}

"manage map" in {
val g1 = Group("name", Map("peter" -> Person("Peter", 18)))

g1.asJson.pretty(printer) should be(
"""{"name":"name","team":{"peter":{"n":"Peter","a":18}}}"""
)

}

}
}
Loading