Skip to content

Commit

Permalink
Introduce error hierarchy instead of String errors in DynamoDBQuery (#…
Browse files Browse the repository at this point in the history
…154)

* Introduce error hierarchy instead of String errors in DynamoDBQuery

* Fix formatting

---------

Co-authored-by: imorozov <[email protected]>
  • Loading branch information
morozovivan95 and iamorozov authored Feb 4, 2023
1 parent 219d517 commit 01afd2e
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 96 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ lazy val zioDynamodb = module("zio-dynamodb", "dynamodb")
val gets = (1 to i).map(p => s"${lowerAlpha(p)} <- get[${upperAlpha(p)}](field$p)").mkString("\n ")
s"""def as[$tparams, $returnType](
| $params
| )(fn: ($ftypes) => $returnType): Either[String, $returnType] =
| )(fn: ($ftypes) => $returnType): Either[DynamoDBError, $returnType] =
| for {
| $gets
| } yield fn($fparams)""".stripMargin
Expand Down Expand Up @@ -233,7 +233,7 @@ lazy val zioDynamodb = module("zio-dynamodb", "dynamodb")
val gets = (1 to i).map(p => s"${lowerAlpha(p)} <- get[${upperAlpha(p)}](field$p)").mkString("\n ")
s"""def as[$tparams, $returnType](
| $params
| )(fn: ($ftypes) => $returnType): Either[String, $returnType] =
| )(fn: ($ftypes) => $returnType): Either[DynamoDBError, $returnType] =
| for {
| $gets
| } yield fn($fparams)""".stripMargin
Expand Down
22 changes: 13 additions & 9 deletions dynamodb/src/main/scala/zio/dynamodb/AttrMap.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package zio.dynamodb

import zio.dynamodb.DynamoDBError.DecodingError

final case class AttrMap(map: Map[String, AttributeValue]) extends GeneratedFromAttributeValueAs { self =>

def get[A](field: String)(implicit ev: FromAttributeValue[A]): Either[String, A] =
def get[A](field: String)(implicit ev: FromAttributeValue[A]): Either[DynamoDBError, A] =
map
.get(field)
.toRight(s"field '$field' not found")
.toRight(DecodingError(s"field '$field' not found"))
.flatMap(ev.fromAttributeValue)

def getOptional[A](field: String)(implicit ev: FromAttributeValue[A]): Either[Nothing, Option[A]] =
Expand All @@ -14,24 +16,26 @@ final case class AttrMap(map: Map[String, AttributeValue]) extends GeneratedFrom
case _ => Right(None)
}

def getItem[A](field: String)(f: AttrMap => Either[String, A]): Either[String, A] =
def getItem[A](field: String)(f: AttrMap => Either[DynamoDBError, A]): Either[DynamoDBError, A] =
get[Item](field).flatMap(item => f(item))

// convenience method so that user does not have to transform between an Option and an Either
def getOptionalItem[A](
field: String
)(f: AttrMap => Either[String, A]): Either[String, Option[A]] =
getOptional[Item](field).flatMap(_.fold[Either[String, Option[A]]](Right(None))(item => f(item).map(Some(_))))
)(f: AttrMap => Either[DynamoDBError, A]): Either[DynamoDBError, Option[A]] =
getOptional[Item](field).flatMap(
_.fold[Either[DynamoDBError, Option[A]]](Right(None))(item => f(item).map(Some(_)))
)

// convenience method so that user does not have to transform between a List and an Either
def getIterableItem[A](field: String)(f: AttrMap => Either[String, A]): Either[String, Iterable[A]] =
get[Iterable[Item]](field).flatMap[String, Iterable[A]](xs => EitherUtil.forEach(xs)(f))
def getIterableItem[A](field: String)(f: AttrMap => Either[DynamoDBError, A]): Either[DynamoDBError, Iterable[A]] =
get[Iterable[Item]](field).flatMap[DynamoDBError, Iterable[A]](xs => EitherUtil.forEach(xs)(f))

// convenience method so that user does not have to transform between an Option, List and an Either
def getOptionalIterableItem[A](
field: String
)(f: AttrMap => Either[String, A]): Either[String, Option[Iterable[A]]] = {
def maybeTransform(maybeItems: Option[Iterable[Item]]): Either[String, Option[Iterable[A]]] =
)(f: AttrMap => Either[DynamoDBError, A]): Either[DynamoDBError, Option[Iterable[A]]] = {
def maybeTransform(maybeItems: Option[Iterable[Item]]): Either[DynamoDBError, Option[Iterable[A]]] =
maybeItems match {
case None => Right(None)
case Some(xs) => EitherUtil.forEach(xs)(f).map(Some(_))
Expand Down
2 changes: 1 addition & 1 deletion dynamodb/src/main/scala/zio/dynamodb/AttributeValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import zio.schema.Schema
sealed trait AttributeValue { self =>
type ScalaType

def decode[A](implicit schema: Schema[A]): Either[String, A] = Codec.decoder(schema)(self)
def decode[A](implicit schema: Schema[A]): Either[DynamoDBError, A] = Codec.decoder(schema)(self)

def ===[From](that: Operand.Size[From, ScalaType]): ConditionExpression[From] = Equals(ValueOperand(self), that)
def <>[From](that: Operand.Size[From, ScalaType]): ConditionExpression[From] = NotEqual(ValueOperand(self), that)
Expand Down
81 changes: 44 additions & 37 deletions dynamodb/src/main/scala/zio/dynamodb/Codec.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package zio.dynamodb

import zio.dynamodb.Annotations.{ discriminator, enumOfCaseObjects, id, maybeDiscriminator, maybeId }
import zio.dynamodb.DynamoDBError.DecodingError
import zio.schema.Schema.{ Optional, Primitive }
import zio.schema.{ FieldSet, Schema, StandardType }
import zio.{ schema, Chunk }

import java.math.BigInteger
import java.time._
import java.time.format.{ DateTimeFormatterBuilder, SignStyle }
import java.time.temporal.ChronoField.YEAR
Expand Down Expand Up @@ -411,7 +413,7 @@ private[dynamodb] object Codec {
private[dynamodb] def decoder[A](schema: Schema[A]): Decoder[A] =
schema match {
case s: Optional[a] => optionalDecoder[a](decoder(s.schema))
case Schema.Fail(s, _) => _ => Left(s)
case Schema.Fail(s, _) => _ => Left(DecodingError(s))
case Schema.GenericRecord(_, structure, _) => genericRecordDecoder(structure).asInstanceOf[Decoder[A]]
case Schema.Tuple2(l, r, _) => tupleDecoder(decoder(l), decoder(r))
case Schema.Transform(codec, f, _, _, _) => transformDecoder(codec, f)
Expand Down Expand Up @@ -528,7 +530,7 @@ private[dynamodb] object Codec {
av match {
case AttributeValue.Map(map) =>
EitherUtil
.forEach[schema.Schema.Field[_, _], (String, Any)](structure.toChunk) {
.forEach[schema.Schema.Field[_, _], (String, Any), DynamoDBError](structure.toChunk) {
case Schema.Field(key, schema: Schema[a], _, _, _, _) =>
val av = map(AttributeValue.String(key))
val dec = decoder(schema)
Expand All @@ -538,7 +540,7 @@ private[dynamodb] object Codec {
}
}
.map(ls => ListMap.newBuilder.++=(ls).result())
case av => Left(s"Expected AttributeValue.Map but found $av")
case av => Left(DecodingError(s"Expected AttributeValue.Map but found $av"))
}

private def primitiveDecoder[A](standardType: StandardType[A]): Decoder[A] =
Expand Down Expand Up @@ -588,7 +590,7 @@ private[dynamodb] object Codec {
case StandardType.UUIDType =>
(av: AttributeValue) =>
FromAttributeValue.stringFromAttributeValue.fromAttributeValue(av).flatMap { s =>
Try(UUID.fromString(s)).toEither.left.map(iae => s"Invalid UUID: ${iae.getMessage}")
Try(UUID.fromString(s)).toEither.left.map(iae => DecodingError(s"Invalid UUID: ${iae.getMessage}"))
}
case StandardType.DayOfWeekType =>
(av: AttributeValue) => javaTimeStringParser(av)(DayOfWeek.valueOf(_))
Expand Down Expand Up @@ -624,16 +626,16 @@ private[dynamodb] object Codec {
(av: AttributeValue) => javaTimeStringParser(av)(ZoneOffset.of(_))
}

private def javaTimeStringParser[A](av: AttributeValue)(unsafeParse: String => A): Either[String, A] =
private def javaTimeStringParser[A](av: AttributeValue)(unsafeParse: String => A): Either[DynamoDBError, A] =
FromAttributeValue.stringFromAttributeValue.fromAttributeValue(av).flatMap { s =>
val stringOrA = Try(unsafeParse(s)).toEither.left
.map(e => s"error parsing string '$s': ${e.getMessage}")
.map(e => DecodingError(s"error parsing string '$s': ${e.getMessage}"))
stringOrA
}

private def transformDecoder[A, B](codec: Schema[A], f: A => Either[String, B]): Decoder[B] = {
val dec = decoder(codec)
(a: AttributeValue) => dec(a).flatMap(f)
(a: AttributeValue) => dec(a).flatMap(f(_).left.map(DecodingError))
}

private def optionalDecoder[A](decoder: Decoder[A]): Decoder[Option[A]] = {
Expand All @@ -649,9 +651,9 @@ private[dynamodb] object Codec {
case (AttributeValue.String("Right"), b) :: Nil =>
decR(b).map(Right(_))
case av =>
Left(s"AttributeValue.Map map element $av not expected.")
Left(DecodingError(s"AttributeValue.Map map element $av not expected."))
}
case av => Left(s"Expected AttributeValue.Map but found $av")
case av => Left(DecodingError(s"Expected AttributeValue.Map but found $av"))
}

private def tupleDecoder[A, B](decL: Decoder[A], decR: Decoder[B]): Decoder[(A, B)] =
Expand All @@ -665,36 +667,36 @@ private[dynamodb] object Codec {
b <- decR(avB)
} yield (a, b)
case av =>
Left(s"Expected an AttributeValue.List of two elements but found $av")
Left(DecodingError(s"Expected an AttributeValue.List of two elements but found $av"))
}

private def sequenceDecoder[Col, A](decoder: Decoder[A], to: Chunk[A] => Col): Decoder[Col] = {
case AttributeValue.List(list) =>
EitherUtil.forEach(list)(decoder(_)).map(xs => to(Chunk.fromIterable(xs)))
case av => Left(s"unable to decode $av as a list")
case av => Left(DecodingError(s"unable to decode $av as a list"))
}

private def setDecoder[A](s: Schema[A]): Decoder[Set[A]] = {
def nativeStringSetDecoder[A]: Decoder[Set[A]] = {
case AttributeValue.StringSet(stringSet) =>
Right(stringSet.asInstanceOf[Set[A]])
case av =>
Left(s"Error: expected a string set but found '$av'")
Left(DecodingError(s"Error: expected a string set but found '$av'"))
}

def nativeNumberSetDecoder[A](f: BigDecimal => A): Decoder[Set[A]] = {
case AttributeValue.NumberSet(numberSet) =>
Right(numberSet.map(f))
case av =>
Left(s"Error: expected a number set but found '$av'")
Left(DecodingError(s"Error: expected a number set but found '$av'"))
}

def nativeBinarySetDecoder[A]: Decoder[Set[A]] = {
case AttributeValue.BinarySet(setOfChunkOfByte) =>
val set: Set[Chunk[Byte]] = setOfChunkOfByte.toSet.map((xs: Iterable[Byte]) => Chunk.fromIterable(xs))
Right(set.asInstanceOf[Set[A]])
case av =>
Left(s"Error: expected a Set of Chunk of Byte but found '$av'")
Left(DecodingError(s"Error: expected a Set of Chunk of Byte but found '$av'"))
}

s match {
Expand All @@ -719,7 +721,7 @@ private[dynamodb] object Codec {
if bigDecimal.isInstanceOf[StandardType.BigDecimalType.type] =>
nativeNumberSetDecoder[BigDecimal](_.bigDecimal).asInstanceOf[Decoder[Set[A]]]
case Schema.Primitive(StandardType.BigIntegerType, _) =>
nativeNumberSetDecoder(bd => bd.toBigInt.bigInteger)
nativeNumberSetDecoder[BigInteger](bd => bd.toBigInt.bigInteger)
case Schema.Transform(Schema.Primitive(bigInt, _), _, _, _, _)
if bigInt.isInstanceOf[StandardType.BigIntegerType.type] =>
nativeNumberSetDecoder[BigInt](_.toBigInt).asInstanceOf[Decoder[Set[A]]]
Expand All @@ -741,7 +743,7 @@ private[dynamodb] object Codec {
decA(av)
}
errorOrList.map(_.toSet)
case av => Left(s"Error: expected AttributeValue.List but found $av")
case av => Left(DecodingError(s"Error: expected AttributeValue.List but found $av"))
}
}

Expand All @@ -757,15 +759,15 @@ private[dynamodb] object Codec {
(av: AttributeValue) => {
av match {
case AttributeValue.Map(map) =>
val xs: Iterable[Either[String, (String, V)]] = map.map {
val xs: Iterable[Either[DynamoDBError, (String, V)]] = map.map {
case (k, v) =>
dec(v) match {
case Right(decV) => Right((k.value, decV))
case Left(s) => Left(s)
}
}
EitherUtil.collectAll(xs).map(_.toMap)
case av => Left(s"Error: expected AttributeValue.Map but found $av")
case av => Left(DecodingError(s"Error: expected AttributeValue.Map but found $av"))
}
}

Expand All @@ -777,10 +779,10 @@ private[dynamodb] object Codec {
case avList @ AttributeValue.List(_) =>
tupleDecoder(decA, decB)(avList)
case av =>
Left(s"Error: expected AttributeValue.List but found $av")
Left(DecodingError(s"Error: expected AttributeValue.List but found $av"))
}
errorOrListOfTuple.map(_.toMap)
case av => Left(s"Error: expected AttributeValue.List but found $av")
case av => Left(DecodingError(s"Error: expected AttributeValue.List but found $av"))
}
}

Expand All @@ -796,32 +798,32 @@ private[dynamodb] object Codec {
case AttributeValue.Map(map) =>
// default enum encoding uses a Map with a single entry that denotes the type
// TODO: think about being stricter and rejecting Maps with > 1 entry ???
map.toList.headOption.fold[Either[String, Z]](Left(s"map $av is empty")) {
map.toList.headOption.fold[Either[DynamoDBError, Z]](Left(DecodingError(s"map $av is empty"))) {
case (AttributeValue.String(subtype), av) =>
cases.find { c =>
maybeId(c.annotations).fold(c.id == subtype)(_ == subtype)
} match {
case Some(c) =>
decoder(c.schema)(av).map(_.asInstanceOf[Z])
case None =>
Left(s"subtype $subtype not found")
Left(DecodingError(s"subtype $subtype not found"))
}
}
case _ =>
Left(s"invalid AttributeValue $av")
Left(DecodingError(s"invalid AttributeValue $av"))
}

private def enumWithDisciminatorOrCaseObjectAnnotationDecoder[Z](
discriminator: String,
cases: Schema.Case[Z, _]*
): Decoder[Z] = { (av: AttributeValue) =>
def findCase(value: String): Either[String, Schema.Case[Z, _]] =
def findCase(value: String): Either[DynamoDBError, Schema.Case[Z, _]] =
cases.find {
case Schema.Case(_, _, _, _, _, Chunk(id(const))) => const == value
case Schema.Case(id, _, _, _, _, _) => id == value
}.toRight(s"type name '$value' not found in schema cases")
}.toRight(DecodingError(s"type name '$value' not found in schema cases"))

def decode(id: String): Either[String, Z] =
def decode(id: String): Either[DynamoDBError, Z] =
findCase(id).flatMap { c =>
val dec = decoder(c.schema)
dec(av).map(_.asInstanceOf[Z])
Expand All @@ -832,30 +834,35 @@ private[dynamodb] object Codec {
if (allCaseObjects(cases))
decode(id)
else
Left(s"Error: not all enumeration elements are case objects. Found $cases")
Left(DecodingError(s"Error: not all enumeration elements are case objects. Found $cases"))
case AttributeValue.Map(map) =>
map
.get(AttributeValue.String(discriminator))
.fold[Either[String, Z]](Left(s"map $av does not contain discriminator field '$discriminator'")) {
.fold[Either[DynamoDBError, Z]](
Left(DecodingError(s"map $av does not contain discriminator field '$discriminator'"))
) {
case AttributeValue.String(typeName) =>
decode(typeName)
case av => Left(s"expected string type but found $av")
case av => Left(DecodingError(s"expected string type but found $av"))
}
case _ => Left(s"unexpected AttributeValue type $av")
case _ => Left(DecodingError(s"unexpected AttributeValue type $av"))
}
}

private[dynamodb] def decodeFields(av: AttributeValue, fields: Schema.Field[_, _]*): Either[String, List[Any]] =
private[dynamodb] def decodeFields(
av: AttributeValue,
fields: Schema.Field[_, _]*
): Either[DynamoDBError, List[Any]] =
av match {
case AttributeValue.Map(map) =>
EitherUtil
.forEach(fields) {
case Schema.Field(key, schema, annotations, _, _, _) =>
val dec = decoder(schema)
val k = maybeId(annotations).getOrElse(key)
val maybeValue = map.get(AttributeValue.String(k))
val maybeDecoder = maybeValue.map(dec).toRight(s"field '$k' not found in $av")
val either: Either[String, Any] = for {
val dec = decoder(schema)
val k = maybeId(annotations).getOrElse(key)
val maybeValue = map.get(AttributeValue.String(k))
val maybeDecoder = maybeValue.map(dec).toRight(DecodingError(s"field '$k' not found in $av"))
val either: Either[DynamoDBError, Any] = for {
decoder <- maybeDecoder
decoded <- decoder
} yield decoded
Expand All @@ -873,7 +880,7 @@ private[dynamodb] object Codec {
}
.map(_.toList)
case _ =>
Left(s"$av is not an AttributeValue.Map")
Left(DecodingError(s"$av is not an AttributeValue.Map"))
}

} // end Decoder
Expand Down
13 changes: 13 additions & 0 deletions dynamodb/src/main/scala/zio/dynamodb/DynamoDBError.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package zio.dynamodb

import scala.util.control.NoStackTrace

sealed trait DynamoDBError extends Exception with NoStackTrace {
def message: String
override def getMessage(): String = message
}

object DynamoDBError {
final case class ValueNotFound(message: String) extends DynamoDBError
final case class DecodingError(message: String) extends DynamoDBError
}
Loading

0 comments on commit 01afd2e

Please sign in to comment.