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

Optimize encoders and skip nul fields #247

Merged
merged 7 commits into from
Jul 8, 2021
Merged
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
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ lazy val scalacOptions_2_13 = Seq(
"-Xfatal-warnings",
"-Ywarn-dead-code",
"-Ymacro-annotations",
"-Xlint:_,-byname-implicit",
"-Xlint:_,-byname-implicit,-unused",
"-Wunused:_,-imports,-synthetics",
"-Xsource:3"
)

Expand Down
21 changes: 0 additions & 21 deletions src/main/scala/scynamo/ScynamoDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,6 @@ import scala.concurrent.duration.{Duration, FiniteDuration}
import scala.jdk.CollectionConverters._
import scala.util.control.NonFatal

case class ErrorStack(frames: List[StackFrame]) {
def push(frame: StackFrame): ErrorStack = ErrorStack(frame +: frames)

override def toString: String =
frames.mkString("ErrorStack(", " -> ", ")")
}

object ErrorStack {
val empty: ErrorStack = ErrorStack(List.empty)
}

sealed trait StackFrame extends Product with Serializable
object StackFrame {
case class Attr(name: String) extends StackFrame
case class Case(name: String) extends StackFrame
case class Enum(name: String) extends StackFrame
case class Index(value: Int) extends StackFrame
case class MapKey[A](value: A) extends StackFrame
case class Custom(name: String) extends StackFrame
}

trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { self =>
def decode(attributeValue: AttributeValue): EitherNec[ScynamoDecodeError, A]

Expand Down
231 changes: 141 additions & 90 deletions src/main/scala/scynamo/ScynamoEncoder.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package scynamo

import cats.data.EitherNec
import cats.syntax.either._
import cats.syntax.parallel._
import cats.Contravariant
import cats.data.{Chain, EitherNec, NonEmptyChain}
import cats.syntax.all._
import scynamo.StackFrame.{Index, MapKey}
import scynamo.generic.auto.AutoDerivationUnlocked
import scynamo.generic.{GenericScynamoEncoder, SemiautoDerivationEncoder}
Expand All @@ -12,119 +12,151 @@ import shapeless.tag.@@
import software.amazon.awssdk.services.dynamodb.model.AttributeValue

import java.time.Instant
import java.util.UUID
import java.util.{Collections, UUID}
import scala.collection.compat._
import scala.collection.immutable.Seq
import scala.concurrent.duration.{Duration, FiniteDuration}
import scala.jdk.CollectionConverters._

trait ScynamoEncoder[A] { self =>
def encode(value: A): EitherNec[ScynamoEncodeError, AttributeValue]

def contramap[B](f: B => A): ScynamoEncoder[B] = value => self.encode(f(value))
def contramap[B](f: B => A): ScynamoEncoder[B] =
ScynamoEncoder.instance(value => self.encode(f(value)))
Copy link
Member

Choose a reason for hiding this comment

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

(Nitpicking:) I'm irritated by all these new line breaks. Do you make them on purpose?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes - I find it hard to read such long lines without a break

}

object ScynamoEncoder extends DefaultScynamoEncoderInstances {
def apply[A](implicit instance: ScynamoEncoder[A]): ScynamoEncoder[A] = instance

// SAM syntax generates anonymous classes because of non-abstract methods like `contramap`.
private[scynamo] def instance[A](f: A => EitherNec[ScynamoEncodeError, AttributeValue]): ScynamoEncoder[A] = f(_)
markus1189 marked this conversation as resolved.
Show resolved Hide resolved
}

trait DefaultScynamoEncoderInstances extends ScynamoIterableEncoder {
implicit val stringEncoder: ScynamoEncoder[String] = value => Right(AttributeValue.builder().s(value).build())
private val rightNul = Right(AttributeValue.builder.nul(true).build())
saeltz marked this conversation as resolved.
Show resolved Hide resolved

implicit val catsInstances: Contravariant[ScynamoEncoder] = new Contravariant[ScynamoEncoder] {
override def contramap[A, B](fa: ScynamoEncoder[A])(f: B => A) = fa.contramap(f)
}

private[this] val numberStringEncoder: ScynamoEncoder[String] = value => Right(AttributeValue.builder().n(value).build())
implicit val stringEncoder: ScynamoEncoder[String] =
ScynamoEncoder.instance(value => Right(AttributeValue.builder.s(value).build()))

implicit val intEncoder: ScynamoEncoder[Int] = numberStringEncoder.contramap[Int](_.toString)
private[this] val numberStringEncoder: ScynamoEncoder[String] =
ScynamoEncoder.instance(value => Right(AttributeValue.builder.n(value).build()))

implicit val longEncoder: ScynamoEncoder[Long] = numberStringEncoder.contramap[Long](_.toString)
implicit val intEncoder: ScynamoEncoder[Int] =
numberStringEncoder.contramap(_.toString)

implicit val bigIntEncoder: ScynamoEncoder[BigInt] = numberStringEncoder.contramap[BigInt](_.toString)
implicit val longEncoder: ScynamoEncoder[Long] =
numberStringEncoder.contramap(_.toString)

implicit val floatEncoder: ScynamoEncoder[Float] = numberStringEncoder.contramap[Float](_.toString)
implicit val bigIntEncoder: ScynamoEncoder[BigInt] =
numberStringEncoder.contramap(_.toString)

implicit val doubleEncoder: ScynamoEncoder[Double] = numberStringEncoder.contramap[Double](_.toString)
implicit val floatEncoder: ScynamoEncoder[Float] =
numberStringEncoder.contramap(_.toString)

implicit val bigDecimalEncoder: ScynamoEncoder[BigDecimal] = numberStringEncoder.contramap[BigDecimal](_.toString)
implicit val doubleEncoder: ScynamoEncoder[Double] =
numberStringEncoder.contramap(_.toString)

implicit val booleanEncoder: ScynamoEncoder[Boolean] = value => Right(AttributeValue.builder().bool(value).build())
implicit val bigDecimalEncoder: ScynamoEncoder[BigDecimal] =
numberStringEncoder.contramap(_.toString)

implicit val instantEncoder: ScynamoEncoder[Instant] = numberStringEncoder.contramap[Instant](_.toEpochMilli.toString)
implicit val booleanEncoder: ScynamoEncoder[Boolean] =
ScynamoEncoder.instance(value => Right(AttributeValue.builder.bool(value).build()))

implicit val instantEncoder: ScynamoEncoder[Instant] =
numberStringEncoder.contramap(_.toEpochMilli.toString)

implicit val instantTtlEncoder: ScynamoEncoder[Instant @@ TimeToLive] =
numberStringEncoder.contramap[Instant @@ TimeToLive](_.getEpochSecond.toString)

implicit val uuidEncoder: ScynamoEncoder[UUID] = stringEncoder.contramap[UUID](_.toString)
implicit val uuidEncoder: ScynamoEncoder[UUID] =
stringEncoder.contramap(_.toString)

implicit def seqEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Seq[A]] =
ScynamoEncoder.instance { xs =>
var allErrors = Chain.empty[ScynamoEncodeError]
val attrValues = List.newBuilder[AttributeValue]
for ((x, i) <- xs.iterator.zipWithIndex) element.encode(x) match {
case Right(attr) => attrValues += attr
case Left(errors) => allErrors ++= StackFrame.encoding(errors, Index(i)).toChain
}

implicit def seqEncoder[A: ScynamoEncoder]: ScynamoEncoder[scala.collection.immutable.Seq[A]] =
value => value.toVector.parTraverse(ScynamoEncoder[A].encode).map(xs => AttributeValue.builder().l(xs: _*).build())
NonEmptyChain.fromChain(allErrors).toLeft(AttributeValue.builder.l(attrValues.result(): _*).build())
}

implicit def listEncoder[A: ScynamoEncoder]: ScynamoEncoder[List[A]] =
value =>
value.zipWithIndex
.parTraverse { case (x, i) =>
ScynamoEncoder[A].encode(x).leftMap(_.map(_.push(Index(i))))
}
.map(xs => AttributeValue.builder().l(xs: _*).build())
seqEncoder[A].narrow
joroKr21 marked this conversation as resolved.
Show resolved Hide resolved

implicit def vectorEncoder[A: ScynamoEncoder]: ScynamoEncoder[Vector[A]] =
value =>
value.zipWithIndex
.parTraverse { case (x, i) =>
ScynamoEncoder[A].encode(x).leftMap(_.map(_.push(Index(i))))
}
.map(xs => AttributeValue.builder().l(xs: _*).build())

implicit def setEncoder[A: ScynamoEncoder]: ScynamoEncoder[Set[A]] = listEncoder[A].contramap[Set[A]](x => x.toList)

implicit def optionEncoder[A: ScynamoEncoder]: ScynamoEncoder[Option[A]] = {
case Some(value) => ScynamoEncoder[A].encode(value)
case None => Right(AttributeValue.builder().nul(true).build())
}
seqEncoder[A].narrow

implicit def someEncoder[A: ScynamoEncoder]: ScynamoEncoder[Some[A]] = x => ScynamoEncoder[A].encode(x.get)
implicit def setEncoder[A: ScynamoEncoder]: ScynamoEncoder[Set[A]] =
listEncoder[A].contramap(_.toList)

implicit val finiteDurationEncoder: ScynamoEncoder[FiniteDuration] = longEncoder.contramap(_.toNanos)

implicit val durationEncoder: ScynamoEncoder[Duration] = longEncoder.contramap(_.toNanos)
implicit def optionEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Option[A]] =
ScynamoEncoder.instance {
case Some(value) => element.encode(value)
case None => rightNul
}

implicit def mapEncoder[A, B](implicit keyEncoder: ScynamoKeyEncoder[A], valueEncoder: ScynamoEncoder[B]): ScynamoEncoder[Map[A, B]] =
value => {
value.toVector
.parTraverse { case (k, v) =>
(keyEncoder.encode(k), valueEncoder.encode(v)).parMapN(_ -> _).leftMap(_.map(_.push(MapKey(k))))
}
.map {
_.foldLeft(new java.util.HashMap[String, AttributeValue]()) { case (acc, (k, v)) =>
acc.put(k, v)
acc
}
implicit def someEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Some[A]] =
ScynamoEncoder.instance(some => element.encode(some.get))

implicit val finiteDurationEncoder: ScynamoEncoder[FiniteDuration] =
markus1189 marked this conversation as resolved.
Show resolved Hide resolved
numberStringEncoder.contramap(_.toNanos.toString)

implicit val durationEncoder: ScynamoEncoder[Duration] =
markus1189 marked this conversation as resolved.
Show resolved Hide resolved
numberStringEncoder.contramap(_.toNanos.toString)

implicit def mapEncoder[A, B](implicit key: ScynamoKeyEncoder[A], value: ScynamoEncoder[B]): ScynamoEncoder[Map[A, B]] =
ScynamoEncoder.instance { kvs =>
var allErrors = Chain.empty[ScynamoEncodeError]
val attrValues = new java.util.HashMap[String, AttributeValue](kvs.size)
kvs.foreachEntry { (k, v) =>
(key.encode(k), value.encode(v)) match {
case (Right(k), Right(attr)) =>
// Omit `nul` for efficiency and GSI support (see https://github.com/aws/aws-sdk-go/issues/1803)
if (!attr.nul) attrValues.put(k, attr)
case (Left(errors), Right(_)) =>
allErrors ++= StackFrame.encoding(errors, MapKey(k)).toChain
case (Right(_), Left(errors)) =>
allErrors ++= StackFrame.encoding(errors, MapKey(k)).toChain
case (Left(kErrors), Left(vErrors)) =>
allErrors ++= StackFrame.encoding(kErrors ++ vErrors, MapKey(k)).toChain
}
.map(hm => AttributeValue.builder().m(hm).build())
}

NonEmptyChain.fromChain(allErrors).toLeft(AttributeValue.builder.m(attrValues).build())
}

implicit val attributeValueEncoder: ScynamoEncoder[AttributeValue] = { value =>
import scynamo.syntax.attributevalue._
implicit val attributeValueEncoder: ScynamoEncoder[AttributeValue] =
ScynamoEncoder.instance { value =>
import scynamo.syntax.attributevalue._

val nonEmptyStringSet = value.asOption(ScynamoType.StringSet).map(x => ScynamoType.StringSet -> (x.size() > 0))
val nonEmptyNumberSet = value.asOption(ScynamoType.NumberSet).map(x => ScynamoType.NumberSet -> (x.size() > 0))
val nonEmptyBinarySet = value.asOption(ScynamoType.BinarySet).map(x => ScynamoType.BinarySet -> (x.size() > 0))
def nonEmpty[A](typ: ScynamoType.Aux[java.util.List[A]] with ScynamoType.TypeInvalidIfEmpty) =
if (value.asOption(typ).exists(!_.isEmpty)) Right(value)
else Either.leftNec(ScynamoEncodeError.invalidEmptyValue(typ))

nonEmptyStringSet.orElse(nonEmptyNumberSet).orElse(nonEmptyBinarySet) match {
case Some((typ, false)) => Either.leftNec(ScynamoEncodeError.invalidEmptyValue(typ))
case Some((_, true)) | None => Right(value)
if (value.hasSs) nonEmpty(ScynamoType.StringSet)
else if (value.hasNs) nonEmpty(ScynamoType.NumberSet)
else if (value.hasBs) nonEmpty(ScynamoType.BinarySet)
else Right(value)
}
}

implicit def eitherScynamoErrorEncoder[A: ScynamoEncoder]: ScynamoEncoder[EitherNec[ScynamoEncodeError, A]] = {
case Left(value) => Left(value)
case Right(value) => ScynamoEncoder[A].encode(value)
}
implicit def eitherScynamoErrorEncoder[A](implicit right: ScynamoEncoder[A]): ScynamoEncoder[EitherNec[ScynamoEncodeError, A]] =
ScynamoEncoder.instance {
case Left(errors) => Left(errors)
case Right(value) => right.encode(value)
}

implicit def fieldEncoder[K, V](implicit V: Lazy[ScynamoEncoder[V]]): ScynamoEncoder[FieldType[K, V]] =
field => V.value.encode(field)
ScynamoEncoder.instance(V.value.encode)
}

trait ScynamoIterableEncoder extends LowestPrioAutoEncoder {
def iterableEncoder[A: ScynamoEncoder]: ScynamoEncoder[Iterable[A]] =
value =>
value.toList.parTraverse(ScynamoEncoder[A].encode).map(encodedValues => AttributeValue.builder().l(encodedValues.asJava).build())
ScynamoEncoder.listEncoder[A].contramap(_.toList)
}

trait LowestPrioAutoEncoder {
Expand All @@ -136,41 +168,60 @@ trait LowestPrioAutoEncoder {

trait ObjectScynamoEncoder[A] extends ScynamoEncoder[A] {
def encodeMap(value: A): EitherNec[ScynamoEncodeError, java.util.Map[String, AttributeValue]]

override def encode(value: A): EitherNec[ScynamoEncodeError, AttributeValue] =
encodeMap(value).map(AttributeValue.builder().m(_).build())
encodeMap(value).map(AttributeValue.builder.m(_).build())
}

object ObjectScynamoEncoder extends SemiautoDerivationEncoder {
def apply[A](implicit instance: ObjectScynamoEncoder[A]): ObjectScynamoEncoder[A] = instance

implicit def mapEncoder[A](implicit valueEncoder: ScynamoEncoder[A]): ObjectScynamoEncoder[Map[String, A]] =
value => {
value.toList
.parTraverse { case (k, v) => valueEncoder.encode(v).map(k -> _) }
.map {
_.foldLeft(new java.util.HashMap[String, AttributeValue]()) { case (acc, (k, v)) =>
acc.put(k, v)
acc
}
// SAM syntax generates anonymous classes because of non-abstract methods like `encode`.
private[scynamo] def instance[A](
f: A => EitherNec[ScynamoEncodeError, java.util.Map[String, AttributeValue]]
): ObjectScynamoEncoder[A] = f(_)

implicit val catsInstances: Contravariant[ObjectScynamoEncoder] = new Contravariant[ObjectScynamoEncoder] {
override def contramap[A, B](fa: ObjectScynamoEncoder[A])(f: B => A) =
instance(value => fa.encodeMap(f(value)))
}

implicit def mapEncoder[A](implicit value: ScynamoEncoder[A]): ObjectScynamoEncoder[Map[String, A]] =
instance { kvs =>
var allErrors = Chain.empty[ScynamoEncodeError]
val attrValues = new java.util.HashMap[String, AttributeValue](kvs.size)
kvs.foreachEntry { (k, v) =>
value.encode(v) match {
// Omit `nul` for efficiency and GSI support (see https://github.com/aws/aws-sdk-go/issues/1803)
case Right(attr) => if (!attr.nul) attrValues.put(k, attr)
case Left(errors) => allErrors ++= StackFrame.encoding(errors, MapKey(k)).toChain
}
}

NonEmptyChain.fromChain(allErrors).toLeft(Collections.unmodifiableMap(attrValues))
}
}

trait ScynamoKeyEncoder[A] { self =>
def encode(value: A): EitherNec[ScynamoEncodeError, String]

def contramap[B](f: B => A): ScynamoKeyEncoder[B] = value => self.encode(f(value))
def contramap[B](f: B => A): ScynamoKeyEncoder[B] =
ScynamoKeyEncoder.instance(value => self.encode(f(value)))
}

object ScynamoKeyEncoder {
def apply[A](implicit encoder: ScynamoKeyEncoder[A]): ScynamoKeyEncoder[A] = encoder

implicit val stringKeyEncoder: ScynamoKeyEncoder[String] = value =>
if (value.nonEmpty)
Right(value)
else
Either.leftNec(ScynamoEncodeError.invalidEmptyValue(ScynamoType.String))
// SAM syntax generates anonymous classes because of non-abstract methods like `contramap`.
private[scynamo] def instance[A](f: A => EitherNec[ScynamoEncodeError, String]): ScynamoKeyEncoder[A] = f(_)

implicit val catsInstances: Contravariant[ScynamoKeyEncoder] = new Contravariant[ScynamoKeyEncoder] {
override def contramap[A, B](fa: ScynamoKeyEncoder[A])(f: B => A) = fa.contramap(f)
}

implicit val stringKeyEncoder: ScynamoKeyEncoder[String] = instance { value =>
if (value.nonEmpty) Right(value)
else Either.leftNec(ScynamoEncodeError.invalidEmptyValue(ScynamoType.String))
}

implicit val uuidKeyEncoder: ScynamoKeyEncoder[UUID] = ScynamoKeyEncoder[String].contramap[UUID](_.toString)
implicit val uuidKeyEncoder: ScynamoKeyEncoder[UUID] =
ScynamoKeyEncoder[String].contramap(_.toString)
}
35 changes: 35 additions & 0 deletions src/main/scala/scynamo/ScynamoError.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package scynamo

import cats.data.{EitherNec, NonEmptyChain}
import cats.syntax.all._
import cats.{Eq, Show}
import scynamo.ScynamoType.TypeInvalidIfEmpty
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
Expand All @@ -17,6 +19,39 @@ object ScynamoError {
}
}

case class ErrorStack(frames: List[StackFrame]) {
def push(frame: StackFrame): ErrorStack = ErrorStack(frame +: frames)

override def toString: String =
frames.mkString("ErrorStack(", " -> ", ")")
}

object ErrorStack {
val empty: ErrorStack = ErrorStack(List.empty)
}

sealed trait StackFrame extends Product with Serializable
object StackFrame {
case class Attr(name: String) extends StackFrame
case class Case(name: String) extends StackFrame
case class Enum(name: String) extends StackFrame
case class Index(value: Int) extends StackFrame
case class MapKey[A](value: A) extends StackFrame
case class Custom(name: String) extends StackFrame

private[scynamo] def encoding[A](
encoded: EitherNec[ScynamoEncodeError, A],
frame: StackFrame
): EitherNec[ScynamoEncodeError, A] =
encoded.leftMap(encoding(_, frame))

private[scynamo] def encoding[A](
errors: NonEmptyChain[ScynamoEncodeError],
frame: StackFrame
): NonEmptyChain[ScynamoEncodeError] =
errors.map(_.push(frame))
}

sealed abstract class ScynamoEncodeError extends ScynamoError {
def push(frame: StackFrame): ScynamoEncodeError =
this match {
Expand Down
Loading