Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace @enumOfCaseObject with @simpleEnum #323

Merged
merged 1 commit into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions docs/codec-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -77,15 +77,15 @@ 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")
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))`
Expand Down
1 change: 0 additions & 1 deletion dynamodb/src/main/scala/zio/dynamodb/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 11 additions & 33 deletions dynamodb/src/main/scala/zio/dynamodb/Codec.scala
Original file line number Diff line number Diff line change
@@ -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._
Expand Down Expand Up @@ -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: _*
)
Expand All @@ -290,7 +289,6 @@ private[dynamodb] object Codec {
}

private def enumWithAnnotationAtClassLevelEncoder[Z](
hasEnumOfCaseObjectsAnnotation: Boolean,
discriminator: String,
cases: Schema.Case[Z, _]*
): Encoder[Z] =
Expand All @@ -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
Expand Down Expand Up @@ -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: _*
)
Expand Down Expand Up @@ -841,7 +833,6 @@ private[dynamodb] object Codec {
}

private def enumWithAnnotationAtClassLevelDecoder[Z](
hasEnumOfCaseObjectsAnnotation: Boolean,
discriminator: String,
cases: Schema.Case[Z, _]*
): Decoder[Z] = { (av: AttributeValue) =>
Expand All @@ -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]](
Expand All @@ -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"))
}
}
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -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
Expand Down
19 changes: 2 additions & 17 deletions dynamodb/src/test/scala/zio/dynamodb/codec/ItemDecoderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not pretty sure if we need to keep these test cases as it seems they test what was tested by zio-schema.

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."
)
)
)
)
},
Comment on lines -301 to -315
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using @simpleEnum checks this in compile time.

test(
"decodes enum and honours @caseName annotation at case class level when there is no @discriminatorName annotation"
) {
Expand Down
21 changes: 3 additions & 18 deletions dynamodb/src/test/scala/zio/dynamodb/codec/ItemEncoderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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") {
Comment on lines -236 to -250
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using @simpleEnum checks this in compile time.

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))
Expand Down
16 changes: 2 additions & 14 deletions dynamodb/src/test/scala/zio/dynamodb/codec/models.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -27,7 +26,7 @@ object TypeSafeRoundTripSerialisationExample extends ZIOAppDefault {
def id: String
}
object Invoice {
@enumOfCaseObjects
@simpleEnum
sealed trait PaymentType
object PaymentType {
@caseName("debit")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Loading