diff --git a/build.sbt b/build.sbt index 9de7680a..37de9705 100644 --- a/build.sbt +++ b/build.sbt @@ -31,10 +31,10 @@ inThisBuild( addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.13" -val zioAwsVersion = "5.20.42.1" -val zioSchemaVersion = "0.4.15" -val zioPreludeVersion = "1.0.0-RC19" +val zioVersion = "2.0.13" +val zioAwsVersion = "5.20.42.1" +val zioSchemaVersion = "0.4.15" +val zioPreludeVersion = "1.0.0-RC19" val zioInteropCats3Version = "23.0.0.8" val catsEffect3Version = "3.5.1" val fs2Version = "3.9.2" diff --git a/docs/codec-customization.md b/docs/codec-customization.md index f411009a..ce86a9a8 100644 --- a/docs/codec-customization.md +++ b/docs/codec-customization.md @@ -37,13 +37,13 @@ Here an intermediate map is used to identify the member of `TraficLight` ie `Map Note that the `Null` is used as in this case we do not care about the value. # Customising encodings via annotations -Encodings can be customised through the use of the following annotations `@discriminatorName`, `@enumOfCaseObjects` and `@fieldName`. +Encodings can be customised through the use of the following annotations `@discriminatorName`, `@simpleEnum` and `@fieldName`. These annotations are useful when working with a legacy DynamoDB database. The `@discriminatorName` encodings does not introduce another map for the purposes of identification but rather adds another discriminator field to the attribute Map. -Concrete examples of using the `@discriminatorName`, `@enumOfCaseObjects` and `@field` annotations can be seen below. +Concrete examples of using the `@discriminatorName`, `@simpleEnum` and `@field` annotations can be seen below. ## Sealed trait members that are case classes @@ -77,7 +77,7 @@ The encoding for case class field names can also be customised via `@fieldName` ## Sealed trait members that are all case objects ```scala -@enumOfCaseObjects +@simpleEnum sealed trait TrafficLight case object GREEN extends TrafficLight @caseName("red_traffic_light") @@ -85,7 +85,7 @@ case object RED extends TrafficLight final case class Box(trafficLightColour: TrafficLight) ``` -We can get a more compact and intuitive encoding of trait members that are case objects by using the `@enumOfCaseObjects` +We can get a more compact and intuitive encoding of trait members that are case objects by using the `@simpleEnum` annotation which encodes to just a value that is the member name. Encoding for an instance of `Box(GREEN)` would be: `Map(trafficLightColour -> String(GREEN))` diff --git a/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala b/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala index 710bdb00..dba4d3ff 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala @@ -4,7 +4,6 @@ import zio.Chunk import zio.schema.annotation.{ caseName, discriminatorName } object Annotations { - final case class enumOfCaseObjects() extends scala.annotation.Annotation def maybeCaseName(annotations: Chunk[Any]): Option[String] = annotations.collect { case caseName(name) => name }.headOption diff --git a/dynamodb/src/main/scala/zio/dynamodb/Codec.scala b/dynamodb/src/main/scala/zio/dynamodb/Codec.scala index 9194db69..31c57d54 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/Codec.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/Codec.scala @@ -1,12 +1,12 @@ package zio.dynamodb -import zio.dynamodb.Annotations.{ enumOfCaseObjects, maybeCaseName, maybeDiscriminator } +import zio.dynamodb.Annotations.{ maybeCaseName, maybeDiscriminator } import zio.dynamodb.DynamoDBError.DecodingError import zio.prelude.{ FlipOps, ForEachOps } import zio.schema.Schema.{ Optional, Primitive } -import zio.schema.annotation.{ caseName, discriminatorName } +import zio.schema.annotation.{ caseName, discriminatorName, simpleEnum } import zio.schema.{ FieldSet, Schema, StandardType } -import zio.{ Chunk } +import zio.Chunk import java.math.BigInteger import java.time._ @@ -269,7 +269,6 @@ private[dynamodb] object Codec { private def enumEncoder[Z](annotations: Chunk[Any], cases: Schema.Case[Z, _]*): Encoder[Z] = if (hasAnnotationAtClassLevel(annotations)) enumWithAnnotationAtClassLevelEncoder( - isCaseObjectAnnotation(annotations), discriminatorWithDefault(annotations), cases: _* ) @@ -290,7 +289,6 @@ private[dynamodb] object Codec { } private def enumWithAnnotationAtClassLevelEncoder[Z]( - hasEnumOfCaseObjectsAnnotation: Boolean, discriminator: String, cases: Schema.Case[Z, _]* ): Encoder[Z] = @@ -303,22 +301,17 @@ private[dynamodb] object Codec { val id = maybeCaseName(case_.annotations).getOrElse(case_.id) val av2 = AttributeValue.String(id) av match { // TODO: review all pattern matches inside of a lambda - case AttributeValue.Map(map) => + case AttributeValue.Map(map) => AttributeValue.Map( map + (AttributeValue.String(discriminator) -> av2) ) - case AttributeValue.Null - if (hasEnumOfCaseObjectsAnnotation && allCaseObjects(cases)) || !hasEnumOfCaseObjectsAnnotation => + case AttributeValue.Null => if (allCaseObjects(cases)) av2 else // these are case objects and are a special case - they need to wrapped in an AttributeValue.Map AttributeValue.Map(Map(AttributeValue.String(discriminator) -> av2)) - case _ if (hasEnumOfCaseObjectsAnnotation && !allCaseObjects(cases)) => - throw new IllegalStateException( - s"Can not encode enum ${case_.id} - @enumOfCaseObjects annotation present when all instances are not case objects." - ) - case av => throw new IllegalStateException(s"unexpected state $av") + case av => throw new IllegalStateException(s"unexpected state $av") } } else AttributeValue.Null @@ -812,7 +805,6 @@ private[dynamodb] object Codec { private def enumDecoder[Z](annotations: Chunk[Any], cases: Schema.Case[Z, _]*): Decoder[Z] = if (hasAnnotationAtClassLevel(annotations)) enumWithAnnotationAtClassLevelDecoder( - isCaseObjectAnnotation(annotations), discriminatorWithDefault(annotations), cases: _* ) @@ -841,7 +833,6 @@ private[dynamodb] object Codec { } private def enumWithAnnotationAtClassLevelDecoder[Z]( - hasEnumOfCaseObjectsAnnotation: Boolean, discriminator: String, cases: Schema.Case[Z, _]* ): Decoder[Z] = { (av: AttributeValue) => @@ -858,10 +849,9 @@ private[dynamodb] object Codec { } av match { - case AttributeValue.String(id) - if (hasEnumOfCaseObjectsAnnotation && allCaseObjects(cases)) || !hasEnumOfCaseObjectsAnnotation => + case AttributeValue.String(id) => decode(id) - case AttributeValue.Map(map) => + case AttributeValue.Map(map) => map .get(AttributeValue.String(discriminator)) .fold[Either[DynamoDBError, Z]]( @@ -872,13 +862,7 @@ private[dynamodb] object Codec { case av => Left(DecodingError(s"expected string type but found $av")) } - case _ if hasEnumOfCaseObjectsAnnotation && !allCaseObjects(cases) => - Left( - DecodingError( - s"Can not decode enum $av - @enumOfCaseObjects annotation present when all instances are not case objects." - ) - ) - case _ => + case _ => Left(DecodingError(s"unexpected AttributeValue type $av")) } } @@ -931,14 +915,8 @@ private[dynamodb] object Codec { private def hasAnnotationAtClassLevel(annotations: Chunk[Any]): Boolean = annotations.exists { - case discriminatorName(_) | enumOfCaseObjects() => true - case _ => false - } - - private def isCaseObjectAnnotation(annotations: Chunk[Any]): Boolean = - annotations.exists { - case enumOfCaseObjects() => true - case _ => false + case discriminatorName(_) | simpleEnum(_) => true + case _ => false } } // end Codec diff --git a/dynamodb/src/test/scala/zio/dynamodb/ProjectionExpressionSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/ProjectionExpressionSpec.scala index 48c82466..b48f40ae 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/ProjectionExpressionSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/ProjectionExpressionSpec.scala @@ -1,7 +1,7 @@ package zio.dynamodb -import zio.dynamodb.Annotations.enumOfCaseObjects import zio.dynamodb.ProjectionExpression.{ $, mapElement, MapElement, Root } +import zio.schema.annotation.simpleEnum import zio.schema.{ DeriveSchema, Schema } import zio.test.Assertion._ import zio.test.{ assert, assertTrue, ZIOSpecDefault } @@ -15,7 +15,7 @@ object ProjectionExpressionSpec extends ZIOSpecDefault { private val groups = "groups" private val payment = "payment" - @enumOfCaseObjects + @simpleEnum sealed trait Payment object Payment { case object CreditCard extends Payment diff --git a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala index eb2e15b5..d4cd4ff8 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala @@ -284,35 +284,20 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(PreBilled(id = 1, s = "foobar")))) }, - test("decodes case object only enum with @enumOfCaseObjects annotation and without @caseName annotation") { + test("decodes case object only enum with @simpleEnum annotation and without @caseName annotation") { val item: Item = Item(Map("enum" -> AttributeValue.String("ONE"))) val actual = DynamoDBQuery.fromItem[WithCaseObjectOnlyEnum](item) assert(actual)(isRight(equalTo(WithCaseObjectOnlyEnum(WithCaseObjectOnlyEnum.ONE)))) }, - test("decodes case object only enum with @enumOfCaseObjects annotation and @caseName annotation of '2'") { + test("decodes case object only enum with @simpleEnum annotation and @caseName annotation of '2'") { val item: Item = Item(Map("enum" -> AttributeValue.String("2"))) val actual = DynamoDBQuery.fromItem[WithCaseObjectOnlyEnum](item) assert(actual)(isRight(equalTo(WithCaseObjectOnlyEnum(WithCaseObjectOnlyEnum.TWO)))) }, - test("fails decoding of enum with @enumOfCaseObjects annotation that does not have all case objects") { - val item: Item = Item(Map("enum" -> AttributeValue.String("ONE"))) - - val actual = DynamoDBQuery.fromItem[WithCaseObjectOnlyEnum2](item) - - assert(actual)( - isLeft( - hasMessage( - equalTo( - "Can not decode enum String(ONE) - @enumOfCaseObjects annotation present when all instances are not case objects." - ) - ) - ) - ) - }, test( "decodes enum and honours @caseName annotation at case class level when there is no @discriminatorName annotation" ) { diff --git a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala index 66f14204..c5f2832b 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala @@ -8,7 +8,6 @@ import zio.test._ import java.time.Instant import scala.collection.immutable.ListMap import zio.test.ZIOSpecDefault -import scala.util.Try object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { override def spec = suite("ItemEncoder Suite")(mainSuite) @@ -219,35 +218,21 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes case object only enum with @enumOfCaseObjects annotation") { + test("encodes case object only enum with @simpleEnum annotation") { val expectedItem: Item = Item(Map("enum" -> AttributeValue.String("ONE"))) val item = DynamoDBQuery.toItem(WithCaseObjectOnlyEnum(WithCaseObjectOnlyEnum.ONE)) assert(item)(equalTo(expectedItem)) }, - test("encodes case object only enum with @enumOfCaseObjects annotation and @caseName annotation of '2'") { + test("encodes case object only enum with @simpleEnum annotation and @caseName annotation of '2'") { val expectedItem: Item = Item(Map("enum" -> AttributeValue.String("2"))) val item = DynamoDBQuery.toItem(WithCaseObjectOnlyEnum(WithCaseObjectOnlyEnum.TWO)) assert(item)(equalTo(expectedItem)) }, - test("fails encoding of enum with @enumOfCaseObjects annotation that does not have all case objects") { - - val item = Try(DynamoDBQuery.toItem(WithCaseObjectOnlyEnum2(WithCaseObjectOnlyEnum2.ONE))) - - assert(item)( - isFailure( - hasMessage( - equalTo( - "Can not encode enum ONE - @enumOfCaseObjects annotation present when all instances are not case objects." - ) - ) - ) - ) - }, - test("encodes enum and honours @caseName annotation when there is no @enumOfCaseObjects annotation") { + test("encodes enum and honours @caseName annotation when there is no @simpleEnum annotation") { val expectedItem: Item = Item("enum" -> Item(Map("1" -> AttributeValue.Null))) val item = DynamoDBQuery.toItem(WithEnumWithoutDiscriminator(WithEnumWithoutDiscriminator.ONE)) diff --git a/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala b/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala index b7f0f8c7..4d7cc95b 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala @@ -1,7 +1,6 @@ package zio.dynamodb.codec -import zio.dynamodb.Annotations.enumOfCaseObjects -import zio.schema.annotation.{ caseName, discriminatorName, fieldName } +import zio.schema.annotation.{ caseName, discriminatorName, fieldName, simpleEnum } import zio.schema.{ DeriveSchema, Schema } import java.time.Instant @@ -58,18 +57,7 @@ object WithDiscriminatedEnum { implicit val schema: Schema[WithDiscriminatedEnum] = DeriveSchema.gen[WithDiscriminatedEnum] } -@enumOfCaseObjects // should fail runtime validation as Three is not a case object -sealed trait CaseObjectOnlyEnum2 -final case class WithCaseObjectOnlyEnum2(`enum`: CaseObjectOnlyEnum2) -object WithCaseObjectOnlyEnum2 { - case object ONE extends CaseObjectOnlyEnum2 - @caseName("2") - case object TWO extends CaseObjectOnlyEnum2 - case class Three(@fieldName("funky_field_name") value: String) extends CaseObjectOnlyEnum2 - implicit val schema: Schema[WithCaseObjectOnlyEnum2] = DeriveSchema.gen[WithCaseObjectOnlyEnum2] -} - -@enumOfCaseObjects +@simpleEnum sealed trait CaseObjectOnlyEnum final case class WithCaseObjectOnlyEnum(`enum`: CaseObjectOnlyEnum) object WithCaseObjectOnlyEnum { diff --git a/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala b/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala index ad10763d..32c6cc8e 100644 --- a/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala +++ b/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala @@ -2,7 +2,6 @@ package zio.dynamodb.examples import zio.Console.printLine import zio.ZIOAppDefault -import zio.dynamodb.Annotations.enumOfCaseObjects import zio.dynamodb.examples.TypeSafeRoundTripSerialisationExample.Invoice.{ Address, Billed, @@ -12,7 +11,7 @@ import zio.dynamodb.examples.TypeSafeRoundTripSerialisationExample.Invoice.{ Product } import zio.dynamodb.{ DynamoDBExecutor, DynamoDBQuery, PrimaryKey } -import zio.schema.annotation.{ caseName, discriminatorName } +import zio.schema.annotation.{ caseName, discriminatorName, simpleEnum } import zio.schema.{ DeriveSchema, Schema } import java.time.Instant @@ -27,7 +26,7 @@ object TypeSafeRoundTripSerialisationExample extends ZIOAppDefault { def id: String } object Invoice { - @enumOfCaseObjects + @simpleEnum sealed trait PaymentType object PaymentType { @caseName("debit") diff --git a/examples/src/main/scala/zio/dynamodb/examples/model/Student.scala b/examples/src/main/scala/zio/dynamodb/examples/model/Student.scala index b8502214..82914101 100644 --- a/examples/src/main/scala/zio/dynamodb/examples/model/Student.scala +++ b/examples/src/main/scala/zio/dynamodb/examples/model/Student.scala @@ -1,14 +1,14 @@ package zio.dynamodb.examples.model -import zio.dynamodb.Annotations.enumOfCaseObjects import zio.dynamodb.ProjectionExpression import zio.schema.DeriveSchema import java.time.Instant import zio.schema.Schema import zio.dynamodb.KeyConditionExpr +import zio.schema.annotation.simpleEnum -@enumOfCaseObjects +@simpleEnum sealed trait Payment object Payment {