diff --git a/build.sbt b/build.sbt index 211914d9..8618a164 100644 --- a/build.sbt +++ b/build.sbt @@ -19,8 +19,8 @@ lazy val root = project versionScheme := Some("early-semver"), libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.2.7" % Test, - "com.chuusai" %% "shapeless" % "2.3.3", - "software.amazon.awssdk" % "dynamodb" % "2.16.34", + "com.chuusai" %% "shapeless" % "2.3.4", + "software.amazon.awssdk" % "dynamodb" % "2.16.43", "org.typelevel" %% "cats-core" % "2.5.0", "org.typelevel" %% "cats-testkit-scalatest" % "2.1.3" % Test, "org.scalacheck" %% "scalacheck" % "1.15.3" % Test, @@ -109,5 +109,5 @@ lazy val sonatypeSettings = { } lazy val mimaSettings = Seq( - mimaPreviousArtifacts := Set("io.moia" %% "scynamo" % "0.7.0") + mimaPreviousArtifacts := Set.empty ) diff --git a/src/main/scala/scynamo/ScynamoDecoder.scala b/src/main/scala/scynamo/ScynamoDecoder.scala index 74d96275..3cda39f6 100644 --- a/src/main/scala/scynamo/ScynamoDecoder.scala +++ b/src/main/scala/scynamo/ScynamoDecoder.scala @@ -1,8 +1,5 @@ package scynamo -import java.time.Instant -import java.util.UUID -import java.util.concurrent.TimeUnit import cats.data.{EitherNec, NonEmptyChain} import cats.syntax.either._ import cats.syntax.parallel._ @@ -10,10 +7,14 @@ import cats.{Monad, SemigroupK} import scynamo.StackFrame.Index import scynamo.generic.auto.AutoDerivationUnlocked import scynamo.generic.{GenericScynamoDecoder, SemiautoDerivationDecoder} -import shapeless.{tag, Lazy} +import shapeless.labelled.{field, FieldType} import shapeless.tag.@@ +import shapeless.{tag, Lazy} import software.amazon.awssdk.services.dynamodb.model.AttributeValue +import java.time.Instant +import java.util.UUID +import java.util.concurrent.TimeUnit import scala.annotation.tailrec import scala.collection.compat._ import scala.concurrent.duration.{Duration, FiniteDuration} @@ -41,7 +42,7 @@ object StackFrame { case class Custom(name: String) extends StackFrame } -trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { +trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { self => def decode(attributeValue: AttributeValue): EitherNec[ScynamoDecodeError, A] def map[B](f: A => B): ScynamoDecoder[B] = @@ -57,6 +58,11 @@ trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { value => f(decode(value)) def defaultValue: Option[A] = None + + def withDefault(value: A): ScynamoDecoder[A] = new ScynamoDecoder[A] { + override def decode(attributeValue: AttributeValue) = self.decode(attributeValue) + override val defaultValue = Some(value) + } } object ScynamoDecoder extends DefaultScynamoDecoderInstances { @@ -167,7 +173,16 @@ trait DefaultScynamoDecoderInstances extends ScynamoDecoderFunctions with Scynam .map(_.toMap) } - implicit val attributeValueDecoder: ScynamoDecoder[AttributeValue] = attributeValue => Right(attributeValue) + implicit val attributeValueDecoder: ScynamoDecoder[AttributeValue] = + attributeValue => Right(attributeValue) + + implicit def fieldDecoder[K, V](implicit V: Lazy[ScynamoDecoder[V]]): ScynamoDecoder[FieldType[K, V]] = + new ScynamoDecoder[FieldType[K, V]] { + override def decode(attributeValue: AttributeValue) = + V.value.decode(attributeValue).map(field[K][V]) + override lazy val defaultValue = + V.value.defaultValue.map(field[K][V]) + } } trait ScynamoIterableDecoder extends LowestPrioAutoDecoder { diff --git a/src/main/scala/scynamo/ScynamoEncoder.scala b/src/main/scala/scynamo/ScynamoEncoder.scala index b03756cb..3c5b0a7b 100644 --- a/src/main/scala/scynamo/ScynamoEncoder.scala +++ b/src/main/scala/scynamo/ScynamoEncoder.scala @@ -7,6 +7,7 @@ import scynamo.StackFrame.{Index, MapKey} import scynamo.generic.auto.AutoDerivationUnlocked import scynamo.generic.{GenericScynamoEncoder, SemiautoDerivationEncoder} import shapeless._ +import shapeless.labelled.FieldType import shapeless.tag.@@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue @@ -115,6 +116,9 @@ trait DefaultScynamoEncoderInstances extends ScynamoIterableEncoder { case Left(value) => Left(value) case Right(value) => ScynamoEncoder[A].encode(value) } + + implicit def fieldEncoder[K, V](implicit V: Lazy[ScynamoEncoder[V]]): ScynamoEncoder[FieldType[K, V]] = + field => V.value.encode(field) } trait ScynamoIterableEncoder extends LowestPrioAutoEncoder { diff --git a/src/main/scala/scynamo/generic/ShapelessScynamoDecoder.scala b/src/main/scala/scynamo/generic/ShapelessScynamoDecoder.scala index 5a2d9a53..c3ae42be 100644 --- a/src/main/scala/scynamo/generic/ShapelessScynamoDecoder.scala +++ b/src/main/scala/scynamo/generic/ShapelessScynamoDecoder.scala @@ -9,6 +9,8 @@ import shapeless._ import shapeless.labelled._ import software.amazon.awssdk.services.dynamodb.model.AttributeValue +import java.util + trait ShapelessScynamoDecoder[Base, A] { def decodeMap(value: java.util.Map[String, AttributeValue]): EitherNec[ScynamoDecodeError, A] } @@ -20,7 +22,7 @@ trait DecoderHListInstances extends ScynamoDecoderFunctions { implicit def deriveHCons[Base, K <: Symbol, V, T <: HList](implicit key: Witness.Aux[K], - sv: Lazy[ScynamoDecoder[V]], + sv: ScynamoDecoder[FieldType[K, V]], st: ShapelessScynamoDecoder[Base, T], opts: ScynamoDerivationOpts[Base] = ScynamoDerivationOpts.default[Base] ): ShapelessScynamoDecoder[Base, FieldType[K, V] :: T] = @@ -28,13 +30,13 @@ trait DecoderHListInstances extends ScynamoDecoderFunctions { val fieldName = opts.transform(key.value.name) val fieldAttrValue = Option(value.get(fieldName)) - val decodedHead = (fieldAttrValue, sv.value.defaultValue) match { - case (Some(field), _) => sv.value.decode(field).leftMap(x => x.map(_.push(Attr(fieldName)))) + val decodedHead = (fieldAttrValue, sv.defaultValue) match { + case (Some(field), _) => sv.decode(field).leftMap(_.map(_.push(Attr(fieldName)))) case (None, Some(default)) => Right(default) case (None, None) => Either.leftNec(ScynamoDecodeError.missingField(fieldName, value)) } - (decodedHead.map(field[K](_)), st.decodeMap(value)).mapN(_ :: _) + (decodedHead, st.decodeMap(value)).mapN(_ :: _) } } @@ -49,13 +51,12 @@ trait DecoderCoproductInstances extends ScynamoDecoderFunctions { sv: Lazy[ScynamoDecoder[V]], st: ShapelessScynamoDecoder[Base, T], opts: ScynamoSealedTraitOpts[Base] = ScynamoSealedTraitOpts.default[Base] - ): ShapelessScynamoDecoder[Base, FieldType[K, V] :+: T] = - value => { - if (value.containsKey(opts.discriminator)) - deriveCConsTagged[Base, K, V, T].decodeMap(value) - else - deriveCConsNested[Base, K, V, T].decodeMap(value) - } + ): ShapelessScynamoDecoder[Base, FieldType[K, V] :+: T] = new ShapelessScynamoDecoder[Base, FieldType[K, V] :+: T] { + lazy val tagged = deriveCConsTagged[Base, K, V, T] + lazy val nested = deriveCConsNested[Base, K, V, T] + override def decodeMap(value: util.Map[String, AttributeValue]) = + (if (value.containsKey(opts.discriminator)) tagged else nested).decodeMap(value) + } def deriveCConsTagged[Base, K <: Symbol, V, T <: Coproduct](implicit key: Witness.Aux[K], diff --git a/src/main/scala/scynamo/generic/ShapelessScynamoEncoder.scala b/src/main/scala/scynamo/generic/ShapelessScynamoEncoder.scala index d3d1032f..8b9ee234 100644 --- a/src/main/scala/scynamo/generic/ShapelessScynamoEncoder.scala +++ b/src/main/scala/scynamo/generic/ShapelessScynamoEncoder.scala @@ -23,14 +23,14 @@ trait EncoderHListInstances { implicit def deriveHCons[Base, K <: Symbol, V, T <: HList](implicit key: Witness.Aux[K], - sv: Lazy[ScynamoEncoder[V]], + sv: ScynamoEncoder[FieldType[K, V]], st: ShapelessScynamoEncoder[Base, T], opts: ScynamoDerivationOpts[Base] = ScynamoDerivationOpts.default[Base] ): ShapelessScynamoEncoder[Base, FieldType[K, V] :: T] = value => { val fieldName = opts.transform(key.value.name) - val encodedHead = sv.value.encode(value.head).leftMap(x => x.map(_.push(Attr(fieldName)))) + val encodedHead = sv.encode(value.head).leftMap(_.map(_.push(Attr(fieldName)))) val encodedTail = st.encodeMap(value.tail) (encodedHead, encodedTail).parMapN { case (head, tail) => diff --git a/src/test/scala/scynamo/SemiautoDerivationTest.scala b/src/test/scala/scynamo/SemiautoDerivationTest.scala index a2fb4f11..56e01127 100644 --- a/src/test/scala/scynamo/SemiautoDerivationTest.scala +++ b/src/test/scala/scynamo/SemiautoDerivationTest.scala @@ -6,6 +6,8 @@ import scynamo.Mixed.{CaseClass, CaseObject} import scynamo.generic.{ScynamoDerivationOpts, ScynamoSealedTraitOpts} import scynamo.generic.semiauto._ import scynamo.syntax.all._ +import shapeless.Witness +import shapeless.labelled.FieldType import software.amazon.awssdk.services.dynamodb.model.AttributeValue class SemiautoDerivationTest extends UnitTest { @@ -113,6 +115,30 @@ class SemiautoDerivationTest extends UnitTest { |""".stripMargin shouldNot compile } } + + "a custom field type encoder or decoder is provided" should { + "override the default one" in { + final case class Apple(variety: String, ripe: Boolean) + object Apple { + val variety = Witness(Symbol("variety")) + val ripe = Witness(Symbol("ripe")) + + implicit val varietyEncoder: ScynamoEncoder[FieldType[variety.T, String]] = + ScynamoEncoder.fieldEncoder(ScynamoEncoder.stringEncoder.contramap[String](_.trim)) + + implicit val ripeDecoder: ScynamoDecoder[FieldType[ripe.T, Boolean]] = + ScynamoDecoder.fieldDecoder(ScynamoDecoder.booleanDecoder.withDefault(true)) + + implicit val codec: ObjectScynamoCodec[Apple] = + ObjectScynamoCodec.deriveScynamoCodec[Apple] + } + + val encoded = Apple("\tGranny Smith\n", ripe = false).encoded.flatMap(_.decode[Apple]) + val decoded = Map("variety" -> "Pink Lady".encodedUnsafe).encodedUnsafe.decode[Apple] + encoded should ===(Right(Apple("Granny Smith", ripe = false))) + decoded should ===(Right(Apple("Pink Lady", ripe = true))) + } + } } sealed trait Shape