Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
WIP Add support for transforming constructor names.
  • Loading branch information
danxmoran committed Sep 17, 2019
commit aae0646d0a26f8142298c00afd852b3350e5711a
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ sealed trait Configuration {
*/
def transformMemberNames: String => String

/** Transforms the value of any constructor names in the JSON, allowing,
* for example, formatting or case changes
*/
def transformConstructorNames: String => String

def useDefaults: Boolean

def discriminator: Option[String]

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

// TODO: Naming???
protected def getC(transformConstructorNames: String => String): Config

protected def applyDiscriminator(discriminator: Option[String]): Config

/** Creates a configuration which produces snake cased member names */
Expand All @@ -34,6 +42,14 @@ sealed trait Configuration {
final def withKebabCaseMemberNames: Config =
getA(renaming.kebabCase)

/** Creates a configuration which produces snake cased constructor names */
final def withSnakeCaseConstructorNames: Config =
getC(renaming.snakeCase)

/** Creates a configuration which produces kebab cased constructor names */
final def withKebabCaseConstructorNames: Config =
getC(renaming.kebabCase)

final def withDiscriminator(name: String): Config =
applyDiscriminator(Some(name))
}
Expand All @@ -47,19 +63,23 @@ object Configuration {
*/
final case class Codec(
transformMemberNames: String => String,
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
) extends Configuration {

type Config = Codec

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

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

protected final def applyDiscriminator(
discriminator: Option[String]
): Codec =
Codec(transformMemberNames, useDefaults, discriminator)
Codec(transformMemberNames, transformConstructorNames, useDefaults, discriminator)
}

/** Configuration allowing customisation of JSON produced when encoding or
Expand All @@ -69,17 +89,21 @@ object Configuration {
*/
final case class DecodeOnly(
transformMemberNames: String => String,
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
) extends Configuration {

type Config = DecodeOnly

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

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

protected final def applyDiscriminator(discriminator: Option[String]) =
DecodeOnly(transformMemberNames, useDefaults, discriminator)
DecodeOnly(transformMemberNames, transformConstructorNames, useDefaults, discriminator)
}

/** Configuration allowing customisation of JSON produced when encoding or
Expand All @@ -89,29 +113,33 @@ object Configuration {
*/
final case class EncodeOnly(
transformMemberNames: String => String,
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
) extends Configuration {

type Config = EncodeOnly

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

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

protected final def applyDiscriminator(discriminator: Option[String]) =
EncodeOnly(transformMemberNames, useDefaults, discriminator)
EncodeOnly(transformMemberNames, transformConstructorNames, useDefaults, discriminator)

}

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

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

/** Create a default configuration with **only** decoder */
val decodeOnly: DecodeOnly =
DecodeOnly(identity, true, None)
DecodeOnly(identity, identity, true, None)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class JsonCodec(
def macroTransform(annottees: Any*): Any = macro GenericJsonCodecMacros.jsonCodecAnnotationMacro
}

// FIXME: Should these annotations also handle constructor transformations?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Biggest open question feature-wise, I think

Copy link
Member

Choose a reason for hiding this comment

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

I'd vote no.

class SnakeCaseJsonCodec extends scala.annotation.StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro GenericJsonCodecMacros.jsonCodecAnnotationMacro
}
Expand Down Expand Up @@ -103,6 +104,8 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context)

private[this] val cfgTransformMemberNames =
q"$config.transformMemberNames"
private[this] val cfgTransformConstructorNames =
q"$config.transformConstructorNames"
private[this] val cfgUseDefaults =
q"$config.useDefaults"
private[this] val cfgDiscriminator =
Expand All @@ -118,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]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)""",
_root_.io.circe.derivation.deriveDecoder[$Type]($cfgTransformMemberNames, $cfgTransformConstructorNames, $cfgUseDefaults, $cfgDiscriminator)""",
q"""implicit val $encoderName: $AsObjectEncoderClass[$Type] =
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgTransformMemberNames, $cfgDiscriminator)""",
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgTransformMemberNames, $cfgTransformConstructorNames, $cfgDiscriminator)""",
q"""implicit val $codecName: $AsObjectCodecClass[$Type] =
_root_.io.circe.derivation.deriveCodec[$Type]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)"""
_root_.io.circe.derivation.deriveCodec[$Type]($cfgTransformMemberNames, $cfgTransformConstructorNames, $cfgUseDefaults, $cfgDiscriminator)"""
)
} else {
val tparamNames = tparams.map(_.name)
Expand All @@ -138,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]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)""",
_root_.io.circe.derivation.deriveDecoder[$Type]($cfgTransformMemberNames, $cfgTransformConstructorNames, $cfgUseDefaults, $cfgDiscriminator)""",
q"""implicit def $encoderName[..$tparams](implicit ..$encodeParams): $AsObjectEncoderClass[$Type] =
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgTransformMemberNames, $cfgDiscriminator)""",
_root_.io.circe.derivation.deriveEncoder[$Type]($cfgTransformMemberNames, $cfgTransformConstructorNames, $cfgDiscriminator)""",
q"""implicit def $codecName[..$tparams](implicit
..${decodeParams ++ encodeParams}
): $AsObjectCodecClass[$Type] =
_root_.io.circe.derivation.deriveCodec[$Type]($cfgTransformMemberNames, $cfgUseDefaults, $cfgDiscriminator)"""
_root_.io.circe.derivation.deriveCodec[$Type]($cfgTransformMemberNames, $cfgTransformConstructorNames, $cfgUseDefaults, $cfgDiscriminator)"""
)
}
codecType match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ object JsonCodecADTSpecSamples {

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

// TODO: Add test cases for constructor transformations
@JsonCodec(Configuration.default.withKebabCaseConstructorNames)
sealed trait ADTTransformed

@JsonCodec case class ADTTransformed1(a: Int) extends ADTTransformed
@JsonCodec case class ADTTransformed2(b: Int) extends ADTTransformed

@JsonCodec(Configuration.default.withSnakeCaseConstructorNames.withDiscriminator("_type"))
sealed trait ADTSnakeDiscriminator

@JsonCodec case class ADTSnakeDiscriminatorA(a: Int) extends ADTSnakeDiscriminator
@JsonCodec case class ADTSnakeDiscriminatorB(b: Int) extends ADTSnakeDiscriminator
}

class JsonCodecADTSpec extends WordSpec with Matchers {
Expand Down Expand Up @@ -77,5 +90,29 @@ class JsonCodecADTSpec extends WordSpec with Matchers {
Right(b1)
)
}

"transform constructor names" in {
val a1: ADTTransformed = ADTTransformed1(1)

a1.asJson.pretty(printer) should be("""{"adt-transformed1":{"a":1}}""")
parse("""{"adt-transformed1":{"a":1}}""").right.get.as[ADTTransformed] should be(Right(a1))

val b1: ADTTransformed = ADTTransformed2(1)

b1.asJson.pretty(printer) should be("""{"adt-transformed2":{"b":1}}""")
parse("""{"adt-transformed2":{"b":1}}""").right.get.as[ADTTransformed] should be(Right(b1))
}

"transform constructor names with a discriminator" in {
val a1: ADTSnakeDiscriminator = ADTSnakeDiscriminatorA(1)

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

val b1: ADTSnakeDiscriminator = ADTSnakeDiscriminatorB(1)

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