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

Implement strict decoding #425

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ sealed trait Configuration {
*/
def transformConstructorNames: String => String

def strictDecoding: Boolean

def useDefaults: Boolean

def discriminator: Option[String]
Expand Down Expand Up @@ -67,7 +69,8 @@ object Configuration {
transformMemberNames: String => String,
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
discriminator: Option[String],
strictDecoding: Boolean = false
) extends Configuration {

type Config = Codec
Expand All @@ -94,7 +97,8 @@ object Configuration {
transformMemberNames: String => String,
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
discriminator: Option[String],
strictDecoding: Boolean = false
) extends Configuration {

type Config = DecodeOnly
Expand All @@ -119,7 +123,8 @@ object Configuration {
transformMemberNames: String => String,
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
discriminator: Option[String],
strictDecoding: Boolean = false
) extends Configuration {

type Config = EncodeOnly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context)
val Type = tq"$objName.type"
val (decoder, encoder, codec) = (
q"""implicit val $decoderName: $DecoderClass[$Type] =
_root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""",
_root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""",
q"""implicit val $encoderName: $AsObjectEncoderClass[$Type] =
_root_.io.circe.derivation.deriveEncoder[$Type]($transformNames, $cfgDiscriminator)""",
q"""implicit val $codecName: $AsObjectCodecClass[$Type] =
_root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)"""
_root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)"""
)
codecType match {
case JsonCodecType.Both => codec
Expand Down Expand Up @@ -141,6 +141,8 @@ private[derivation] final class GenericJsonCodecMacros(val c: blackbox.Context)
q"$config.useDefaults"
private[this] val cfgDiscriminator =
q"$config.discriminator"
private[this] val cfgStrictDecoding =
q"$config.strictDecoding"

private[this] def codec(clsDef: ClassDef): Tree = {
val tpname = clsDef.name
Expand All @@ -153,11 +155,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]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""",
_root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""",
q"""implicit val $encoderName: $AsObjectEncoderClass[$Type] =
_root_.io.circe.derivation.deriveEncoder[$Type]($transformNames, $cfgDiscriminator)""",
q"""implicit val $codecName: $AsObjectCodecClass[$Type] =
_root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)"""
_root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)"""
)
} else {
val tparamNames = tparams.map(_.name)
Expand All @@ -172,13 +174,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]($transformNames, $cfgUseDefaults, $cfgDiscriminator)""",
_root_.io.circe.derivation.deriveDecoder[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)""",
q"""implicit def $encoderName[..$tparams](implicit ..$encodeParams): $AsObjectEncoderClass[$Type] =
_root_.io.circe.derivation.deriveEncoder[$Type]($transformNames, $cfgDiscriminator)""",
q"""implicit def $codecName[..$tparams](implicit
..${decodeParams ++ encodeParams}
): $AsObjectCodecClass[$Type] =
_root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator)"""
_root_.io.circe.derivation.deriveCodec[$Type]($transformNames, $cfgUseDefaults, $cfgDiscriminator, $cfgStrictDecoding)"""
)
}
codecType match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
private[this] val defaultDiscriminator =
c.Expr[Option[String]](q"_root_.scala.None")
private[this] val trueExpression = c.Expr[Boolean](q"true")
private[this] val falseExpression = c.Expr[Boolean](q"false")

private[this] def failWithMessage(message: String): Nothing = c.abort(c.enclosingPosition, message)
private[this] def nameOf(s: Symbol): String = s.asClass.name.decodedName.toString.trim
Expand Down Expand Up @@ -248,23 +249,25 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
}

def materializeDecoder[T: c.WeakTypeTag]: c.Expr[Decoder[T]] =
materializeDecoderImpl[T](None, trueExpression, defaultDiscriminator)
materializeDecoderImpl[T](None, trueExpression, defaultDiscriminator, falseExpression)

def materializeEncoder[T: c.WeakTypeTag]: c.Expr[Encoder.AsObject[T]] =
materializeEncoderImpl[T](None, defaultDiscriminator)

def materializeCodec[T: c.WeakTypeTag]: c.Expr[Codec.AsObject[T]] =
materializeCodecImpl[T](None, trueExpression, defaultDiscriminator)
materializeCodecImpl[T](None, trueExpression, defaultDiscriminator, falseExpression)

def materializeDecoderWithTransformNames[T: c.WeakTypeTag](
transformNames: c.Expr[String => String],
useDefaults: c.Expr[Boolean],
discriminator: c.Expr[Option[String]]
discriminator: c.Expr[Option[String]],
strictDecoding: c.Expr[Boolean]
): c.Expr[Decoder[T]] =
materializeDecoderImpl[T](
Some(transformNames),
useDefaults,
discriminator
discriminator,
strictDecoding
)

def materializeDecoderWithTransformNamesAndDefaults[T: c.WeakTypeTag](
Expand All @@ -273,7 +276,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
materializeDecoderImpl[T](
Some(transformNames),
trueExpression,
defaultDiscriminator
defaultDiscriminator,
falseExpression
)

def materializeEncoderWithTransformNames[T: c.WeakTypeTag](
Expand All @@ -290,12 +294,14 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
def materializeCodecWithTransformNames[T: c.WeakTypeTag](
transformNames: c.Expr[String => String],
useDefaults: c.Expr[Boolean],
discriminator: c.Expr[Option[String]]
discriminator: c.Expr[Option[String]],
strictDecoding: c.Expr[Boolean]
): c.Expr[Codec.AsObject[T]] =
materializeCodecImpl[T](
Some(transformNames),
useDefaults,
discriminator
discriminator,
strictDecoding
)

def materializeCodecWithTransformNamesAndDefaults[T: c.WeakTypeTag](
Expand All @@ -304,13 +310,15 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
materializeCodecImpl[T](
Some(transformNames),
trueExpression,
defaultDiscriminator
defaultDiscriminator,
falseExpression
)

private[this] def materializeCodecImpl[T: c.WeakTypeTag](
transformNames: Option[c.Expr[String => String]],
useDefaults: c.Expr[Boolean],
discriminator: c.Expr[Option[String]]
discriminator: c.Expr[Option[String]],
strictDecoding: c.Expr[Boolean]
): c.Expr[Codec.AsObject[T]] = {
val tpe = weakTypeOf[T]

Expand All @@ -319,7 +327,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
materializeCodecCaseClassImpl[T](
transformNames,
useDefaults,
discriminator
discriminator,
strictDecoding
)
} else {
materializeCodecTraitImpl[T](
Expand All @@ -334,26 +343,29 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
private[this] def materializeDecoderImpl[T: c.WeakTypeTag](
transformNames: Option[c.Expr[String => String]],
useDefaults: c.Expr[Boolean],
discriminator: c.Expr[Option[String]]
discriminator: c.Expr[Option[String]],
strictDecoding: c.Expr[Boolean]
): c.Expr[Decoder[T]] = {
val tpe = weakTypeOf[T]

val subclasses = allSubclasses(tpe.typeSymbol.asClass)
if (subclasses.isEmpty) {
materializeDecoderCaseClassImpl[T](transformNames, useDefaults)
materializeDecoderCaseClassImpl[T](transformNames, useDefaults, strictDecoding)
} else {
materializeDecoderTraitImpl[T](
transformNames,
subclasses,
discriminator
discriminator,
strictDecoding
)
}
}

private[this] def materializeDecoderTraitImpl[T: c.WeakTypeTag](
transformConstructorNames: Option[c.Expr[String => String]],
subclasses: Set[Symbol],
discriminator: c.Expr[Option[String]]
discriminator: c.Expr[Option[String]],
strictDecoding: c.Expr[Boolean]
): c.Expr[Decoder[T]] = {
val tpe = weakTypeOf[T]

Expand Down Expand Up @@ -420,7 +432,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {

private[this] def materializeDecoderCaseClassImpl[T: c.WeakTypeTag](
transformMemberNames: Option[c.Expr[String => String]],
useDefaults: c.Expr[Boolean]
useDefaults: c.Expr[Boolean],
strictDecoding: c.Expr[Boolean]
): c.Expr[Decoder[T]] = {
val tpe = weakTypeOf[T]

Expand Down Expand Up @@ -527,7 +540,7 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
}
"""

c.Expr[Decoder[T]](
val nonStrictDecoder = c.Expr[Decoder[T]](
q"""
new _root_.io.circe.Decoder[$tpe] {
..$instanceDefs
Expand All @@ -547,6 +560,40 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
}
"""
)

val expectedFields: Tree = repr.paramListsWithNames.flatten match {
case Nil => q"""_root_.scala.Predef.Set.empty[String]"""
case head :: tail =>
tail.foldLeft(q"""_root_.scala.Predef.Set(${head._1.keyName.getOrElse(transformName(head._1.decodedName))})""") {
case (acc, (member, _)) =>
q"""$acc ++ _root_.scala.Predef.Set(${member.keyName.getOrElse(transformName(member.decodedName))})"""
}
}

c.Expr[Decoder[T]](
q"""
val nonStrictDecoder = $nonStrictDecoder

if ($strictDecoding) {
val expectedFields = $expectedFields
nonStrictDecoder.validate { cursor: _root_.io.circe.HCursor =>
val maybeUnexpectedErrors = for {
json <- cursor.focus
jsonKeys <- json.hcursor.keys
unexpected = jsonKeys.toSet -- expectedFields
} yield {
unexpected.toList.map { unexpectedField =>
s"Unexpected field: [$$unexpectedField]; valid fields: $${expectedFields.mkString(", ")}"
}
}

maybeUnexpectedErrors.getOrElse(List("Couldn't determine decoded fields."))
}
} else {
nonStrictDecoder
}
"""
)
}
}
}
Expand Down Expand Up @@ -880,7 +927,8 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
private[this] def materializeCodecCaseClassImpl[T: c.WeakTypeTag](
transformMemberNames: Option[c.Expr[String => String]],
useDefaults: c.Expr[Boolean],
discriminator: c.Expr[Option[String]]
discriminator: c.Expr[Option[String]],
strictDecoding: c.Expr[Boolean]
): c.Expr[Codec.AsObject[T]] = {
val tpe = weakTypeOf[T]

Expand Down Expand Up @@ -1030,7 +1078,7 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
}
"""

c.Expr[Codec.AsObject[T]](
val nonStrictCodec = c.Expr[Codec.AsObject[T]](
q"""
new _root_.io.circe.Codec.AsObject[$tpe] {
..$encoderInstanceDefs
Expand All @@ -1054,6 +1102,49 @@ class DerivationMacros(val c: blackbox.Context) extends ScalaVersionCompat {
}
"""
)

val expectedFields: Tree = repr.paramListsWithNames.flatten match {
case Nil => q"""_root_.scala.Predef.Set.empty[String]"""
case head :: tail =>
tail.foldLeft(q"""_root_.scala.Predef.Set(${head._1.keyName.getOrElse(transformName(head._1.decodedName))})""") {
case (acc, (member, _)) =>
q"""$acc ++ _root_.scala.Predef.Set(${member.keyName.getOrElse(transformName(member.decodedName))})"""
}
}

c.Expr[Codec.AsObject[T]](
q"""
val nonStrictCodec = $nonStrictCodec

if ($strictDecoding) {
val expectedFields = $expectedFields
val strictDecoder = nonStrictCodec.validate { cursor: _root_.io.circe.HCursor =>
val maybeUnexpectedErrors = for {
json <- cursor.focus
jsonKeys <- json.hcursor.keys
unexpected = jsonKeys.toSet -- expectedFields
} yield {
unexpected.toList.map { unexpectedField =>
s"Unexpected field: [$$unexpectedField]; valid fields: $${expectedFields.mkString(", ")}"
}
}

maybeUnexpectedErrors.getOrElse(List("Couldn't determine decoded fields."))
}
new _root_.io.circe.Codec.AsObject[$tpe] {
final def encodeObject(a: $tpe): _root_.io.circe.JsonObject = nonStrictCodec.encodeObject(a)

final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[$tpe] = strictDecoder(c)

final override def decodeAccumulating(
c: _root_.io.circe.HCursor
): _root_.io.circe.Decoder.AccumulatingResult[$tpe] = strictDecoder.decodeAccumulating(c)
}
} else {
nonStrictCodec
}
"""
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ package object derivation {
final def deriveDecoder[A](
transformNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
discriminator: Option[String],
strictDecoding: Boolean
): Decoder[A] =
macro DerivationMacros.materializeDecoderWithTransformNames[A]

Expand All @@ -29,7 +30,8 @@ package object derivation {
final def deriveCodec[A](
transformNames: String => String,
useDefaults: Boolean,
discriminator: Option[String]
discriminator: Option[String],
strictDecoding: Boolean
): Codec.AsObject[A] =
macro DerivationMacros.materializeCodecWithTransformNames[A]

Expand Down
Loading