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 4 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
12 changes: 12 additions & 0 deletions src/main/scala/scynamo/ScynamoDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ object 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))
joroKr21 marked this conversation as resolved.
Show resolved Hide resolved
}

trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { self =>
Expand Down
219 changes: 134 additions & 85 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,150 @@ 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

// Ignore `nul` for efficiency and GSI support.
joroKr21 marked this conversation as resolved.
Show resolved Hide resolved
private[scynamo] def attributes(kvs: IndexedSeq[(String, AttributeValue)]) = {
joroKr21 marked this conversation as resolved.
Show resolved Hide resolved
val attr = new java.util.HashMap[String, AttributeValue](kvs.size)
for ((k, v) <- kvs) if (!v.nul) attr.put(k, v)
attr
}
}

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)
}

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

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

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

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

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

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

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

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

implicit val booleanEncoder: ScynamoEncoder[Boolean] = value => Right(AttributeValue.builder().bool(value).build())
implicit val booleanEncoder: ScynamoEncoder[Boolean] =
ScynamoEncoder.instance(value => Right(AttributeValue.builder.bool(value).build()))

implicit val instantEncoder: ScynamoEncoder[Instant] = numberStringEncoder.contramap[Instant](_.toEpochMilli.toString)
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: ScynamoEncoder]: ScynamoEncoder[scala.collection.immutable.Seq[A]] =
value => value.toVector.parTraverse(ScynamoEncoder[A].encode).map(xs => AttributeValue.builder().l(xs: _*).build())
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
}

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())
seqEncoder[A].narrow

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

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

implicit def someEncoder[A: ScynamoEncoder]: ScynamoEncoder[Some[A]] = x => ScynamoEncoder[A].encode(x.get)
implicit def someEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Some[A]] =
ScynamoEncoder.instance(some => element.encode(some.get))

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

implicit val durationEncoder: ScynamoEncoder[Duration] = longEncoder.contramap(_.toNanos)
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 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 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)).parTupled match {
joroKr21 marked this conversation as resolved.
Show resolved Hide resolved
case Right((k, attr)) => if (!attr.nul) attrValues.put(k, attr)
case Left(errors) => allErrors ++= StackFrame.encoding(errors, 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._

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 +167,59 @@ 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 {
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package scynamo.generic
import scynamo.ObjectScynamoEncoder
import shapeless.{LabelledGeneric, Lazy}

import java.util.Collections

trait GenericScynamoEncoder[A] extends ObjectScynamoEncoder[A]

object GenericScynamoEncoder extends GenericScynamoEncoderInstances
Expand All @@ -12,5 +14,5 @@ trait GenericScynamoEncoderInstances {
gen: LabelledGeneric.Aux[F, G],
sg: Lazy[ShapelessScynamoEncoder[F, G]]
): GenericScynamoEncoder[F] =
value => sg.value.encodeMap(gen.to(value))
value => sg.value.encodeMap(gen.to(value)).map(Collections.unmodifiableMap(_))
}
Loading