Skip to content

Commit

Permalink
Replace @enumOfCaseObject with @simpleEnum (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
grouzen authored and googley42 committed Nov 25, 2023
1 parent cd2daf7 commit 456519b
Show file tree
Hide file tree
Showing 10 changed files with 32 additions and 98 deletions.
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") {
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"
) {
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") {
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

0 comments on commit 456519b

Please sign in to comment.