diff --git a/feature/dynamodb/attributevalue/decode.go b/feature/dynamodb/attributevalue/decode.go index 15a94f4b24d..9be77b9912a 100644 --- a/feature/dynamodb/attributevalue/decode.go +++ b/feature/dynamodb/attributevalue/decode.go @@ -243,6 +243,11 @@ type DecoderOptions struct { // call UnmarshalText on the target. If the attributevalue is a binary, its // value will be used to call UnmarshalBinary. UseEncodingUnmarshalers bool + + // When enabled, the decoder will call Unmarshaler.UnmarshalDynamoDBAttributeValue + // for each individual set item instead of the whole set at once. + // See issue #2895. + FixUnmarshalIndividualSetValues bool } // A Decoder provides unmarshaling AttributeValues to Go value types. @@ -447,7 +452,15 @@ func (d *Decoder) decodeBinarySet(bs [][]byte, v reflect.Value) error { } u, elem := indirect[Unmarshaler](v.Index(i), indirectOptions{}) if u != nil { - return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberBS{Value: bs}) + if d.options.FixUnmarshalIndividualSetValues { + err := u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberB{Value: bs[i]}) + if err != nil { + return err + } + continue + } else { + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberBS{Value: bs}) + } } if err := d.decodeBinary(bs[i], elem); err != nil { return err @@ -582,7 +595,15 @@ func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { } u, elem := indirect[Unmarshaler](v.Index(i), indirectOptions{}) if u != nil { - return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberNS{Value: ns}) + if d.options.FixUnmarshalIndividualSetValues { + err := u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberN{Value: ns[i]}) + if err != nil { + return err + } + continue + } else { + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberNS{Value: ns}) + } } if err := d.decodeNumber(ns[i], elem, tag{}); err != nil { return err @@ -804,7 +825,15 @@ func (d *Decoder) decodeStringSet(ss []string, v reflect.Value) error { } u, elem := indirect[Unmarshaler](v.Index(i), indirectOptions{}) if u != nil { - return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberSS{Value: ss}) + if d.options.FixUnmarshalIndividualSetValues { + err := u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberS{Value: ss[i]}) + if err != nil { + return err + } + continue + } else { + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberSS{Value: ss}) + } } if err := d.decodeString(ss[i], elem, tag{}); err != nil { return err diff --git a/feature/dynamodb/attributevalue/decode_test.go b/feature/dynamodb/attributevalue/decode_test.go index 9481ed5f7e8..c99887eacbb 100644 --- a/feature/dynamodb/attributevalue/decode_test.go +++ b/feature/dynamodb/attributevalue/decode_test.go @@ -1268,3 +1268,79 @@ func TestUnmarshalBinary(t *testing.T) { t.Errorf("expected %v, got %v", expected, actual) } } + +type testStringItem string + +func (t *testStringItem) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error { + v, ok := av.(*types.AttributeValueMemberS) + if !ok { + return fmt.Errorf("expecting string value") + } + *t = testStringItem(v.Value) + return nil +} + +type testNumberItem float64 + +func (t *testNumberItem) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error { + v, ok := av.(*types.AttributeValueMemberN) + if !ok { + return fmt.Errorf("expecting number value") + } + n, err := strconv.ParseFloat(v.Value, 64) + if err != nil { + return err + } + *t = testNumberItem(n) + return nil +} + +type testBinaryItem []byte + +func (t *testBinaryItem) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error { + v, ok := av.(*types.AttributeValueMemberB) + if !ok { + return fmt.Errorf("expecting binary value") + } + *t = make([]byte, len(v.Value)) + copy(*t, v.Value) + return nil +} + +type testStringSetWithUnmarshaler struct { + Strings []testStringItem `dynamodbav:",stringset"` + Numbers []testNumberItem `dynamodbav:",numberset"` + Binaries []testBinaryItem `dynamodbav:",binaryset"` +} + +func TestUnmarshalIndividualSetValues(t *testing.T) { + in := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Strings": &types.AttributeValueMemberSS{ + Value: []string{"a", "b"}, + }, + "Numbers": &types.AttributeValueMemberNS{ + Value: []string{"1", "2"}, + }, + "Binaries": &types.AttributeValueMemberBS{ + Value: [][]byte{{1, 2}, {3, 4}}, + }, + }, + } + var actual testStringSetWithUnmarshaler + err := UnmarshalWithOptions(in, &actual, func(o *DecoderOptions) { + o.FixUnmarshalIndividualSetValues = true + }) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + expected := testStringSetWithUnmarshaler{ + Strings: []testStringItem{"a", "b"}, + Numbers: []testNumberItem{1, 2}, + Binaries: []testBinaryItem{{1, 2}, {3, 4}}, + } + if diff := cmpDiff(expected, actual); diff != "" { + t.Errorf("expect value match\n%s", diff) + } +} diff --git a/feature/dynamodbstreams/attributevalue/decode.go b/feature/dynamodbstreams/attributevalue/decode.go index 35021869967..f8ddfecd09c 100644 --- a/feature/dynamodbstreams/attributevalue/decode.go +++ b/feature/dynamodbstreams/attributevalue/decode.go @@ -243,6 +243,11 @@ type DecoderOptions struct { // call UnmarshalText on the target. If the attributevalue is a binary, its // value will be used to call UnmarshalBinary. UseEncodingUnmarshalers bool + + // When enabled, the decoder will call Unmarshaler.UnmarshalDynamoDBStreamsAttributeValue + // for each individual set item instead of the whole set at once. + // See issue #2895. + FixUnmarshalIndividualSetValues bool } // A Decoder provides unmarshaling AttributeValues to Go value types. @@ -447,7 +452,15 @@ func (d *Decoder) decodeBinarySet(bs [][]byte, v reflect.Value) error { } u, elem := indirect[Unmarshaler](v.Index(i), indirectOptions{}) if u != nil { - return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberBS{Value: bs}) + if d.options.FixUnmarshalIndividualSetValues { + err := u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberB{Value: bs[i]}) + if err != nil { + return err + } + continue + } else { + return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberBS{Value: bs}) + } } if err := d.decodeBinary(bs[i], elem); err != nil { return err @@ -582,7 +595,15 @@ func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { } u, elem := indirect[Unmarshaler](v.Index(i), indirectOptions{}) if u != nil { - return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberNS{Value: ns}) + if d.options.FixUnmarshalIndividualSetValues { + err := u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberN{Value: ns[i]}) + if err != nil { + return err + } + continue + } else { + return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberNS{Value: ns}) + } } if err := d.decodeNumber(ns[i], elem, tag{}); err != nil { return err @@ -804,7 +825,15 @@ func (d *Decoder) decodeStringSet(ss []string, v reflect.Value) error { } u, elem := indirect[Unmarshaler](v.Index(i), indirectOptions{}) if u != nil { - return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberSS{Value: ss}) + if d.options.FixUnmarshalIndividualSetValues { + err := u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberS{Value: ss[i]}) + if err != nil { + return err + } + continue + } else { + return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberSS{Value: ss}) + } } if err := d.decodeString(ss[i], elem, tag{}); err != nil { return err diff --git a/feature/dynamodbstreams/attributevalue/decode_test.go b/feature/dynamodbstreams/attributevalue/decode_test.go index 1bccef17abb..666fdf1c004 100644 --- a/feature/dynamodbstreams/attributevalue/decode_test.go +++ b/feature/dynamodbstreams/attributevalue/decode_test.go @@ -1268,3 +1268,79 @@ func TestUnmarshalBinary(t *testing.T) { t.Errorf("expected %v, got %v", expected, actual) } } + +type testStringItem string + +func (t *testStringItem) UnmarshalDynamoDBStreasmAttributeValue(av types.AttributeValue) error { + v, ok := av.(*types.AttributeValueMemberS) + if !ok { + return fmt.Errorf("expecting string value") + } + *t = testStringItem(v.Value) + return nil +} + +type testNumberItem float64 + +func (t *testNumberItem) UnmarshalDynamoDBStreamsAttributeValue(av types.AttributeValue) error { + v, ok := av.(*types.AttributeValueMemberN) + if !ok { + return fmt.Errorf("expecting number value") + } + n, err := strconv.ParseFloat(v.Value, 64) + if err != nil { + return err + } + *t = testNumberItem(n) + return nil +} + +type testBinaryItem []byte + +func (t *testBinaryItem) UnmarshalDynamoDBStreamsAttributeValue(av types.AttributeValue) error { + v, ok := av.(*types.AttributeValueMemberB) + if !ok { + return fmt.Errorf("expecting binary value") + } + *t = make([]byte, len(v.Value)) + copy(*t, v.Value) + return nil +} + +type testStringSetWithUnmarshaler struct { + Strings []testStringItem `dynamodbav:",stringset"` + Numbers []testNumberItem `dynamodbav:",numberset"` + Binaries []testBinaryItem `dynamodbav:",binaryset"` +} + +func TestUnmarshalIndividualSetValues(t *testing.T) { + in := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Strings": &types.AttributeValueMemberSS{ + Value: []string{"a", "b"}, + }, + "Numbers": &types.AttributeValueMemberNS{ + Value: []string{"1", "2"}, + }, + "Binaries": &types.AttributeValueMemberBS{ + Value: [][]byte{{1, 2}, {3, 4}}, + }, + }, + } + var actual testStringSetWithUnmarshaler + err := UnmarshalWithOptions(in, &actual, func(o *DecoderOptions) { + o.FixUnmarshalIndividualSetValues = true + }) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + expected := testStringSetWithUnmarshaler{ + Strings: []testStringItem{"a", "b"}, + Numbers: []testNumberItem{1, 2}, + Binaries: []testBinaryItem{{1, 2}, {3, 4}}, + } + if diff := cmpDiff(expected, actual); diff != "" { + t.Errorf("expect value match\n%s", diff) + } +}