Skip to content

Commit 08a23e5

Browse files
author
Georgi Krastev
authored
Optimize encoders and skip nul fields (#247)
1 parent 7d41643 commit 08a23e5

File tree

10 files changed

+306
-153
lines changed

10 files changed

+306
-153
lines changed

build.sbt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ lazy val scalacOptions_2_13 = Seq(
7575
"-Xfatal-warnings",
7676
"-Ywarn-dead-code",
7777
"-Ymacro-annotations",
78-
"-Xlint:_,-byname-implicit",
78+
"-Xlint:_,-byname-implicit,-unused",
79+
"-Wunused:_,-imports,-synthetics",
7980
"-Xsource:3"
8081
)
8182

src/main/scala/scynamo/ScynamoDecoder.scala

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,6 @@ import scala.concurrent.duration.{Duration, FiniteDuration}
2121
import scala.jdk.CollectionConverters._
2222
import scala.util.control.NonFatal
2323

24-
case class ErrorStack(frames: List[StackFrame]) {
25-
def push(frame: StackFrame): ErrorStack = ErrorStack(frame +: frames)
26-
27-
override def toString: String =
28-
frames.mkString("ErrorStack(", " -> ", ")")
29-
}
30-
31-
object ErrorStack {
32-
val empty: ErrorStack = ErrorStack(List.empty)
33-
}
34-
35-
sealed trait StackFrame extends Product with Serializable
36-
object StackFrame {
37-
case class Attr(name: String) extends StackFrame
38-
case class Case(name: String) extends StackFrame
39-
case class Enum(name: String) extends StackFrame
40-
case class Index(value: Int) extends StackFrame
41-
case class MapKey[A](value: A) extends StackFrame
42-
case class Custom(name: String) extends StackFrame
43-
}
44-
4524
trait ScynamoDecoder[A] extends ScynamoDecoderFunctions { self =>
4625
def decode(attributeValue: AttributeValue): EitherNec[ScynamoDecodeError, A]
4726

Lines changed: 141 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package scynamo
22

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

1414
import java.time.Instant
15-
import java.util.UUID
15+
import java.util.{Collections, UUID}
16+
import scala.collection.compat._
17+
import scala.collection.immutable.Seq
1618
import scala.concurrent.duration.{Duration, FiniteDuration}
17-
import scala.jdk.CollectionConverters._
1819

1920
trait ScynamoEncoder[A] { self =>
2021
def encode(value: A): EitherNec[ScynamoEncodeError, AttributeValue]
21-
22-
def contramap[B](f: B => A): ScynamoEncoder[B] = value => self.encode(f(value))
22+
def contramap[B](f: B => A): ScynamoEncoder[B] =
23+
ScynamoEncoder.instance(value => self.encode(f(value)))
2324
}
2425

2526
object ScynamoEncoder extends DefaultScynamoEncoderInstances {
2627
def apply[A](implicit instance: ScynamoEncoder[A]): ScynamoEncoder[A] = instance
28+
29+
// SAM syntax generates anonymous classes because of non-abstract methods like `contramap`.
30+
private[scynamo] def instance[A](f: A => EitherNec[ScynamoEncodeError, AttributeValue]): ScynamoEncoder[A] = f(_)
2731
}
2832

2933
trait DefaultScynamoEncoderInstances extends ScynamoIterableEncoder {
30-
implicit val stringEncoder: ScynamoEncoder[String] = value => Right(AttributeValue.builder().s(value).build())
34+
private val rightNul = Right(AttributeValue.builder.nul(true).build())
35+
36+
implicit val catsInstances: Contravariant[ScynamoEncoder] = new Contravariant[ScynamoEncoder] {
37+
override def contramap[A, B](fa: ScynamoEncoder[A])(f: B => A) = fa.contramap(f)
38+
}
3139

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

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

36-
implicit val longEncoder: ScynamoEncoder[Long] = numberStringEncoder.contramap[Long](_.toString)
46+
implicit val intEncoder: ScynamoEncoder[Int] =
47+
numberStringEncoder.contramap(_.toString)
3748

38-
implicit val bigIntEncoder: ScynamoEncoder[BigInt] = numberStringEncoder.contramap[BigInt](_.toString)
49+
implicit val longEncoder: ScynamoEncoder[Long] =
50+
numberStringEncoder.contramap(_.toString)
3951

40-
implicit val floatEncoder: ScynamoEncoder[Float] = numberStringEncoder.contramap[Float](_.toString)
52+
implicit val bigIntEncoder: ScynamoEncoder[BigInt] =
53+
numberStringEncoder.contramap(_.toString)
4154

42-
implicit val doubleEncoder: ScynamoEncoder[Double] = numberStringEncoder.contramap[Double](_.toString)
55+
implicit val floatEncoder: ScynamoEncoder[Float] =
56+
numberStringEncoder.contramap(_.toString)
4357

44-
implicit val bigDecimalEncoder: ScynamoEncoder[BigDecimal] = numberStringEncoder.contramap[BigDecimal](_.toString)
58+
implicit val doubleEncoder: ScynamoEncoder[Double] =
59+
numberStringEncoder.contramap(_.toString)
4560

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

48-
implicit val instantEncoder: ScynamoEncoder[Instant] = numberStringEncoder.contramap[Instant](_.toEpochMilli.toString)
64+
implicit val booleanEncoder: ScynamoEncoder[Boolean] =
65+
ScynamoEncoder.instance(value => Right(AttributeValue.builder.bool(value).build()))
66+
67+
implicit val instantEncoder: ScynamoEncoder[Instant] =
68+
numberStringEncoder.contramap(_.toEpochMilli.toString)
4969

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

53-
implicit val uuidEncoder: ScynamoEncoder[UUID] = stringEncoder.contramap[UUID](_.toString)
73+
implicit val uuidEncoder: ScynamoEncoder[UUID] =
74+
stringEncoder.contramap(_.toString)
75+
76+
implicit def seqEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Seq[A]] =
77+
ScynamoEncoder.instance { xs =>
78+
var allErrors = Chain.empty[ScynamoEncodeError]
79+
val attrValues = List.newBuilder[AttributeValue]
80+
for ((x, i) <- xs.iterator.zipWithIndex) element.encode(x) match {
81+
case Right(attr) => attrValues += attr
82+
case Left(errors) => allErrors ++= StackFrame.encoding(errors, Index(i)).toChain
83+
}
5484

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

5888
implicit def listEncoder[A: ScynamoEncoder]: ScynamoEncoder[List[A]] =
59-
value =>
60-
value.zipWithIndex
61-
.parTraverse { case (x, i) =>
62-
ScynamoEncoder[A].encode(x).leftMap(_.map(_.push(Index(i))))
63-
}
64-
.map(xs => AttributeValue.builder().l(xs: _*).build())
89+
seqEncoder[A].narrow
6590

6691
implicit def vectorEncoder[A: ScynamoEncoder]: ScynamoEncoder[Vector[A]] =
67-
value =>
68-
value.zipWithIndex
69-
.parTraverse { case (x, i) =>
70-
ScynamoEncoder[A].encode(x).leftMap(_.map(_.push(Index(i))))
71-
}
72-
.map(xs => AttributeValue.builder().l(xs: _*).build())
73-
74-
implicit def setEncoder[A: ScynamoEncoder]: ScynamoEncoder[Set[A]] = listEncoder[A].contramap[Set[A]](x => x.toList)
75-
76-
implicit def optionEncoder[A: ScynamoEncoder]: ScynamoEncoder[Option[A]] = {
77-
case Some(value) => ScynamoEncoder[A].encode(value)
78-
case None => Right(AttributeValue.builder().nul(true).build())
79-
}
92+
seqEncoder[A].narrow
8093

81-
implicit def someEncoder[A: ScynamoEncoder]: ScynamoEncoder[Some[A]] = x => ScynamoEncoder[A].encode(x.get)
94+
implicit def setEncoder[A: ScynamoEncoder]: ScynamoEncoder[Set[A]] =
95+
listEncoder[A].contramap(_.toList)
8296

83-
implicit val finiteDurationEncoder: ScynamoEncoder[FiniteDuration] = longEncoder.contramap(_.toNanos)
84-
85-
implicit val durationEncoder: ScynamoEncoder[Duration] = longEncoder.contramap(_.toNanos)
97+
implicit def optionEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Option[A]] =
98+
ScynamoEncoder.instance {
99+
case Some(value) => element.encode(value)
100+
case None => rightNul
101+
}
86102

87-
implicit def mapEncoder[A, B](implicit keyEncoder: ScynamoKeyEncoder[A], valueEncoder: ScynamoEncoder[B]): ScynamoEncoder[Map[A, B]] =
88-
value => {
89-
value.toVector
90-
.parTraverse { case (k, v) =>
91-
(keyEncoder.encode(k), valueEncoder.encode(v)).parMapN(_ -> _).leftMap(_.map(_.push(MapKey(k))))
92-
}
93-
.map {
94-
_.foldLeft(new java.util.HashMap[String, AttributeValue]()) { case (acc, (k, v)) =>
95-
acc.put(k, v)
96-
acc
97-
}
103+
implicit def someEncoder[A](implicit element: ScynamoEncoder[A]): ScynamoEncoder[Some[A]] =
104+
ScynamoEncoder.instance(some => element.encode(some.get))
105+
106+
implicit val finiteDurationEncoder: ScynamoEncoder[FiniteDuration] =
107+
numberStringEncoder.contramap(_.toNanos.toString)
108+
109+
implicit val durationEncoder: ScynamoEncoder[Duration] =
110+
numberStringEncoder.contramap(_.toNanos.toString)
111+
112+
implicit def mapEncoder[A, B](implicit key: ScynamoKeyEncoder[A], value: ScynamoEncoder[B]): ScynamoEncoder[Map[A, B]] =
113+
ScynamoEncoder.instance { kvs =>
114+
var allErrors = Chain.empty[ScynamoEncodeError]
115+
val attrValues = new java.util.HashMap[String, AttributeValue](kvs.size)
116+
kvs.foreachEntry { (k, v) =>
117+
(key.encode(k), value.encode(v)) match {
118+
case (Right(k), Right(attr)) =>
119+
// Omit `nul` for efficiency and GSI support (see https://github.com/aws/aws-sdk-go/issues/1803)
120+
if (!attr.nul) attrValues.put(k, attr)
121+
case (Left(errors), Right(_)) =>
122+
allErrors ++= StackFrame.encoding(errors, MapKey(k)).toChain
123+
case (Right(_), Left(errors)) =>
124+
allErrors ++= StackFrame.encoding(errors, MapKey(k)).toChain
125+
case (Left(kErrors), Left(vErrors)) =>
126+
allErrors ++= StackFrame.encoding(kErrors ++ vErrors, MapKey(k)).toChain
98127
}
99-
.map(hm => AttributeValue.builder().m(hm).build())
128+
}
129+
130+
NonEmptyChain.fromChain(allErrors).toLeft(AttributeValue.builder.m(attrValues).build())
100131
}
101132

102-
implicit val attributeValueEncoder: ScynamoEncoder[AttributeValue] = { value =>
103-
import scynamo.syntax.attributevalue._
133+
implicit val attributeValueEncoder: ScynamoEncoder[AttributeValue] =
134+
ScynamoEncoder.instance { value =>
135+
import scynamo.syntax.attributevalue._
104136

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

109-
nonEmptyStringSet.orElse(nonEmptyNumberSet).orElse(nonEmptyBinarySet) match {
110-
case Some((typ, false)) => Either.leftNec(ScynamoEncodeError.invalidEmptyValue(typ))
111-
case Some((_, true)) | None => Right(value)
141+
if (value.hasSs) nonEmpty(ScynamoType.StringSet)
142+
else if (value.hasNs) nonEmpty(ScynamoType.NumberSet)
143+
else if (value.hasBs) nonEmpty(ScynamoType.BinarySet)
144+
else Right(value)
112145
}
113-
}
114146

115-
implicit def eitherScynamoErrorEncoder[A: ScynamoEncoder]: ScynamoEncoder[EitherNec[ScynamoEncodeError, A]] = {
116-
case Left(value) => Left(value)
117-
case Right(value) => ScynamoEncoder[A].encode(value)
118-
}
147+
implicit def eitherScynamoErrorEncoder[A](implicit right: ScynamoEncoder[A]): ScynamoEncoder[EitherNec[ScynamoEncodeError, A]] =
148+
ScynamoEncoder.instance {
149+
case Left(errors) => Left(errors)
150+
case Right(value) => right.encode(value)
151+
}
119152

120153
implicit def fieldEncoder[K, V](implicit V: Lazy[ScynamoEncoder[V]]): ScynamoEncoder[FieldType[K, V]] =
121-
field => V.value.encode(field)
154+
ScynamoEncoder.instance(V.value.encode)
122155
}
123156

124157
trait ScynamoIterableEncoder extends LowestPrioAutoEncoder {
125158
def iterableEncoder[A: ScynamoEncoder]: ScynamoEncoder[Iterable[A]] =
126-
value =>
127-
value.toList.parTraverse(ScynamoEncoder[A].encode).map(encodedValues => AttributeValue.builder().l(encodedValues.asJava).build())
159+
ScynamoEncoder.listEncoder[A].contramap(_.toList)
128160
}
129161

130162
trait LowestPrioAutoEncoder {
@@ -136,41 +168,60 @@ trait LowestPrioAutoEncoder {
136168

137169
trait ObjectScynamoEncoder[A] extends ScynamoEncoder[A] {
138170
def encodeMap(value: A): EitherNec[ScynamoEncodeError, java.util.Map[String, AttributeValue]]
139-
140171
override def encode(value: A): EitherNec[ScynamoEncodeError, AttributeValue] =
141-
encodeMap(value).map(AttributeValue.builder().m(_).build())
172+
encodeMap(value).map(AttributeValue.builder.m(_).build())
142173
}
143174

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

147-
implicit def mapEncoder[A](implicit valueEncoder: ScynamoEncoder[A]): ObjectScynamoEncoder[Map[String, A]] =
148-
value => {
149-
value.toList
150-
.parTraverse { case (k, v) => valueEncoder.encode(v).map(k -> _) }
151-
.map {
152-
_.foldLeft(new java.util.HashMap[String, AttributeValue]()) { case (acc, (k, v)) =>
153-
acc.put(k, v)
154-
acc
155-
}
178+
// SAM syntax generates anonymous classes because of non-abstract methods like `encode`.
179+
private[scynamo] def instance[A](
180+
f: A => EitherNec[ScynamoEncodeError, java.util.Map[String, AttributeValue]]
181+
): ObjectScynamoEncoder[A] = f(_)
182+
183+
implicit val catsInstances: Contravariant[ObjectScynamoEncoder] = new Contravariant[ObjectScynamoEncoder] {
184+
override def contramap[A, B](fa: ObjectScynamoEncoder[A])(f: B => A) =
185+
instance(value => fa.encodeMap(f(value)))
186+
}
187+
188+
implicit def mapEncoder[A](implicit value: ScynamoEncoder[A]): ObjectScynamoEncoder[Map[String, A]] =
189+
instance { kvs =>
190+
var allErrors = Chain.empty[ScynamoEncodeError]
191+
val attrValues = new java.util.HashMap[String, AttributeValue](kvs.size)
192+
kvs.foreachEntry { (k, v) =>
193+
value.encode(v) match {
194+
// Omit `nul` for efficiency and GSI support (see https://github.com/aws/aws-sdk-go/issues/1803)
195+
case Right(attr) => if (!attr.nul) attrValues.put(k, attr)
196+
case Left(errors) => allErrors ++= StackFrame.encoding(errors, MapKey(k)).toChain
156197
}
198+
}
199+
200+
NonEmptyChain.fromChain(allErrors).toLeft(Collections.unmodifiableMap(attrValues))
157201
}
158202
}
159203

160204
trait ScynamoKeyEncoder[A] { self =>
161205
def encode(value: A): EitherNec[ScynamoEncodeError, String]
162-
163-
def contramap[B](f: B => A): ScynamoKeyEncoder[B] = value => self.encode(f(value))
206+
def contramap[B](f: B => A): ScynamoKeyEncoder[B] =
207+
ScynamoKeyEncoder.instance(value => self.encode(f(value)))
164208
}
165209

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

169-
implicit val stringKeyEncoder: ScynamoKeyEncoder[String] = value =>
170-
if (value.nonEmpty)
171-
Right(value)
172-
else
173-
Either.leftNec(ScynamoEncodeError.invalidEmptyValue(ScynamoType.String))
213+
// SAM syntax generates anonymous classes because of non-abstract methods like `contramap`.
214+
private[scynamo] def instance[A](f: A => EitherNec[ScynamoEncodeError, String]): ScynamoKeyEncoder[A] = f(_)
215+
216+
implicit val catsInstances: Contravariant[ScynamoKeyEncoder] = new Contravariant[ScynamoKeyEncoder] {
217+
override def contramap[A, B](fa: ScynamoKeyEncoder[A])(f: B => A) = fa.contramap(f)
218+
}
219+
220+
implicit val stringKeyEncoder: ScynamoKeyEncoder[String] = instance { value =>
221+
if (value.nonEmpty) Right(value)
222+
else Either.leftNec(ScynamoEncodeError.invalidEmptyValue(ScynamoType.String))
223+
}
174224

175-
implicit val uuidKeyEncoder: ScynamoKeyEncoder[UUID] = ScynamoKeyEncoder[String].contramap[UUID](_.toString)
225+
implicit val uuidKeyEncoder: ScynamoKeyEncoder[UUID] =
226+
ScynamoKeyEncoder[String].contramap(_.toString)
176227
}

src/main/scala/scynamo/ScynamoError.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scynamo
22

3+
import cats.data.{EitherNec, NonEmptyChain}
4+
import cats.syntax.all._
35
import cats.{Eq, Show}
46
import scynamo.ScynamoType.TypeInvalidIfEmpty
57
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
@@ -17,6 +19,39 @@ object ScynamoError {
1719
}
1820
}
1921

22+
case class ErrorStack(frames: List[StackFrame]) {
23+
def push(frame: StackFrame): ErrorStack = ErrorStack(frame +: frames)
24+
25+
override def toString: String =
26+
frames.mkString("ErrorStack(", " -> ", ")")
27+
}
28+
29+
object ErrorStack {
30+
val empty: ErrorStack = ErrorStack(List.empty)
31+
}
32+
33+
sealed trait StackFrame extends Product with Serializable
34+
object StackFrame {
35+
case class Attr(name: String) extends StackFrame
36+
case class Case(name: String) extends StackFrame
37+
case class Enum(name: String) extends StackFrame
38+
case class Index(value: Int) extends StackFrame
39+
case class MapKey[A](value: A) extends StackFrame
40+
case class Custom(name: String) extends StackFrame
41+
42+
private[scynamo] def encoding[A](
43+
encoded: EitherNec[ScynamoEncodeError, A],
44+
frame: StackFrame
45+
): EitherNec[ScynamoEncodeError, A] =
46+
encoded.leftMap(encoding(_, frame))
47+
48+
private[scynamo] def encoding[A](
49+
errors: NonEmptyChain[ScynamoEncodeError],
50+
frame: StackFrame
51+
): NonEmptyChain[ScynamoEncodeError] =
52+
errors.map(_.push(frame))
53+
}
54+
2055
sealed abstract class ScynamoEncodeError extends ScynamoError {
2156
def push(frame: StackFrame): ScynamoEncodeError =
2257
this match {

0 commit comments

Comments
 (0)