From 307dd5658f4ba722a2c6e96f80af3d7bb436dc40 Mon Sep 17 00:00:00 2001 From: Avinder Bahra Date: Mon, 6 Feb 2023 07:59:23 +0000 Subject: [PATCH] Align series/2.x branch codec generation annotations with zio-schema (#155) --- docs/codec-customization.md | 22 ++--- .../main/scala/zio/dynamodb/Annotations.scala | 11 +-- .../src/main/scala/zio/dynamodb/Codec.scala | 96 ++++++++++++------- .../zio/dynamodb/ProjectionExpression.scala | 8 +- .../OpticsShouldRespectAnnotationsSpec.scala | 28 +++--- .../zio/dynamodb/codec/ItemDecoderSpec.scala | 42 +++++--- .../zio/dynamodb/codec/ItemEncoderSpec.scala | 37 ++++--- .../scala/zio/dynamodb/codec/models.scala | 42 +++++--- ...ypeSafeRoundTripSerialisationExample.scala | 9 +- .../TypeSafeAPIExampleWithDiscriminator.scala | 9 +- 10 files changed, 188 insertions(+), 116 deletions(-) diff --git a/docs/codec-customization.md b/docs/codec-customization.md index 9571abd9d..f411009a3 100644 --- a/docs/codec-customization.md +++ b/docs/codec-customization.md @@ -37,23 +37,23 @@ 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 `@discriminator`, `@enumOfCaseObjects` and `@id`. +Encodings can be customised through the use of the following annotations `@discriminatorName`, `@enumOfCaseObjects` and `@fieldName`. These annotations are useful when working with a legacy DynamoDB database. -The `@discriminator` encodings does not introduce another map for the purposes of identification but rather adds another +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 `@discriminator`, `@enumOfCaseObjects` and `@id` annotations can be seen below. +Concrete examples of using the `@discriminatorName`, `@enumOfCaseObjects` and `@field` annotations can be seen below. ## Sealed trait members that are case classes ```scala -@discriminator("light_type") +@discriminatorName("light_type") sealed trait TrafficLight final case class Green(rgb: Int) extends TrafficLight -@id("red_traffic_light") +@caseName("red_traffic_light") final case class Red(rgb: Int) extends TrafficLight -final case class Amber(@id("red_green_blue") rgb: Int) extends TrafficLight +final case class Amber(@fieldName("red_green_blue") rgb: Int) extends TrafficLight final case class Box(trafficLightColour: TrafficLight) ``` @@ -61,15 +61,15 @@ encoding for an instance of `Box(Green(42))` would be: `Map(trafficLightColour -> Map(String(rgb) -> Number(42), String(light_type) -> String(Green)))` -We can specify the field name used to identify the case class through the `@discriminator` annotation. The discriminator +We can specify the field name used to identify the case class through the `@discriminatorName` annotation. The discriminator encoding removes the intermediate map and inserts a new field with a name specified by discriminator annotation and a value that identifies the member which defaults to the class name. -This can be further customised using the `@id` annotation - encoding for an instance of `Box(Red(42))` would be: +This can be further customised using the `@caseName` annotation - encoding for an instance of `Box(Red(42))` would be: `Map(trafficLightColour -> Map(String(rgb) -> Number(42), String(light_type) -> String(red_traffic_light)))` -The encoding for case class field names can also be customised via `@id` - encoding for an instance of `Box(Amber(42))` would be: +The encoding for case class field names can also be customised via `@fieldName` - encoding for an instance of `Box(Amber(42))` would be: `Map(trafficLightColour -> Map(String(red_green_blue) -> Number(42), String(light_type) -> String(Amber)))` @@ -80,7 +80,7 @@ The encoding for case class field names can also be customised via `@id` - encod @enumOfCaseObjects sealed trait TrafficLight case object GREEN extends TrafficLight -@id("red_traffic_light") +@caseName("red_traffic_light") case object RED extends TrafficLight final case class Box(trafficLightColour: TrafficLight) ``` @@ -90,7 +90,7 @@ annotation which encodes to just a value that is the member name. Encoding for a `Map(trafficLightColour -> String(GREEN))` -This can be further customised by using the `@id` annotation again - encoding for `Box(RED)` would be +This can be further customised by using the `@caseName` annotation again - encoding for `Box(RED)` would be `Map(trafficLightColour -> String(red_traffic_light))` diff --git a/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala b/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala index 7a2706def..710bdb003 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/Annotations.scala @@ -1,15 +1,14 @@ package zio.dynamodb import zio.Chunk +import zio.schema.annotation.{ caseName, discriminatorName } object Annotations { - final case class discriminator(name: String) extends scala.annotation.Annotation - final case class enumOfCaseObjects() extends scala.annotation.Annotation - final case class id(value: String) extends scala.annotation.Annotation + final case class enumOfCaseObjects() extends scala.annotation.Annotation - def maybeId(annotations: Chunk[Any]): Option[String] = - annotations.collect { case id(name) => name }.headOption + def maybeCaseName(annotations: Chunk[Any]): Option[String] = + annotations.collect { case caseName(name) => name }.headOption def maybeDiscriminator(annotations: Chunk[Any]): Option[String] = - annotations.collect { case discriminator(name) => name }.headOption + annotations.collect { case discriminatorName(name) => name }.headOption } diff --git a/dynamodb/src/main/scala/zio/dynamodb/Codec.scala b/dynamodb/src/main/scala/zio/dynamodb/Codec.scala index aef58f9b4..00a6e578f 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/Codec.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/Codec.scala @@ -1,8 +1,9 @@ package zio.dynamodb -import zio.dynamodb.Annotations.{ discriminator, enumOfCaseObjects, id, maybeDiscriminator, maybeId } +import zio.dynamodb.Annotations.{ enumOfCaseObjects, maybeCaseName, maybeDiscriminator } import zio.dynamodb.DynamoDBError.DecodingError import zio.schema.Schema.{ Optional, Primitive } +import zio.schema.annotation.{ caseName, discriminatorName } import zio.schema.{ FieldSet, Schema, StandardType } import zio.{ schema, Chunk } @@ -177,7 +178,7 @@ private[dynamodb] object Codec { val enc = encoder(s._1.schema) val extractedFieldValue = s._1.get(a) val av = enc(extractedFieldValue) - val k = maybeId(s._1.annotations).getOrElse(s._1.name) + val k = s._1.name @tailrec def appendToMap[B](schema: Schema[B]): AttributeValue.Map = @@ -264,8 +265,12 @@ private[dynamodb] object Codec { (col: Col) => AttributeValue.List(from(col).map(encoder)) private def enumEncoder[Z](annotations: Chunk[Any], cases: Schema.Case[Z, _]*): Encoder[Z] = - if (isEnumWithDiscriminatorOrCaseObjectAnnotationCodec(annotations)) - enumWithDiscriminatorOrCaseObjectAnnotationEncoder(discriminatorWithDefault(annotations), cases: _*) + if (hasAnnotationAtClassLevel(annotations)) + enumWithAnnotationAtClassLevelEncoder( + isCaseObjectAnnotation(annotations), + discriminatorWithDefault(annotations), + cases: _* + ) else defaultEnumEncoder(cases: _*) @@ -276,36 +281,42 @@ private[dynamodb] object Codec { val case_ = cases(fieldIndex) val enc = encoder(case_.schema.asInstanceOf[Schema[Any]]) val av = enc(a) - val id = maybeId(case_.annotations).getOrElse(case_.id) + val id = maybeCaseName(case_.annotations).getOrElse(case_.id) AttributeValue.Map(Map.empty + (AttributeValue.String(id) -> av)) } else AttributeValue.Null } - private def enumWithDiscriminatorOrCaseObjectAnnotationEncoder[Z]( + private def enumWithAnnotationAtClassLevelEncoder[Z]( + hasEnumOfCaseObjectsAnnotation: Boolean, discriminator: String, cases: Schema.Case[Z, _]* ): Encoder[Z] = (a: Z) => { val fieldIndex = cases.indexWhere(c => c.deconstructOption(a).isDefined) if (fieldIndex > -1) { - val case_ = cases(fieldIndex) - val enc = encoder(case_.schema.asInstanceOf[Schema[Any]]) - lazy val id = maybeId(case_.annotations).getOrElse(case_.id) - val av = enc(a) + val case_ = cases(fieldIndex) + val enc = encoder(case_.schema.asInstanceOf[Schema[Any]]) + val av = enc(a) + 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) -> AttributeValue.String(id)) + map + (AttributeValue.String(discriminator) -> av2) ) - case AttributeValue.Null => - val av2 = AttributeValue.String(id) + case AttributeValue.Null + if (hasEnumOfCaseObjectsAnnotation && allCaseObjects(cases)) || !hasEnumOfCaseObjectsAnnotation => 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 av => throw new IllegalStateException(s"unexpected state $av") + 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") } } else AttributeValue.Null @@ -787,8 +798,12 @@ private[dynamodb] object Codec { } private def enumDecoder[Z](annotations: Chunk[Any], cases: Schema.Case[Z, _]*): Decoder[Z] = - if (isEnumWithDiscriminatorOrCaseObjectAnnotationCodec(annotations)) - enumWithDisciminatorOrCaseObjectAnnotationDecoder(discriminatorWithDefault(annotations), cases: _*) + if (hasAnnotationAtClassLevel(annotations)) + enumWithAnnotationAtClassLevelDecoder( + isCaseObjectAnnotation(annotations), + discriminatorWithDefault(annotations), + cases: _* + ) else defaultEnumDecoder(cases: _*) @@ -801,7 +816,7 @@ private[dynamodb] object Codec { 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) + maybeCaseName(c.annotations).fold(c.id == subtype)(_ == subtype) } match { case Some(c) => decoder(c.schema)(av).map(_.asInstanceOf[Z]) @@ -813,14 +828,15 @@ private[dynamodb] object Codec { Left(DecodingError(s"invalid AttributeValue $av")) } - private def enumWithDisciminatorOrCaseObjectAnnotationDecoder[Z]( + private def enumWithAnnotationAtClassLevelDecoder[Z]( + hasEnumOfCaseObjectsAnnotation: Boolean, discriminator: String, cases: Schema.Case[Z, _]* ): Decoder[Z] = { (av: AttributeValue) => 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 + case Schema.Case(_, _, _, _, _, Chunk(caseName(const))) => const == value + case Schema.Case(id, _, _, _, _, _) => id == value }.toRight(DecodingError(s"type name '$value' not found in schema cases")) def decode(id: String): Either[DynamoDBError, Z] = @@ -830,12 +846,10 @@ private[dynamodb] object Codec { } av match { - case AttributeValue.String(id) => - if (allCaseObjects(cases)) - decode(id) - else - Left(DecodingError(s"Error: not all enumeration elements are case objects. Found $cases")) - case AttributeValue.Map(map) => + case AttributeValue.String(id) + if (hasEnumOfCaseObjectsAnnotation && allCaseObjects(cases)) || !hasEnumOfCaseObjectsAnnotation => + decode(id) + case AttributeValue.Map(map) => map .get(AttributeValue.String(discriminator)) .fold[Either[DynamoDBError, Z]]( @@ -843,9 +857,17 @@ private[dynamodb] object Codec { ) { case AttributeValue.String(typeName) => decode(typeName) - case av => Left(DecodingError(s"expected string type but found $av")) + case av => + Left(DecodingError(s"expected string type but found $av")) } - case _ => Left(DecodingError(s"unexpected AttributeValue type $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 _ => + Left(DecodingError(s"unexpected AttributeValue type $av")) } } @@ -857,9 +879,9 @@ private[dynamodb] object Codec { case AttributeValue.Map(map) => EitherUtil .forEach(fields) { - case Schema.Field(key, schema, annotations, _, _, _) => + case Schema.Field(key, schema, _, _, _, _) => val dec = decoder(schema) - val k = maybeId(annotations).getOrElse(key) + val k = key // @fieldName is respected by the zio-schema macro 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 { @@ -896,10 +918,16 @@ private[dynamodb] object Codec { private def discriminatorWithDefault(annotations: Chunk[Any]): String = maybeDiscriminator(annotations).getOrElse("discriminator") - private def isEnumWithDiscriminatorOrCaseObjectAnnotationCodec(annotations: Chunk[Any]): Boolean = + 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 discriminator(_) | enumOfCaseObjects() => true - case _ => false + case enumOfCaseObjects() => true + case _ => false } } // end Codec diff --git a/dynamodb/src/main/scala/zio/dynamodb/ProjectionExpression.scala b/dynamodb/src/main/scala/zio/dynamodb/ProjectionExpression.scala index a10b4cfe4..03955ec54 100644 --- a/dynamodb/src/main/scala/zio/dynamodb/ProjectionExpression.scala +++ b/dynamodb/src/main/scala/zio/dynamodb/ProjectionExpression.scala @@ -1,7 +1,7 @@ package zio.dynamodb import zio.Chunk -import zio.dynamodb.Annotations.{ maybeDiscriminator, maybeId } +import zio.dynamodb.Annotations.{ maybeCaseName, maybeDiscriminator } import zio.dynamodb.ConditionExpression.Operand.ProjectionExpressionOperand import zio.dynamodb.ProjectionExpression.{ ListElement, MapElement, Root } import zio.dynamodb.UpdateExpression.SetOperand.{ IfNotExists, ListAppend, ListPrepend, PathOperand } @@ -689,10 +689,10 @@ object ProjectionExpression extends ProjectionExpressionLowPriorityImplicits0 { override type Prism[F, From, To] = ProjectionExpression[From, To] override type Traversal[From, To] = Unit - // respects @id annotation + // respects @caseName annotation override def makeLens[F, S, A](product: Schema.Record[S], term: Schema.Field[S, A]): Lens[F, S, A] = { - val label = maybeId(term.annotations).getOrElse(term.name) + val label = maybeCaseName(term.annotations).getOrElse(term.name) ProjectionExpression.MapElement(Root, label) } @@ -702,7 +702,7 @@ object ProjectionExpression extends ProjectionExpressionLowPriorityImplicits0 { ProjectionExpression.Root.asInstanceOf[Prism[F, S, A]] case None => ProjectionExpression - .MapElement(Root, maybeId(term.annotations).getOrElse(term.id)) + .MapElement(Root, maybeCaseName(term.annotations).getOrElse(term.id)) .asInstanceOf[Prism[F, S, A]] } diff --git a/dynamodb/src/test/scala/zio/dynamodb/OpticsShouldRespectAnnotationsSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/OpticsShouldRespectAnnotationsSpec.scala index 5d739c378..091a43fca 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/OpticsShouldRespectAnnotationsSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/OpticsShouldRespectAnnotationsSpec.scala @@ -1,6 +1,6 @@ package zio.dynamodb -import zio.dynamodb.Annotations.{ discriminator, id } +import zio.schema.annotation.{ caseName, discriminatorName, fieldName } import zio.schema.{ DeriveSchema, Schema } import zio.test.Assertion.equalTo import zio.test.{ assert, ZIOSpecDefault } @@ -12,7 +12,7 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { object BoxOfCaseObjectOnlyEnum { case object ONE extends CaseObjectOnlyEnum - @id("2") + @caseName("2") case object TWO extends CaseObjectOnlyEnum implicit val schema: Schema[BoxOfCaseObjectOnlyEnum] = DeriveSchema.gen[BoxOfCaseObjectOnlyEnum] val sumType = ProjectionExpression.accessors[BoxOfCaseObjectOnlyEnum] @@ -29,7 +29,7 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { val rgb: ProjectionExpression[Green, Int] = ProjectionExpression.accessors[Green] } - @id("red_traffic_light") + @caseName("red_traffic_light") final case class Red(rgb: Int) extends TrafficLight object Red { @@ -37,7 +37,7 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { val rgb: ProjectionExpression[Red, Int] = ProjectionExpression.accessors[Red] } - final case class Amber(@id("red_green_blue") rgb: Int) extends TrafficLight + final case class Amber(@fieldName("red_green_blue") rgb: Int) extends TrafficLight object Amber { implicit val schema = DeriveSchema.gen[Amber] @@ -57,7 +57,7 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { } - @discriminator("light_type") + @discriminatorName("light_type") sealed trait TrafficLightDiscriminated object TrafficLightDiscriminated { @@ -68,7 +68,7 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { val rgb: ProjectionExpression[Green, Int] = ProjectionExpression.accessors[Green] } - @id("red_traffic_light") + @caseName("red_traffic_light") final case class Red(rgb: Int) extends TrafficLightDiscriminated object Red { @@ -76,7 +76,7 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { val rgb: ProjectionExpression[Red, Int] = ProjectionExpression.accessors[Red] } - final case class Amber(@id("red_green_blue") rgb: Int) extends TrafficLightDiscriminated + final case class Amber(@fieldName("red_green_blue") rgb: Int) extends TrafficLightDiscriminated object Amber { implicit val schema = DeriveSchema.gen[Amber] @@ -118,13 +118,15 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { TrafficLightDiscriminated.Box.trafficLightColour >>> TrafficLightDiscriminated.green >>> TrafficLightDiscriminated.Green.rgb assert(pe.toString)(equalTo("trafficLightColour.rgb")) }, - test("@id annotations at class level do not affect traversal as they are bypassed ie trafficLightColour.rgb") { + test( + "@caseName annotations at class level do not affect traversal as they are bypassed ie trafficLightColour.rgb" + ) { // Map(String(rgb) -> Number(42), String(light_type) -> String(red_traffic_light)) val pe = TrafficLightDiscriminated.Box.trafficLightColour >>> TrafficLightDiscriminated.red >>> TrafficLightDiscriminated.Red.rgb assert(pe.toString)(equalTo("trafficLightColour.rgb")) }, - test("@id annotations at field level are honoured") { + test("@fieldName annotations are honoured") { // Map(String(rgb) -> Number(42), String(light_type) -> String(Amber)) val pe = TrafficLightDiscriminated.Box.trafficLightColour >>> TrafficLightDiscriminated.amber >>> TrafficLightDiscriminated.Amber.rgb @@ -137,13 +139,13 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { val nonDiscriminatedSuite = { val conditionExpressionSuite = suite("ConditionExpression suite")( - test("Path with no @id at class or field level results in a PE of trafficLightColour.Green.rgb") { + test("Path with no @caseName or @fieldName annotations results in a PE of trafficLightColour.Green.rgb") { val ce = TrafficLight.Box.trafficLightColour >>> TrafficLight.green >>> TrafficLight.Green.rgb === 1 assert(ce.toString)( equalTo("Equals(ProjectionExpressionOperand(trafficLightColour.Green.rgb),ValueOperand(Number(1)))") ) }, - test("Path with @id at class but not field level results in a PE of trafficLightColour.red_traffic_light.rgb") { + test("Path with @caseName but no @fieldName results in a PE of trafficLightColour.red_traffic_light.rgb") { val ce = TrafficLight.Box.trafficLightColour >>> TrafficLight.red >>> TrafficLight.Red.rgb === 1 assert(ce.toString)( equalTo( @@ -158,13 +160,13 @@ object OpticsShouldRespectAnnotationsSpec extends ZIOSpecDefault { val pe = TrafficLight.Box.trafficLightColour >>> TrafficLight.green >>> TrafficLight.Green.rgb assert(pe.toString)(equalTo("trafficLightColour.Green.rgb")) }, - test("@id annotations at class level are honoured") { + test("@caseName annotations are honoured") { // Map(trafficLightColour -> Map(String(red_traffic_light) -> Map(String(rgb) -> Number(42)))) val pe = TrafficLight.Box.trafficLightColour >>> TrafficLight.red >>> TrafficLight.Red.rgb assert(pe.toString)(equalTo("trafficLightColour.red_traffic_light.rgb")) }, - test("@id annotations at field level are honoured") { + test("@fieldName annotations are honoured") { val pe = TrafficLight.Box.trafficLightColour >>> TrafficLight.amber >>> TrafficLight.Amber.rgb assert(pe.toString)(equalTo("trafficLightColour.Amber.red_green_blue")) diff --git a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala index 8a1f1fee8..eb2e15b55 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala @@ -3,11 +3,10 @@ package zio.dynamodb.codec import zio.dynamodb._ import zio.dynamodb.codec.Invoice.PreBilled import zio.test.Assertion._ -import zio.test._ +import zio.test.{ ZIOSpecDefault, _ } import java.time.Instant import scala.collection.immutable.ListMap -import zio.test.ZIOSpecDefault object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { override def spec = suite("ItemDecoder Suite")(mainSuite) @@ -188,7 +187,7 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(expected))) }, - test("decodes enum with discriminator annotation") { + test("decodes enum with @discriminatorName annotation") { val item: Item = Item( Map( @@ -205,13 +204,13 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(WithDiscriminatedEnum(WithDiscriminatedEnum.StringValue("foobar"))))) }, - test("decodes enum with discriminator annotation and id annotation on a case class field ") { + test("decodes enum with @discriminatorName annotation and @fieldName annotation on a case class field ") { val item: Item = Item( Map( "enum" -> AttributeValue.Map( Map( - AttributeValue.String("funky_value") -> AttributeValue.String("foobar"), + AttributeValue.String("funky_field_name") -> AttributeValue.String("foobar"), AttributeValue.String("funkyDiscriminator") -> AttributeValue.String("StringValue2") ) ) @@ -222,7 +221,7 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(WithDiscriminatedEnum(WithDiscriminatedEnum.StringValue2("foobar"))))) }, - test("decodes enum with discriminator annotation and an id annotation on a case class") { + test("decodes enum with @discriminatorName annotation and an @caseName annotation on a case class") { val item: Item = Item( Map( @@ -239,7 +238,7 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(WithDiscriminatedEnum(WithDiscriminatedEnum.IntValue(1))))) }, - test("decodes enum with discriminator annotation and case object as item without a id annotation") { + test("decodes enum with @discriminatorName annotation and case object as item without a @caseName annotation") { val item: Item = Item( Map( @@ -255,7 +254,7 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(WithDiscriminatedEnum(WithDiscriminatedEnum.ONE)))) }, - test("decodes enum with discriminator annotation and case object as item with a id annotation of '2'") { + test("decodes enum with @discriminatorName annotation and case object as item with a @caseName annotation of '2'") { val item: Item = Item( Map( @@ -271,7 +270,7 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(WithDiscriminatedEnum(WithDiscriminatedEnum.TWO)))) }, - test("decodes top level enum with discriminator annotation") { + test("decodes top level enum with @discriminatorName annotation") { val item: Item = Item( Map( @@ -285,28 +284,45 @@ object ItemDecoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(actual)(isRight(equalTo(PreBilled(id = 1, s = "foobar")))) }, - test("decodes case object only enum with id annotation") { + test("decodes case object only enum with @enumOfCaseObjects 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 enumNameAsValue annotation and id annotation of '2'") { + test("decodes case object only enum with @enumOfCaseObjects 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("decodes enum and honours @id annotation at case class level when there is no @discriminator annotation") { + 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" + ) { val item: Item = Item("enum" -> Item(Map("1" -> AttributeValue.Null))) val actual = DynamoDBQuery.fromItem[WithEnumWithoutDiscriminator](item) assert(actual)(isRight(equalTo(WithEnumWithoutDiscriminator(WithEnumWithoutDiscriminator.ONE)))) }, - test("decodes enum without @discriminator annotation and uses @id field level annotation") { + test("decodes enum without @discriminatorName annotation and uses @caseName annotation") { val item: Item = Item("enum" -> Item(Map("1" -> AttributeValue.Null))) val actual = DynamoDBQuery.fromItem[WithEnumWithoutDiscriminator](item) diff --git a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala index 1e0c273cd..66f14204c 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala @@ -8,6 +8,7 @@ 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) @@ -121,7 +122,7 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes enum with discriminator annotation") { + test("encodes enum with @discriminatorName annotation") { val expectedItem: Item = Item( Map( @@ -138,13 +139,13 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes enum with discriminator annotation and @id annotation at field level for a case class") { + test("encodes enum with @discriminatorName annotation and @fieldName annotation for a case class") { val expectedItem: Item = Item( Map( "enum" -> AttributeValue.Map( Map( - AttributeValue.String("funky_value") -> AttributeValue.String("foobar"), + AttributeValue.String("funky_field_name") -> AttributeValue.String("foobar"), AttributeValue.String("funkyDiscriminator") -> AttributeValue.String("StringValue2") ) ) @@ -155,7 +156,7 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes enum with discriminator annotation and an @id annotation on a case class") { + test("encodes enum with @discriminatorName annotation and an @caseName annotation on a case class") { val expectedItem: Item = Item( Map( @@ -172,7 +173,7 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes enum with discriminator annotation and case object as item without a @id annotation") { + test("encodes enum with @discriminatorName annotation and case object as item without a @caseName annotation") { val expectedItem: Item = Item( Map( @@ -188,7 +189,7 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes enum with discriminator annotation and case object as item with @id annotation of '2'") { + test("encodes enum with @discriminatorName annotation and case object as item with @caseName annotation of '2'") { val expectedItem: Item = Item( Map( @@ -204,7 +205,7 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes top level enum with discriminator annotation") { + test("encodes top level enum with @discriminatorName annotation") { val expectedItem: Item = Item( Map( @@ -225,27 +226,41 @@ object ItemEncoderSpec extends ZIOSpecDefault with CodecTestFixtures { assert(item)(equalTo(expectedItem)) }, - test("encodes case object only enum with @enumOfCaseObjects annotation and @id annotation of '2'") { + test("encodes case object only enum with @enumOfCaseObjects 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("encodes enum and honours @id annotation when there is no @enumOfCaseObjects annotation") { + 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") { val expectedItem: Item = Item("enum" -> Item(Map("1" -> AttributeValue.Null))) val item = DynamoDBQuery.toItem(WithEnumWithoutDiscriminator(WithEnumWithoutDiscriminator.ONE)) assert(item)(equalTo(expectedItem)) }, - test("encodes enum without @discriminator annotation and uses @id field level annotation") { + test("encodes enum without @discriminatorName annotation and uses @fieldName field level annotation") { val expectedItem: Item = Item( Map( "enum" -> AttributeValue.Map( Map( AttributeValue.String("Three") -> AttributeValue.Map( - Map(AttributeValue.String("funky_value") -> AttributeValue.String("value")) + Map(AttributeValue.String("funky_field_name") -> AttributeValue.String("value")) ) ) ) diff --git a/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala b/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala index 4a84be904..13c6b6930 100644 --- a/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala +++ b/dynamodb/src/test/scala/zio/dynamodb/codec/models.scala @@ -1,6 +1,7 @@ package zio.dynamodb.codec -import zio.dynamodb.Annotations.{ discriminator, enumOfCaseObjects, id } +import zio.dynamodb.Annotations.enumOfCaseObjects +import zio.schema.annotation.{ caseName, discriminatorName, fieldName } import zio.schema.{ DeriveSchema, Schema } import java.time.Instant @@ -41,27 +42,38 @@ final case class CaseClassOfSetOfInt(set: Set[Int]) final case class CaseClassOfTuple2(tuple2: (String, Int)) -@discriminator(name = "funkyDiscriminator") +@discriminatorName(tag = "funkyDiscriminator") sealed trait EnumWithDiscriminator final case class WithDiscriminatedEnum(enum: EnumWithDiscriminator) object WithDiscriminatedEnum { - final case class StringValue(value: String) extends EnumWithDiscriminator - final case class StringValue2(@id("funky_value") value: String) extends EnumWithDiscriminator - @id("ival") - final case class IntValue(value: Int) extends EnumWithDiscriminator - final case object ONE extends EnumWithDiscriminator - @id("2") - final case object TWO extends EnumWithDiscriminator + final case class StringValue(value: String) extends EnumWithDiscriminator + final case class StringValue2(@fieldName("funky_field_name") value: String) extends EnumWithDiscriminator + @caseName("ival") + final case class IntValue(value: Int) extends EnumWithDiscriminator + final case object ONE extends EnumWithDiscriminator + @caseName("2") + final case object TWO extends EnumWithDiscriminator 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 sealed trait CaseObjectOnlyEnum final case class WithCaseObjectOnlyEnum(enum: CaseObjectOnlyEnum) object WithCaseObjectOnlyEnum { case object ONE extends CaseObjectOnlyEnum - @id("2") + @caseName("2") case object TWO extends CaseObjectOnlyEnum implicit val schema: Schema[WithCaseObjectOnlyEnum] = DeriveSchema.gen[WithCaseObjectOnlyEnum] } @@ -69,14 +81,14 @@ object WithCaseObjectOnlyEnum { sealed trait EnumWithoutDiscriminator final case class WithEnumWithoutDiscriminator(enum: EnumWithoutDiscriminator) object WithEnumWithoutDiscriminator { - @id("1") // this should get ignored as there is no annotation at the trait level - case object ONE extends EnumWithoutDiscriminator - case object TWO extends EnumWithoutDiscriminator - case class Three(@id("funky_value") value: String) extends EnumWithoutDiscriminator + @caseName("1") + case object ONE extends EnumWithoutDiscriminator + case object TWO extends EnumWithoutDiscriminator + case class Three(@fieldName("funky_field_name") value: String) extends EnumWithoutDiscriminator implicit val schema: Schema[WithEnumWithoutDiscriminator] = DeriveSchema.gen[WithEnumWithoutDiscriminator] } -@discriminator(name = "funkyDiscriminator") +@discriminatorName(tag = "funkyDiscriminator") sealed trait Invoice { def id: Int } diff --git a/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala b/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala index cd16e0d0c..0f095b0d7 100644 --- a/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala +++ b/examples/src/main/scala/zio/dynamodb/examples/TypeSafeRoundTripSerialisationExample.scala @@ -2,7 +2,7 @@ package zio.dynamodb.examples import zio.Console.printLine import zio.ZIOAppDefault -import zio.dynamodb.Annotations.{ discriminator, enumOfCaseObjects, id } +import zio.dynamodb.Annotations.enumOfCaseObjects import zio.dynamodb.examples.TypeSafeRoundTripSerialisationExample.Invoice.{ Address, Billed, @@ -11,13 +11,14 @@ import zio.dynamodb.examples.TypeSafeRoundTripSerialisationExample.Invoice.{ Product } import zio.dynamodb.{ DynamoDBExecutor, DynamoDBQuery, PrimaryKey } +import zio.schema.annotation.{ caseName, discriminatorName } import zio.schema.{ DeriveSchema, Schema } import java.time.Instant object TypeSafeRoundTripSerialisationExample extends ZIOAppDefault { - @discriminator("invoiceType") + @discriminatorName("invoiceType") sealed trait Invoice { def id: String } @@ -25,9 +26,9 @@ object TypeSafeRoundTripSerialisationExample extends ZIOAppDefault { @enumOfCaseObjects sealed trait PaymentType object PaymentType { - @id("debit") + @caseName("debit") case object DebitCard extends PaymentType - @id("credit") + @caseName("credit") case object CreditCard extends PaymentType } diff --git a/examples/src/main/scala/zio/dynamodb/examples/dynamodblocal/TypeSafeAPIExampleWithDiscriminator.scala b/examples/src/main/scala/zio/dynamodb/examples/dynamodblocal/TypeSafeAPIExampleWithDiscriminator.scala index cf3af6afc..a2df0232f 100644 --- a/examples/src/main/scala/zio/dynamodb/examples/dynamodblocal/TypeSafeAPIExampleWithDiscriminator.scala +++ b/examples/src/main/scala/zio/dynamodb/examples/dynamodblocal/TypeSafeAPIExampleWithDiscriminator.scala @@ -1,17 +1,16 @@ package zio.dynamodb.examples.dynamodblocal -import zio.dynamodb.examples.dynamodblocal.DynamoDB._ -import zio.dynamodb.Annotations.discriminator +import zio.{ ZIO, ZIOAppDefault } import zio.dynamodb.DynamoDBQuery._ import zio.dynamodb._ +import zio.dynamodb.examples.dynamodblocal.DynamoDB._ import zio.dynamodb.examples.dynamodblocal.TypeSafeAPIExampleWithDiscriminator.TrafficLight.{ Amber, Box, Green } import zio.schema.DeriveSchema -import zio.ZIOAppDefault -import zio.ZIO +import zio.schema.annotation.discriminatorName object TypeSafeAPIExampleWithDiscriminator extends ZIOAppDefault { - @discriminator("light_type") + @discriminatorName("light_type") sealed trait TrafficLight object TrafficLight {