Skip to content

Commit 20f5bdd

Browse files
authored
3.x improve codec coverage (#895)
1 parent 403583b commit 20f5bdd

1 file changed

Lines changed: 150 additions & 4 deletions

File tree

schema-dynamodb/src/test/scala/zio/dynamodb/blocks/schema/DynamoDBCodecDeriverSpec.scala

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package zio.dynamodb.blocks.schema
1919
import zio.blocks.chunk.Chunk
2020
import zio.blocks.maybe.Maybe
2121
import zio.blocks.schema.{ DynamicValue, Modifier, NameMapper, PrimitiveValue, Schema }
22-
import zio.blocks.schema.json.Json
22+
import zio.blocks.schema.json.{ DiscriminatorKind, Json }
2323
import zio.dynamodb.AttributeValue
2424
import zio.test._
2525

@@ -111,6 +111,20 @@ object DynamoDBCodecDeriverSpec extends ZIOSpecDefault {
111111
case class WithDefaultField(name: String, rank: Int = 99)
112112
object WithDefaultField { implicit val schema: Schema[WithDefaultField] = Schema.derived }
113113

114+
// covers every primitive register type's default-value handling
115+
// (FieldInfo.defaultEqualsValue / setMissingValueOrDefault), not just Int/String
116+
case class WithPrimitiveDefaults(
117+
id: String,
118+
longVal: Long = 100L,
119+
boolVal: Boolean = true,
120+
byteVal: Byte = 1,
121+
charVal: Char = 'x',
122+
shortVal: Short = 10,
123+
floatVal: Float = 1.5f,
124+
doubleVal: Double = 2.5
125+
)
126+
object WithPrimitiveDefaults { implicit val schema: Schema[WithPrimitiveDefaults] = Schema.derived }
127+
114128
case class WithMaybe(id: String, value: Maybe[String])
115129
object WithMaybe { implicit val schema: Schema[WithMaybe] = Schema.derived }
116130

@@ -214,18 +228,21 @@ object DynamoDBCodecDeriverSpec extends ZIOSpecDefault {
214228
optionSuite,
215229
enumSuite,
216230
adtSuite,
231+
adtNoneDiscriminatorSuite,
217232
adtDiscriminatorFieldSuite,
218233
adtCaseNamingSuite,
219234
adtDiscriminatorAndCaseNamingSuite,
220235
enumCaseNamingSuite,
221236
encodeTransientSuite,
222237
transientDefaultValueSuite,
223238
defaultValueDecodeSuite,
239+
primitiveDefaultsSuite,
224240
emptyCollectionSuite,
225241
maybeSuite,
226242
sequenceSuite,
227243
nativeSetSuite,
228244
mapSuite,
245+
nonNativeMapSuite,
229246
dynamicValueSuite,
230247
jsonAVSuite,
231248
byteSequenceCompatSuite,
@@ -779,6 +796,45 @@ object DynamoDBCodecDeriverSpec extends ZIOSpecDefault {
779796
}
780797
)
781798

799+
// ---- ADT with DiscriminatorKind.None (decode by trying each case) -------
800+
//
801+
// No discriminator key/field is written; decoding tries each case's codec
802+
// in turn and takes the first one that succeeds. Scala 3 union types are a
803+
// more common use of this mode (see DynamoDBCodecDeriverVersionSpecificSpec),
804+
// but the underlying derivation is generic over any variant, so a plain
805+
// sealed trait exercises the same code path cross-version.
806+
807+
private val noneDiscriminatorDeriver = DynamoDBCodecDeriver.withDiscriminatorKind(DiscriminatorKind.None)
808+
809+
private def noneDiscriminatorCodecFor[A](implicit s: Schema[A]): DynamoDBCodec[A] =
810+
s.deriving(noneDiscriminatorDeriver).derive
811+
812+
private val adtNoneDiscriminatorSuite = suite("ADT (DiscriminatorKind.None) codec")(
813+
test("round-trips Circle without writing a discriminator") {
814+
val codec = noneDiscriminatorCodecFor[Shape]
815+
val value: Shape = Shape.Circle(5)
816+
assertTrue(codec.decoder(codec.encoder(value)) == Right(value))
817+
},
818+
test("round-trips Rect without writing a discriminator") {
819+
val codec = noneDiscriminatorCodecFor[Shape]
820+
val value: Shape = Shape.Rect(10, 20)
821+
assertTrue(codec.decoder(codec.encoder(value)) == Right(value))
822+
},
823+
test("encoded Circle carries no case-name key, unlike DiscriminatorKind.Key") {
824+
val codec = noneDiscriminatorCodecFor[Shape]
825+
codec.encoder(Shape.Circle(5)) match {
826+
case m: AttributeValue.Map =>
827+
val keys = m.value.keys.map(_.value).toList
828+
assertTrue(!keys.contains("Circle"))
829+
case _ => assertTrue(false)
830+
}
831+
},
832+
test("decode error when no case codec matches") {
833+
val codec = noneDiscriminatorCodecFor[Shape]
834+
assertTrue(codec.decoder(AttributeValue.String("nope")).isLeft)
835+
}
836+
)
837+
782838
// ---- @Modifier.discriminator -------------------------------------------
783839

784840
private val adtDiscriminatorFieldSuite =
@@ -1063,6 +1119,44 @@ object DynamoDBCodecDeriverSpec extends ZIOSpecDefault {
10631119
}
10641120
)
10651121

1122+
// ---- default-value handling across every primitive register type --------
1123+
//
1124+
// The suites above only exercise Int/String defaults. FieldInfo dispatches
1125+
// on register type for both defaultEqualsValue (encode-side omission check)
1126+
// and setMissingValueOrDefault (decode-side fallback) with a case per
1127+
// primitive type, so cover Long/Boolean/Byte/Char/Short/Float/Double too.
1128+
1129+
private val primitiveDefaultsSuite = suite("default-value handling for every primitive type")(
1130+
test("all primitive-default fields are omitted when equal to their defaults") {
1131+
val deriver = DynamoDBCodecDeriver.withTransientDefaultValue(transientDefaultValue = true)
1132+
val codec = WithPrimitiveDefaults.schema.deriving(deriver).derive
1133+
codec.encoder(WithPrimitiveDefaults("a")) match {
1134+
case m: AttributeValue.Map =>
1135+
val keys = m.value.keys.map(_.value).toSet
1136+
assertTrue(keys == Set("id"))
1137+
case _ => assertTrue(false)
1138+
}
1139+
},
1140+
test("all primitive-default fields are written when not at their defaults") {
1141+
val deriver = DynamoDBCodecDeriver.withTransientDefaultValue(transientDefaultValue = true)
1142+
val codec = WithPrimitiveDefaults.schema.deriving(deriver).derive
1143+
val value = WithPrimitiveDefaults("a", 200L, false, 2, 'y', 20, 9.5f, 8.5)
1144+
codec.encoder(value) match {
1145+
case m: AttributeValue.Map =>
1146+
val keys = m.value.keys.map(_.value).toSet
1147+
assertTrue(
1148+
keys == Set("id", "longVal", "boolVal", "byteVal", "charVal", "shortVal", "floatVal", "doubleVal")
1149+
)
1150+
case _ => assertTrue(false)
1151+
}
1152+
},
1153+
test("decoding an item missing every primitive-default field uses each schema default") {
1154+
val codec = codecFor[WithPrimitiveDefaults]
1155+
val stored = AttributeValue.Map(Map(AttributeValue.String("id") -> AttributeValue.String("a")))
1156+
assertTrue(codec.decoder(stored) == Right(WithPrimitiveDefaults("a")))
1157+
}
1158+
)
1159+
10661160
// ---- emptyCollectionConstructor ----------------------------------------
10671161

10681162
private val collectionsCodec = codecFor[WithCollections]
@@ -1310,6 +1404,51 @@ object DynamoDBCodecDeriverSpec extends ZIOSpecDefault {
13101404
}
13111405
)
13121406

1407+
// ---- non-native map codec (non-String keys, encoded as List of [k, v]) --
1408+
//
1409+
// isNativeMap requires the key type to be a primitive String; any other key
1410+
// type (Int here) falls back to a Sequence-of-tuple2 encoding instead of a
1411+
// native AttributeValue.Map.
1412+
1413+
private val nonNativeMapSuite = suite("map codec (non-String key, Sequence-of-tuple2 encoding)")(
1414+
test("Map[Int, String] encodes as AttributeValue.List of 2-element [key, value] lists") {
1415+
val codec = codecFor[Map[Int, String]]
1416+
codec.encoder(Map(1 -> "a")) match {
1417+
case AttributeValue.List(entries) =>
1418+
entries.toList match {
1419+
case AttributeValue.List(kv) :: Nil =>
1420+
assertTrue(kv.toList == List(AttributeValue.Number(BigDecimal(1)), AttributeValue.String("a")))
1421+
case _ => assertTrue(false)
1422+
}
1423+
case _ => assertTrue(false)
1424+
}
1425+
},
1426+
test("Map[Int, String] round-trips") {
1427+
assertTrue(roundTrip(Map(1 -> "a", 2 -> "b")) == Right(Map(1 -> "a", 2 -> "b")))
1428+
},
1429+
test("empty Map[Int, String] round-trips") {
1430+
assertTrue(roundTrip(Map.empty[Int, String]) == Right(Map.empty[Int, String]))
1431+
},
1432+
test("decode error for non-List attribute value") {
1433+
val codec = codecFor[Map[Int, String]]
1434+
assertTrue(codec.decoder(AttributeValue.String("nope")).isLeft)
1435+
},
1436+
test("decode error for a malformed entry (not a 2-element list)") {
1437+
val codec = codecFor[Map[Int, String]]
1438+
val malformed = AttributeValue.List(
1439+
List(AttributeValue.List(List(AttributeValue.Number(BigDecimal(1)))))
1440+
)
1441+
assertTrue(codec.decoder(malformed).isLeft)
1442+
},
1443+
test("decode error when the key can't be decoded") {
1444+
val codec = codecFor[Map[Int, String]]
1445+
val malformed = AttributeValue.List(
1446+
List(AttributeValue.List(List(AttributeValue.String("not-a-number"), AttributeValue.String("a"))))
1447+
)
1448+
assertTrue(codec.decoder(malformed).isLeft)
1449+
}
1450+
)
1451+
13131452
// ---- DynamicValue codec ------------------------------------------------
13141453

13151454
private val dvCodec = DynamoDBCodecDeriver.dynamicValueCodec
@@ -1904,9 +2043,11 @@ object DynamoDBCodecDeriverSpec extends ZIOSpecDefault {
19042043
// put a project-local given/implicit Schema there; unlike case classes in
19052044
// this suite, we define explicit Schema.derived instances for each tuple
19062045
// arity used below.
1907-
private implicit val tuple2Schema: Schema[(String, Int)] = Schema.derived
1908-
private implicit val tuple3Schema: Schema[(String, Int, Boolean)] = Schema.derived
1909-
private implicit val tuple4Schema: Schema[(String, Int, Boolean, String)] = Schema.derived
2046+
private implicit val tuple2Schema: Schema[(String, Int)] = Schema.derived
2047+
private implicit val tuple3Schema: Schema[(String, Int, Boolean)] = Schema.derived
2048+
private implicit val tuple4Schema: Schema[(String, Int, Boolean, String)] = Schema.derived
2049+
private implicit val tuplePrimitivesSchema: Schema[(Int, Long, Boolean, Byte, Char, Short, Float, Double)] =
2050+
Schema.derived
19102051

19112052
private val tupleCompatOldDeriver =
19122053
DynamoDBCodecDeriver.withSchema1TupleCompatibility(Schema1Compat.ReadBothWriteOld)
@@ -2000,6 +2141,11 @@ object DynamoDBCodecDeriverSpec extends ZIOSpecDefault {
20002141
test("non-List attribute value yields a decode error") {
20012142
val codec = codecFor[(String, Int)]
20022143
assertTrue(codec.decoder(AttributeValue.String("not-a-list")).isLeft)
2144+
},
2145+
test("round-trips a tuple covering every primitive register type (Int/Long/Boolean/Byte/Char/Short/Float/Double)") {
2146+
val codec = codecFor[(Int, Long, Boolean, Byte, Char, Short, Float, Double)]
2147+
val value = (1, 2L, true, 3.toByte, 'c', 4.toShort, 5.5f, 6.6)
2148+
assertTrue(codec.decoder(codec.encoder(value)) == Right(value))
20032149
}
20042150
)
20052151

0 commit comments

Comments
 (0)