Skip to content

Commit

Permalink
Merge branch 'master' of github.com:moia-oss/scynamo
Browse files Browse the repository at this point in the history
  • Loading branch information
CristinaHG committed Apr 21, 2021
2 parents 40f16d3 + 7efe29d commit c0bd701
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 22 deletions.
6 changes: 3 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -109,5 +109,5 @@ lazy val sonatypeSettings = {
}

lazy val mimaSettings = Seq(
mimaPreviousArtifacts := Set("io.moia" %% "scynamo" % "0.7.0")
mimaPreviousArtifacts := Set.empty
)
27 changes: 21 additions & 6 deletions src/main/scala/scynamo/ScynamoDecoder.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
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._
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}
Expand Down Expand Up @@ -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] =
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/scynamo/ScynamoEncoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 12 additions & 11 deletions src/main/scala/scynamo/generic/ShapelessScynamoDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
Expand All @@ -20,21 +22,21 @@ 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] =
value => {
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(_ :: _)
}
}

Expand All @@ -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],
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/scynamo/generic/ShapelessScynamoEncoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
26 changes: 26 additions & 0 deletions src/test/scala/scynamo/SemiautoDerivationTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c0bd701

Please sign in to comment.