package attributevalue import ( "fmt" "reflect" "strings" "testing" "time" smithydocument "github.com/aws/smithy-go/document" "github.com/google/go-cmp/cmp/cmpopts" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" "github.com/google/go-cmp/cmp" ) type testTextMarshaler struct { Foo string } func (t *testTextMarshaler) UnmarshalText(b []byte) error { if !strings.HasPrefix(string(b), "Foo:") { return fmt.Errorf(`missing "Foo:" prefix`) } t.Foo = string(b)[len("Foo:"):] return nil } func (t testTextMarshaler) MarshalText() ([]byte, error) { return []byte("Foo:" + t.Foo), nil } type testBinarySetStruct struct { Binarys [][]byte `dynamodbav:",binaryset"` } type testNumberSetStruct struct { Numbers []int `dynamodbav:",numberset"` } type testStringSetStruct struct { Strings []string `dynamodbav:",stringset"` } type testIntAsStringStruct struct { Value int `dynamodbav:",string"` } type testOmitEmptyStruct struct { Value string `dynamodbav:",omitempty"` Value2 *string `dynamodbav:",omitempty"` Value3 int } type testAliasedString string type testAliasedStringSlice []string type testAliasedInt int type testAliasedIntSlice []int type testAliasedMap map[string]int type testAliasedSlice []string type testAliasedByteSlice []byte type testAliasedBool bool type testAliasedBoolSlice []bool type testAliasedStruct struct { Value testAliasedString Value2 testAliasedInt Value3 testAliasedMap Value4 testAliasedSlice Value5 testAliasedByteSlice Value6 []testAliasedInt Value7 []testAliasedString Value8 []testAliasedByteSlice `dynamodbav:",binaryset"` Value9 []testAliasedInt `dynamodbav:",numberset"` Value10 []testAliasedString `dynamodbav:",stringset"` Value11 testAliasedIntSlice Value12 testAliasedStringSlice Value13 testAliasedBool Value14 testAliasedBoolSlice Value15 map[testAliasedString]string } type testNamedPointer *int var testDate, _ = time.Parse(time.RFC3339, "2016-05-03T17:06:26.209072Z") var sharedTestCases = map[string]struct { in types.AttributeValue actual, expected interface{} err error }{ "binary slice": { in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, actual: &[]byte{}, expected: []byte{48, 49}, }, "Binary slice oversized": { in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, actual: func() *[]byte { v := make([]byte, 0, 10) return &v }(), expected: []byte{48, 49}, }, "binary slice pointer": { in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, actual: func() **[]byte { v := make([]byte, 0, 10) v2 := &v return &v2 }(), expected: []byte{48, 49}, }, "bool": { in: &types.AttributeValueMemberBOOL{Value: true}, actual: new(bool), expected: true, }, "list": { in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberN{Value: "123"}, }}, actual: &[]int{}, expected: []int{123}, }, "map, interface": { in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ "abc": &types.AttributeValueMemberN{Value: "123"}, }}, actual: &map[string]int{}, expected: map[string]int{"abc": 123}, }, "map, struct": { in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ "Abc": &types.AttributeValueMemberN{Value: "123"}, }}, actual: &struct{ Abc int }{}, expected: struct{ Abc int }{Abc: 123}, }, "map, struct with tags": { in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ "abc": &types.AttributeValueMemberN{Value: "123"}, }}, actual: &struct { Abc int `json:"abc" dynamodbav:"abc"` }{}, expected: struct { Abc int `json:"abc" dynamodbav:"abc"` }{Abc: 123}, }, "number, int": { in: &types.AttributeValueMemberN{Value: "123"}, actual: new(int), expected: 123, }, "number, Float": { in: &types.AttributeValueMemberN{Value: "123.1"}, actual: new(float64), expected: float64(123.1), }, "null ptr": { in: &types.AttributeValueMemberNULL{Value: true}, actual: new(*string), expected: nil, }, "string": { in: &types.AttributeValueMemberS{Value: "abc"}, actual: new(string), expected: "abc", }, "empty string": { in: &types.AttributeValueMemberS{Value: ""}, actual: new(string), expected: "", }, "binary Set": { in: &types.AttributeValueMemberM{ Value: map[string]types.AttributeValue{ "Binarys": &types.AttributeValueMemberBS{Value: [][]byte{{48, 49}, {50, 51}}}, }, }, actual: &testBinarySetStruct{}, expected: testBinarySetStruct{Binarys: [][]byte{{48, 49}, {50, 51}}}, }, "number Set": { in: &types.AttributeValueMemberM{ Value: map[string]types.AttributeValue{ "Numbers": &types.AttributeValueMemberNS{Value: []string{"123", "321"}}, }, }, actual: &testNumberSetStruct{}, expected: testNumberSetStruct{Numbers: []int{123, 321}}, }, "string Set": { in: &types.AttributeValueMemberM{ Value: map[string]types.AttributeValue{ "Strings": &types.AttributeValueMemberSS{Value: []string{"abc", "efg"}}, }, }, actual: &testStringSetStruct{}, expected: testStringSetStruct{Strings: []string{"abc", "efg"}}, }, "int value as string": { in: &types.AttributeValueMemberM{ Value: map[string]types.AttributeValue{ "Value": &types.AttributeValueMemberS{Value: "123"}, }, }, actual: &testIntAsStringStruct{}, expected: testIntAsStringStruct{Value: 123}, }, "omitempty": { in: &types.AttributeValueMemberM{ Value: map[string]types.AttributeValue{ "Value3": &types.AttributeValueMemberN{Value: "0"}, }, }, actual: &testOmitEmptyStruct{}, expected: testOmitEmptyStruct{Value: "", Value2: nil, Value3: 0}, }, "aliased type": { in: &types.AttributeValueMemberM{ Value: map[string]types.AttributeValue{ "Value": &types.AttributeValueMemberS{Value: "123"}, "Value2": &types.AttributeValueMemberN{Value: "123"}, "Value3": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ "Key": &types.AttributeValueMemberN{Value: "321"}, }}, "Value4": &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberS{Value: "1"}, &types.AttributeValueMemberS{Value: "2"}, &types.AttributeValueMemberS{Value: "3"}, }}, "Value5": &types.AttributeValueMemberB{Value: []byte{0, 1, 2}}, "Value6": &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberN{Value: "1"}, &types.AttributeValueMemberN{Value: "2"}, &types.AttributeValueMemberN{Value: "3"}, }}, "Value7": &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberS{Value: "1"}, &types.AttributeValueMemberS{Value: "2"}, &types.AttributeValueMemberS{Value: "3"}, }}, "Value8": &types.AttributeValueMemberBS{Value: [][]byte{ {0, 1, 2}, {3, 4, 5}, }}, "Value9": &types.AttributeValueMemberNS{Value: []string{ "1", "2", "3", }}, "Value10": &types.AttributeValueMemberSS{Value: []string{ "1", "2", "3", }}, "Value11": &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberN{Value: "1"}, &types.AttributeValueMemberN{Value: "2"}, &types.AttributeValueMemberN{Value: "3"}, }}, "Value12": &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberS{Value: "1"}, &types.AttributeValueMemberS{Value: "2"}, &types.AttributeValueMemberS{Value: "3"}, }}, "Value13": &types.AttributeValueMemberBOOL{Value: true}, "Value14": &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberBOOL{Value: true}, &types.AttributeValueMemberBOOL{Value: false}, &types.AttributeValueMemberBOOL{Value: true}, }}, "Value15": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ "TestKey": &types.AttributeValueMemberS{Value: "TestElement"}, }}, }, }, actual: &testAliasedStruct{}, expected: testAliasedStruct{ Value: "123", Value2: 123, Value3: testAliasedMap{ "Key": 321, }, Value4: testAliasedSlice{"1", "2", "3"}, Value5: testAliasedByteSlice{0, 1, 2}, Value6: []testAliasedInt{1, 2, 3}, Value7: []testAliasedString{"1", "2", "3"}, Value8: []testAliasedByteSlice{ {0, 1, 2}, {3, 4, 5}, }, Value9: []testAliasedInt{1, 2, 3}, Value10: []testAliasedString{"1", "2", "3"}, Value11: testAliasedIntSlice{1, 2, 3}, Value12: testAliasedStringSlice{"1", "2", "3"}, Value13: true, Value14: testAliasedBoolSlice{true, false, true}, Value15: map[testAliasedString]string{"TestKey": "TestElement"}, }, }, "number named pointer": { in: &types.AttributeValueMemberN{Value: "123"}, actual: new(testNamedPointer), expected: testNamedPointer(aws.Int(123)), }, "time.Time": { in: &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, actual: new(time.Time), expected: testDate, }, "time.Time List": { in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, &types.AttributeValueMemberS{Value: "2016-05-04T17:06:26.209072Z"}, }}, actual: new([]time.Time), expected: []time.Time{testDate, testDate.Add(24 * time.Hour)}, }, "time.Time struct": { in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ "abc": &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, }}, actual: &struct { Abc time.Time `json:"abc" dynamodbav:"abc"` }{}, expected: struct { Abc time.Time `json:"abc" dynamodbav:"abc"` }{Abc: testDate}, }, "time.Time ptr struct": { in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ "abc": &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, }}, actual: &struct { Abc *time.Time `json:"abc" dynamodbav:"abc"` }{}, expected: struct { Abc *time.Time `json:"abc" dynamodbav:"abc"` }{Abc: &testDate}, }, } var sharedListTestCases = map[string]struct { in []types.AttributeValue actual, expected interface{} err error }{ "union members": { in: []types.AttributeValue{ &types.AttributeValueMemberB{Value: []byte{48, 49}}, &types.AttributeValueMemberBOOL{Value: true}, &types.AttributeValueMemberN{Value: "123"}, &types.AttributeValueMemberS{Value: "123"}, }, actual: func() *[]interface{} { v := []interface{}{} return &v }(), expected: []interface{}{[]byte{48, 49}, true, 123., "123"}, }, "numbers": { in: []types.AttributeValue{ &types.AttributeValueMemberN{Value: "1"}, &types.AttributeValueMemberN{Value: "2"}, &types.AttributeValueMemberN{Value: "3"}, }, actual: &[]interface{}{}, expected: []interface{}{1., 2., 3.}, }, } var sharedMapTestCases = map[string]struct { in map[string]types.AttributeValue actual, expected interface{} err error }{ "union members": { in: map[string]types.AttributeValue{ "B": &types.AttributeValueMemberB{Value: []byte{48, 49}}, "BOOL": &types.AttributeValueMemberBOOL{Value: true}, "N": &types.AttributeValueMemberN{Value: "123"}, "S": &types.AttributeValueMemberS{Value: "123"}, }, actual: &map[string]interface{}{}, expected: map[string]interface{}{ "B": []byte{48, 49}, "BOOL": true, "N": 123., "S": "123", }, }, } func assertConvertTest(t *testing.T, actual, expected interface{}, err, expectedErr error) { t.Helper() if expectedErr != nil { if err != nil { if e, a := expectedErr, err; !strings.Contains(a.Error(), e.Error()) { t.Errorf("expect %v, got %v", e, a) } } else { t.Fatalf("expected error, %v", expectedErr) } } else if err != nil { t.Fatalf("expect no error, got %v", err) } else { if diff := cmp.Diff(ptrToValue(expected), ptrToValue(actual), cmpopts.IgnoreTypes(smithydocument.NoSerde{})); len(diff) != 0 { t.Errorf("expect match\n%s", diff) } } } func ptrToValue(in interface{}) interface{} { v := reflect.ValueOf(in) if v.Kind() == reflect.Ptr { v = v.Elem() } if !v.IsValid() { return nil } if v.Kind() == reflect.Ptr { return ptrToValue(v.Interface()) } return v.Interface() } func getIgnoreAVUnexportedOptions() cmp.Options { return cmp.Options{ cmpopts.IgnoreUnexported(types.AttributeValueMemberM{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberN{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberNS{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberBOOL{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberB{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberBS{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberL{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberS{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberSS{}), cmpopts.IgnoreUnexported(types.AttributeValueMemberNULL{}), } }