diff --git a/build.sbt b/build.sbt index b92bc1a6e..8bf826e5d 100644 --- a/build.sbt +++ b/build.sbt @@ -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 @@ -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 diff --git a/dynamodb/src/main/scala/zio/dynamodb/AttrMap.scala b/dynamodb/src/main/scala/zio/dynamodb/AttrMap.scala index 4cd241734..a4e39cbb2 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/AttrMap.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/AttrMap.scala @@ -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]] = @@ -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(_)) diff --git a/dynamodb/src/main/scala/zio/dynamodb/AttributeValue.scala b/dynamodb/src/main/scala/zio/dynamodb/AttributeValue.scala index 3cd15e720..505d821e1 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/AttributeValue.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/AttributeValue.scala @@ -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) diff --git a/dynamodb/src/main/scala/zio/dynamodb/Codec.scala b/dynamodb/src/main/scala/zio/dynamodb/Codec.scala index 40ca4a6be..aef58f9b4 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/Codec.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/Codec.scala @@ -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 @@ -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) @@ -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) @@ -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] = @@ -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(_)) @@ -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]] = { @@ -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)] = @@ -665,13 +667,13 @@ 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]] = { @@ -679,14 +681,14 @@ private[dynamodb] object Codec { 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]] = { @@ -694,7 +696,7 @@ private[dynamodb] object Codec { 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 { @@ -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]]] @@ -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")) } } @@ -757,7 +759,7 @@ 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)) @@ -765,7 +767,7 @@ private[dynamodb] object Codec { } } 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")) } } @@ -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")) } } @@ -796,7 +798,7 @@ 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) @@ -804,24 +806,24 @@ private[dynamodb] object Codec { 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]) @@ -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 @@ -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 diff --git a/dynamodb/src/main/scala/zio/dynamodb/DynamoDBError.scala b/dynamodb/src/main/scala/zio/dynamodb/DynamoDBError.scala new file mode 100644 index 000000000..538e144be --- /dev/null +++ b/dynamodb/src/main/scala/zio/dynamodb/DynamoDBError.scala @@ -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 +} diff --git a/dynamodb/src/main/scala/zio/dynamodb/DynamoDBQuery.scala b/dynamodb/src/main/scala/zio/dynamodb/DynamoDBQuery.scala index 93f336f19..bff7cd409 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/DynamoDBQuery.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/DynamoDBQuery.scala @@ -1,5 +1,6 @@ package zio.dynamodb +import zio.dynamodb.DynamoDBError.ValueNotFound import zio.dynamodb.proofs.{ CanFilter, CanWhere, CanWhereKey } import zio.dynamodb.DynamoDBQuery.BatchGetItem.TableGet import zio.dynamodb.DynamoDBQuery.BatchWriteItem.{ Delete, Put } @@ -449,14 +450,14 @@ object DynamoDBQuery { tableName: String, key: PrimaryKey, projections: ProjectionExpression[_, _]* - ): DynamoDBQuery[A, Either[String, A]] = + ): DynamoDBQuery[A, Either[DynamoDBError, A]] = getItem(tableName, key, projections: _*).map { case Some(item) => fromItem(item) - case None => Left(s"value with key $key not found") + case None => Left(ValueNotFound(s"value with key $key not found")) } - private[dynamodb] def fromItem[A: Schema](item: Item): Either[String, A] = { + private[dynamodb] def fromItem[A: Schema](item: Item): Either[DynamoDBError, A] = { val av = ToAttributeValue.attrMapToAttributeValue.toAttributeValue(item) av.decode(Schema[A]) } @@ -504,7 +505,7 @@ object DynamoDBQuery { tableName: String, limit: Int, projections: ProjectionExpression[_, _]* - ): DynamoDBQuery[A, Either[String, (Chunk[A], LastEvaluatedKey)]] = + ): DynamoDBQuery[A, Either[DynamoDBError, (Chunk[A], LastEvaluatedKey)]] = scanSomeItem(tableName, limit, projections: _*).map { case (itemsChunk, lek) => EitherUtil.forEach(itemsChunk)(item => fromItem(item)).map(Chunk.fromIterable) match { @@ -552,7 +553,7 @@ object DynamoDBQuery { tableName: String, limit: Int, projections: ProjectionExpression[_, _]* - ): DynamoDBQuery[A, Either[String, (Chunk[A], LastEvaluatedKey)]] = + ): DynamoDBQuery[A, Either[DynamoDBError, (Chunk[A], LastEvaluatedKey)]] = querySomeItem(tableName, limit, projections: _*).map { case (itemsChunk, lek) => EitherUtil.forEach(itemsChunk)(item => fromItem(item)).map(Chunk.fromIterable) match { diff --git a/dynamodb/src/main/scala/zio/dynamodb/EitherUtil.scala b/dynamodb/src/main/scala/zio/dynamodb/EitherUtil.scala index 7b40db143..b5865cb80 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/EitherUtil.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/EitherUtil.scala @@ -3,9 +3,9 @@ package zio.dynamodb import scala.annotation.tailrec object EitherUtil { - def forEach[A, B](list: Iterable[A])(f: A => Either[String, B]): Either[String, Iterable[B]] = { + def forEach[A, B, E](list: Iterable[A])(f: A => Either[E, B]): Either[E, Iterable[B]] = { @tailrec - def loop[A2, B2](xs: Iterable[A2], acc: List[B2])(f: A2 => Either[String, B2]): Either[String, Iterable[B2]] = + def loop[A2, B2, E2](xs: Iterable[A2], acc: List[B2])(f: A2 => Either[E2, B2]): Either[E2, Iterable[B2]] = xs match { case head :: tail => f(head) match { @@ -18,5 +18,5 @@ object EitherUtil { loop(list.toList, List.empty)(f) } - def collectAll[A](list: Iterable[Either[String, A]]): Either[String, Iterable[A]] = forEach(list)(identity) + def collectAll[A, E](list: Iterable[Either[E, A]]): Either[E, Iterable[A]] = forEach(list)(identity) } diff --git a/dynamodb/src/main/scala/zio/dynamodb/FromAttributeValue.scala b/dynamodb/src/main/scala/zio/dynamodb/FromAttributeValue.scala index aecd17f69..76837f24c 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/FromAttributeValue.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/FromAttributeValue.scala @@ -1,7 +1,9 @@ package zio.dynamodb +import zio.dynamodb.DynamoDBError.DecodingError + trait FromAttributeValue[+A] { - def fromAttributeValue(av: AttributeValue): Either[String, A] + def fromAttributeValue(av: AttributeValue): Either[DynamoDBError, A] } object FromAttributeValue { @@ -17,76 +19,76 @@ object FromAttributeValue { implicit val binaryFromAttributeValue: FromAttributeValue[Iterable[Byte]] = { case AttributeValue.Binary(b) => Right(b) - case av => Left(s"Error getting binary value. Expected AttributeValue.Binary but found $av") + case av => Left(DecodingError(s"Error getting binary value. Expected AttributeValue.Binary but found $av")) } implicit val byteFromAttributeValue: FromAttributeValue[Byte] = { - case AttributeValue.Binary(b) => b.headOption.toRight("Error: byte array is empty") - case av => Left(s"Error getting byte value. Expected AttributeValue.Binary but found $av") + case AttributeValue.Binary(b) => b.headOption.toRight(DecodingError("Error: byte array is empty")) + case av => Left(DecodingError(s"Error getting byte value. Expected AttributeValue.Binary but found $av")) } implicit def binarySetFromAttributeValue: FromAttributeValue[Iterable[Iterable[Byte]]] = { case AttributeValue.BinarySet(set) => Right(set) - case av => Left(s"Error getting binary set value. Expected AttributeValue.BinarySet but found $av") + case av => Left(DecodingError(s"Error getting binary set value. Expected AttributeValue.BinarySet but found $av")) } implicit val booleanFromAttributeValue: FromAttributeValue[Boolean] = { case AttributeValue.Bool(b) => Right(b) - case av => Left(s"Error getting boolean value. Expected AttributeValue.Bool but found $av") + case av => Left(DecodingError(s"Error getting boolean value. Expected AttributeValue.Bool but found $av")) } implicit val stringFromAttributeValue: FromAttributeValue[String] = { case AttributeValue.String(s) => Right(s) - case av => Left(s"Error getting string value. Expected AttributeValue.String but found $av") + case av => Left(DecodingError(s"Error getting string value. Expected AttributeValue.String but found $av")) } implicit val shortFromAttributeValue: FromAttributeValue[Short] = { case AttributeValue.Number(bd) => Right(bd.shortValue) - case av => Left(s"Error getting short value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting short value. Expected AttributeValue.Number but found $av")) } implicit val shortSetFromAttributeValue: FromAttributeValue[Set[Short]] = { case AttributeValue.NumberSet(bdSet) => Right(bdSet.map(_.shortValue)) - case av => Left(s"Error getting short set value. Expected AttributeValue.NumberSet but found $av") + case av => Left(DecodingError(s"Error getting short set value. Expected AttributeValue.NumberSet but found $av")) } implicit val intFromAttributeValue: FromAttributeValue[Int] = { case AttributeValue.Number(bd) => Right(bd.intValue) - case av => Left(s"Error getting int value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting int value. Expected AttributeValue.Number but found $av")) } implicit val intSetFromAttributeValue: FromAttributeValue[Set[Int]] = { case AttributeValue.NumberSet(bdSet) => Right(bdSet.map(_.intValue)) - case av => Left(s"Error getting int set value. Expected AttributeValue.NumberSet but found $av") + case av => Left(DecodingError(s"Error getting int set value. Expected AttributeValue.NumberSet but found $av")) } implicit val longFromAttributeValue: FromAttributeValue[Long] = { case AttributeValue.Number(bd) => Right(bd.longValue) - case av => Left(s"Error getting long value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting long value. Expected AttributeValue.Number but found $av")) } implicit val longSetFromAttributeValue: FromAttributeValue[Set[Long]] = { case AttributeValue.NumberSet(bdSet) => Right(bdSet.map(_.longValue)) - case av => Left(s"Error getting long set value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting long set value. Expected AttributeValue.Number but found $av")) } implicit val floatFromAttributeValue: FromAttributeValue[Float] = { case AttributeValue.Number(bd) => Right(bd.floatValue) - case av => Left(s"Error getting float value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting float value. Expected AttributeValue.Number but found $av")) } implicit val floatSetFromAttributeValue: FromAttributeValue[Set[Float]] = { case AttributeValue.NumberSet(bdSet) => Right(bdSet.map(_.floatValue)) - case av => Left(s"Error getting float set value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting float set value. Expected AttributeValue.Number but found $av")) } implicit val doubleFromAttributeValue: FromAttributeValue[Double] = { case AttributeValue.Number(bd) => Right(bd.doubleValue) - case av => Left(s"Error getting double value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting double value. Expected AttributeValue.Number but found $av")) } implicit val doubleSetFromAttributeValue: FromAttributeValue[Set[Double]] = { case AttributeValue.NumberSet(bdSet) => Right(bdSet.map(_.doubleValue)) - case av => Left(s"Error getting double value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting double value. Expected AttributeValue.Number but found $av")) } implicit val bigDecimalFromAttributeValue: FromAttributeValue[BigDecimal] = { case AttributeValue.Number(bd) => Right(bd) - case av => Left(s"Error getting BigDecimal value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting BigDecimal value. Expected AttributeValue.Number but found $av")) } implicit val bigDecimalSetFromAttributeValue: FromAttributeValue[Set[BigDecimal]] = { case AttributeValue.NumberSet(bdSet) => Right(bdSet) - case av => Left(s"Error getting BigDecimal set value. Expected AttributeValue.Number but found $av") + case av => Left(DecodingError(s"Error getting BigDecimal set value. Expected AttributeValue.Number but found $av")) } implicit def mapFromAttributeValue[A](implicit ev: FromAttributeValue[A]): FromAttributeValue[Map[String, A]] = { @@ -97,24 +99,24 @@ object FromAttributeValue { ev.fromAttributeValue(avV).map(v => (avK.value, v)) } .map(_.toMap) - case av => Left(s"Error getting map value. Expected AttributeValue.Map but found $av") + case av => Left(DecodingError(s"Error getting map value. Expected AttributeValue.Map but found $av")) } implicit def stringSetFromAttributeValue: FromAttributeValue[Set[String]] = { case AttributeValue.StringSet(set) => Right(set) - case av => Left(s"Error getting string set value. Expected AttributeValue.StringSet but found $av") + case av => Left(DecodingError(s"Error getting string set value. Expected AttributeValue.StringSet but found $av")) } implicit val attrMapFromAttributeValue: FromAttributeValue[AttrMap] = { case AttributeValue.Map(map) => Right(new AttrMap(map.toMap.map { case (k, v) => k.value -> v })) - case av => Left(s"Error getting AttrMap value. Expected AttributeValue.Map but found $av") + case av => Left(DecodingError(s"Error getting AttrMap value. Expected AttributeValue.Map but found $av")) } implicit def iterableFromAttributeValue[A](implicit ev: FromAttributeValue[A]): FromAttributeValue[Iterable[A]] = { case AttributeValue.List(list) => EitherUtil.forEach(list)(ev.fromAttributeValue) - case av => Left(s"Error getting iterable value. Expected AttributeValue.List but found $av") + case av => Left(DecodingError(s"Error getting iterable value. Expected AttributeValue.List but found $av")) } } diff --git a/dynamodb/src/main/scala/zio/dynamodb/package.scala b/dynamodb/src/main/scala/zio/dynamodb/package.scala index 9b2faf462..8b1fa6c90 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/package.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/package.scala @@ -16,7 +16,7 @@ package object dynamodb { type TableNameAndPK = (String, String) type Encoder[A] = A => AttributeValue - type Decoder[+A] = AttributeValue => Either[String, A] + type Decoder[+A] = AttributeValue => Either[DynamoDBError, A] private[dynamodb] def ddbExecute[A](query: DynamoDBQuery[_, A]): ZIO[DynamoDBExecutor, Throwable, A] = ZIO.serviceWithZIO[DynamoDBExecutor](_.execute(query)) diff --git a/dynamodb/src/test/scala/zio/dynamodb/AttributeValueRoundTripSerialisationSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/AttributeValueRoundTripSerialisationSpec.scala index f6d967c8b..d848e3c26 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/AttributeValueRoundTripSerialisationSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/AttributeValueRoundTripSerialisationSpec.scala @@ -7,8 +7,8 @@ object AttributeValueRoundTripSerialisationSpec extends ZIOSpecDefault { private val serialisationSuite = suite("AttributeValue Serialisation suite")(test("round trip serialisation") { check(genSerializable) { s => check(s.genA) { (a: s.Element) => - val av: AttributeValue = s.to.toAttributeValue(a) - val v: Either[String, s.Element] = s.from.fromAttributeValue(av) + val av: AttributeValue = s.to.toAttributeValue(a) + val v: Either[DynamoDBError, s.Element] = s.from.fromAttributeValue(av) assert(v)(isRight(equalTo(a))) } } diff --git a/dynamodb/src/test/scala/zio/dynamodb/FromAttributeValueSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/FromAttributeValueSpec.scala index d4936250c..7ba6ebc81 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/FromAttributeValueSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/FromAttributeValueSpec.scala @@ -1,5 +1,6 @@ package zio.dynamodb +import zio.dynamodb.DynamoDBError.DecodingError import zio.test.Assertion._ import zio.test.{ ZIOSpecDefault, _ } @@ -14,7 +15,7 @@ object FromAttributeValueSpec extends ZIOSpecDefault { }, test("get[String] should return a Left of String when it does not exists") { val attrMap = AttrMap.empty - assert(attrMap.get[String]("f1"))(isLeft(equalTo("field 'f1' not found"))) + assert(attrMap.get[String]("f1"))(isLeft(equalTo(DecodingError("field 'f1' not found")))) }, test("getOptional[String] should return a Right of Some String when it exists") { val attrMap = AttrMap("f1" -> "a") @@ -34,8 +35,8 @@ object FromAttributeValueSpec extends ZIOSpecDefault { }, test("getOptItem should return a Right of Some(Foo) when it exists in the AttrMap") { final case class Foo(s: String, o: Option[String]) - val attrMap = AttrMap("f1" -> AttrMap("f2" -> "a", "f3" -> "b")) - val either: Either[String, Option[Foo]] = for { + val attrMap = AttrMap("f1" -> AttrMap("f2" -> "a", "f3" -> "b")) + val either: Either[DynamoDBError, Option[Foo]] = for { maybe <- attrMap.getOptionalItem("f1") { m => for { s <- m.get[String]("f2") @@ -47,8 +48,8 @@ object FromAttributeValueSpec extends ZIOSpecDefault { }, test("getItemList should return a Right of Iterable[Foo] when it exists in the AttrMap") { final case class Foo(s: String, o: Option[String]) - val attrMap = AttrMap("f1" -> List(AttrMap("f2" -> "a", "f3" -> "b"), AttrMap("f2" -> "c", "f3" -> "d"))) - val either: Either[String, Iterable[Foo]] = for { + val attrMap = AttrMap("f1" -> List(AttrMap("f2" -> "a", "f3" -> "b"), AttrMap("f2" -> "c", "f3" -> "d"))) + val either: Either[DynamoDBError, Iterable[Foo]] = for { xs <- attrMap.getIterableItem[Foo]("f1") { m => for { s <- m.get[String]("f2") @@ -60,8 +61,8 @@ object FromAttributeValueSpec extends ZIOSpecDefault { }, test("getOptionalIterableItem should return a Right of Option[Iterable[Foo]] when it exists in the AttrMap") { final case class Foo(s: String, o: Option[String]) - val attrMap = AttrMap("f1" -> List(AttrMap("f2" -> "a", "f3" -> "b"), AttrMap("f2" -> "c", "f3" -> "d"))) - val either: Either[String, Option[Iterable[Foo]]] = for { + val attrMap = AttrMap("f1" -> List(AttrMap("f2" -> "a", "f3" -> "b"), AttrMap("f2" -> "c", "f3" -> "d"))) + val either: Either[DynamoDBError, Option[Iterable[Foo]]] = for { xs <- attrMap.getOptionalIterableItem[Foo]("f1") { m => for { s <- m.get[String]("f2") diff --git a/dynamodb/src/test/scala/zio/dynamodb/GetAndPutSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/GetAndPutSpec.scala index 06440838d..2ae50087c 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/GetAndPutSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/GetAndPutSpec.scala @@ -1,5 +1,6 @@ package zio.dynamodb +import zio.dynamodb.DynamoDBError.{ DecodingError, ValueNotFound } import zio.dynamodb.DynamoDBQuery.{ get, put } import zio.dynamodb.codec.Invoice import zio.dynamodb.codec.Invoice.PreBilled @@ -29,13 +30,13 @@ object GetAndPutSpec extends ZIOSpecDefault { test("that does not exists") { for { found <- get[SimpleCaseClass2]("table1", primaryKey1).execute - } yield assertTrue(found == Left("value with key AttrMap(Map(id -> Number(1))) not found")) + } yield assertTrue(found == Left(ValueNotFound("value with key AttrMap(Map(id -> Number(1))) not found"))) }, test("with missing attributes results in an error") { for { _ <- TestDynamoDBExecutor.addItems("table1", primaryKey1 -> Item("id" -> 1)) found <- get[SimpleCaseClass2]("table1", primaryKey1).execute - } yield assertTrue(found == Left("field 'name' not found in Map(Map(String(id) -> Number(1)))")) + } yield assertTrue(found == Left(DecodingError("field 'name' not found in Map(Map(String(id) -> Number(1)))"))) }, test("batched") { for { diff --git a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala index af809af59..8a1f1fee8 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala @@ -16,7 +16,7 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { test("decodes generic record") { val expected: Map[String, Any] = ListMap("foo" -> "FOO", "bar" -> 1) - val actual: Either[String, Map[String, Any]] = Codec.decoder(recordSchema)( + val actual: Either[DynamoDBError, Map[String, Any]] = Codec.decoder(recordSchema)( AttributeValue.Map(Map(toAvString("foo") -> toAvString("FOO"), toAvString("bar") -> toAvNum(1))) ) diff --git a/examples/src/main/scala/zio/dynamodb/examples/RoundTripSerialisationExample.scala b/examples/src/main/scala/zio/dynamodb/examples/RoundTripSerialisationExample.scala index 24d956ea8..0b7ebf7ab 100644 --- a/examples/src/main/scala/zio/dynamodb/examples/RoundTripSerialisationExample.scala +++ b/examples/src/main/scala/zio/dynamodb/examples/RoundTripSerialisationExample.scala @@ -1,6 +1,7 @@ package zio.dynamodb.examples -import zio.dynamodb.{ AttrMap, Item } +import zio.dynamodb.DynamoDBError.DecodingError +import zio.dynamodb.{ AttrMap, DynamoDBError, Item } import java.time.{ Instant, ZoneOffset } import scala.util.Try @@ -73,12 +74,12 @@ object RoundTripSerialisationExample extends App { println("invoiceToAttrMap: " + invoiceToAttrMap(invoice1)) - def attrMapToInvoice(m: AttrMap): Either[String, Invoice] = + def attrMapToInvoice(m: AttrMap): Either[DynamoDBError, Invoice] = for { id <- m.get[String]("id") sequence <- m.get[Int]("sequence") dueDateString <- m.get[String]("dueDate") - dueDate <- stringToDate(dueDateString) + dueDate <- stringToDate(dueDateString).left.map(DecodingError) total <- m.get[BigDecimal]("total") isTest <- m.get[Boolean]("isTest") categoryMap <- m.get[Map[String, String]]("categoryMap")