@@ -19,7 +19,7 @@ package zio.dynamodb.blocks.schema
1919import zio .blocks .chunk .Chunk
2020import zio .blocks .maybe .Maybe
2121import zio .blocks .schema .{ DynamicValue , Modifier , NameMapper , PrimitiveValue , Schema }
22- import zio .blocks .schema .json .Json
22+ import zio .blocks .schema .json .{ DiscriminatorKind , Json }
2323import zio .dynamodb .AttributeValue
2424import 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