diff --git a/d4s-circe/src/test/scala/d4s/codecs/DynamoCodecTest.scala b/d4s-circe/src/test/scala/d4s/codecs/DynamoCodecTest.scala index 3d245d2..aa8bfd5 100644 --- a/d4s-circe/src/test/scala/d4s/codecs/DynamoCodecTest.scala +++ b/d4s-circe/src/test/scala/d4s/codecs/DynamoCodecTest.scala @@ -3,8 +3,8 @@ package d4s.codecs import d4s.codecs.Fixtures._ import d4s.codecs.circe.{D4SCirceAttributeCodec, D4SCirceCodec} import d4s.models.DynamoException.DecoderException -import io.circe.{Decoder, Encoder} import io.circe.generic.extras.semiauto +import io.circe.{Decoder, Encoder} import org.scalacheck.Prop import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.scalacheck.Checkers @@ -13,6 +13,11 @@ import software.amazon.awssdk.core.SdkBytes import scala.jdk.CollectionConverters._ final class DynamoCodecTest extends AnyWordSpec with Checkers { + "decode options from missed attribute" in { + val decoder = D4SDecoder.optionDecoder[Int] + assert(decoder.decodeOptional(None, "").exists(_.isEmpty)) + } + "encode/decode TestCaseClass" in check { Prop.forAllNoShrink { testData: TestCaseClass => diff --git a/d4s/src/main/scala/d4s/codecs/D4SDecoder.scala b/d4s/src/main/scala/d4s/codecs/D4SDecoder.scala index 796c9dd..e7d8631 100644 --- a/d4s/src/main/scala/d4s/codecs/D4SDecoder.scala +++ b/d4s/src/main/scala/d4s/codecs/D4SDecoder.scala @@ -23,6 +23,12 @@ import scala.util.control.NonFatal trait D4SDecoder[T] { def decode(attr: AttributeValue): Either[DecoderException, T] def decodeObject(item: Map[String, AttributeValue]): Either[DecoderException, T] + def decodeOptional(attr: Option[AttributeValue], label: String): Either[DecoderException, T] = { + attr match { + case Some(value) => decode(value) + case None => Left(DecoderException(s"Cannot find parameter with name `$label` of type [${this.getClass.getSimpleName}].", None)) + } + } final def decodeObject(item: java.util.Map[String, AttributeValue]): Either[DecoderException, T] = decodeObject(item.asScala.toMap) @@ -80,10 +86,8 @@ object D4SDecoder extends D4SDecoderScala213 { item => ctx.constructMonadic { p => - item.get(p.label) match { - case Some(value) => p.typeclass.decode(value) - case None => Left(DecoderException(s"Cannot find parameter with name ${p.label}", None)) - } + val label = p.label + p.typeclass.decodeOptional(item.get(label), label) } } @@ -92,7 +96,7 @@ object D4SDecoder extends D4SDecoderScala213 { if (item.m().isEmpty) { ctx.subtypes .find(_.typeName.short == item.s()) - .toRight(DecoderException(s" Cannot decode item of type ${ctx.typeName.full} from string: ${item.s()}", None)) + .toRight(DecoderException(s"Cannot decode item of type [${ctx.typeName.full}] from string: ${item.s()}", None)) .flatMap(_.typeclass.decode(item)) } else { if (item.m().size != 1) { @@ -101,7 +105,7 @@ object D4SDecoder extends D4SDecoderScala213 { val (typeName, attrValue) = item.m().asScala.head ctx.subtypes .find(_.typeName.short == typeName) - .toRight(DecoderException(s"Cannot find a subtype $typeName for a sealed trait ${ctx.typeName.full}", None)) + .toRight(DecoderException(s"Cannot find a subtype [$typeName] for a sealed trait [${ctx.typeName.full}]", None)) .flatMap(_.typeclass.decode(attrValue)) } } @@ -188,9 +192,14 @@ object D4SDecoder extends D4SDecoderScala213 { } } - implicit def optionDecoder[A](implicit T: D4SDecoder[A]): D4SDecoder[Option[A]] = attributeDecoder { - attr => - if (attr.nul()) Right(None) else T.decode(attr).map(Some(_)) + implicit def optionDecoder[A](implicit T: D4SDecoder[A]): D4SDecoder[Option[A]] = new D4SDecoder[Option[A]] { + override def decode(attr: AttributeValue): Either[DecoderException, Option[A]] = decodeOptional(Some(attr), "") + override def decodeObject(item: Map[String, AttributeValue]): Either[DecoderException, Option[A]] = decode(AttributeValue.builder().m(item.asJava).build()) + override def decodeOptional(attr: Option[AttributeValue], label: String): Either[DecoderException, Option[A]] = attr match { + case None => Right(None) + case Some(attr) if attr.nul() => Right(None) + case Some(attr) => T.decode(attr).map(Some(_)) + } } implicit def eitherDecoder[A: D4SDecoder, B: D4SDecoder]: D4SDecoder[Either[A, B]] = D4SDecoder.derived diff --git a/d4s/src/main/scala/d4s/models/DynamoException.scala b/d4s/src/main/scala/d4s/models/DynamoException.scala index 038ba06..46c7adf 100644 --- a/d4s/src/main/scala/d4s/models/DynamoException.scala +++ b/d4s/src/main/scala/d4s/models/DynamoException.scala @@ -32,7 +32,7 @@ object DynamoException { } } - final case class DecoderException(override val message: String, maybeCause: Option[Throwable]) extends DynamoException(message, maybeCause.orNull) { + final case class DecoderException(override val message: String, maybeCause: Option[Throwable]) extends DynamoException(message, maybeCause.getOrElse(new RuntimeException(message))) { def union(that: DecoderException): DecoderException = { val errorLog = message + "\n" + that.getMessage DecoderException(errorLog, maybeCause.orElse(that.maybeCause))