1package dynamodbattribute
2
3import (
4	"reflect"
5	"strings"
6	"testing"
7	"time"
8
9	"github.com/aws/aws-sdk-go/aws"
10	"github.com/aws/aws-sdk-go/service/dynamodb"
11)
12
13type testBinarySetStruct struct {
14	Binarys [][]byte `dynamodbav:",binaryset"`
15}
16type testNumberSetStruct struct {
17	Numbers []int `dynamodbav:",numberset"`
18}
19type testStringSetStruct struct {
20	Strings []string `dynamodbav:",stringset"`
21}
22
23type testIntAsStringStruct struct {
24	Value int `dynamodbav:",string"`
25}
26
27type testOmitEmptyStruct struct {
28	Value  string  `dynamodbav:",omitempty"`
29	Value2 *string `dynamodbav:",omitempty"`
30	Value3 int
31}
32
33type testAliasedString string
34type testAliasedStringSlice []string
35type testAliasedInt int
36type testAliasedIntSlice []int
37type testAliasedMap map[string]int
38type testAliasedSlice []string
39type testAliasedByteSlice []byte
40type testAliasedBool bool
41type testAliasedBoolSlice []bool
42
43type testAliasedStruct struct {
44	Value  testAliasedString
45	Value2 testAliasedInt
46	Value3 testAliasedMap
47	Value4 testAliasedSlice
48
49	Value5 testAliasedByteSlice
50	Value6 []testAliasedInt
51	Value7 []testAliasedString
52
53	Value8  []testAliasedByteSlice `dynamodbav:",binaryset"`
54	Value9  []testAliasedInt       `dynamodbav:",numberset"`
55	Value10 []testAliasedString    `dynamodbav:",stringset"`
56
57	Value11 testAliasedIntSlice
58	Value12 testAliasedStringSlice
59
60	Value13 testAliasedBool
61	Value14 testAliasedBoolSlice
62
63	Value15 map[testAliasedString]string
64}
65
66type testNamedPointer *int
67
68var testDate, _ = time.Parse(time.RFC3339, "2016-05-03T17:06:26.209072Z")
69
70var sharedTestCases = []struct {
71	in                   *dynamodb.AttributeValue
72	actual, expected     interface{}
73	encoderOpts          func(encoder *Encoder)
74	enableEmptyString    bool
75	enableEmptyByteSlice bool
76	err                  error
77}{
78	0: { // Binary slice
79		in:       &dynamodb.AttributeValue{B: []byte{48, 49}},
80		actual:   &[]byte{},
81		expected: []byte{48, 49},
82	},
83	1: { // Binary slice oversized
84		in: &dynamodb.AttributeValue{B: []byte{48, 49}},
85		actual: func() *[]byte {
86			v := make([]byte, 0, 10)
87			return &v
88		}(),
89		expected: []byte{48, 49},
90	},
91	2: { // Binary slice pointer
92		in: &dynamodb.AttributeValue{B: []byte{48, 49}},
93		actual: func() **[]byte {
94			v := make([]byte, 0, 10)
95			v2 := &v
96			return &v2
97		}(),
98		expected: []byte{48, 49},
99	},
100	3: { // Bool
101		in:       &dynamodb.AttributeValue{BOOL: aws.Bool(true)},
102		actual:   new(bool),
103		expected: true,
104	},
105	4: { // List
106		in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
107			{N: aws.String("123")},
108		}},
109		actual:   &[]int{},
110		expected: []int{123},
111	},
112	5: { // Map, interface
113		in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
114			"abc": {N: aws.String("123")},
115		}},
116		actual:   &map[string]int{},
117		expected: map[string]int{"abc": 123},
118	},
119	6: { // Map, struct
120		in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
121			"Abc": {N: aws.String("123")},
122		}},
123		actual:   &struct{ Abc int }{},
124		expected: struct{ Abc int }{Abc: 123},
125	},
126	7: { // Map, struct
127		in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
128			"abc": {N: aws.String("123")},
129		}},
130		actual: &struct {
131			Abc int `json:"abc" dynamodbav:"abc"`
132		}{},
133		expected: struct {
134			Abc int `json:"abc" dynamodbav:"abc"`
135		}{Abc: 123},
136	},
137	8: { // Number, int
138		in:       &dynamodb.AttributeValue{N: aws.String("123")},
139		actual:   new(int),
140		expected: 123,
141	},
142	9: { // Number, Float
143		in:       &dynamodb.AttributeValue{N: aws.String("123.1")},
144		actual:   new(float64),
145		expected: float64(123.1),
146	},
147	10: { // Null string
148		in:       &dynamodb.AttributeValue{NULL: aws.Bool(true)},
149		actual:   new(string),
150		expected: "",
151	},
152	11: { // Null ptr string
153		in:       &dynamodb.AttributeValue{NULL: aws.Bool(true)},
154		actual:   new(*string),
155		expected: nil,
156	},
157	12: { // Non-Empty String
158		in:       &dynamodb.AttributeValue{S: aws.String("abc")},
159		actual:   new(string),
160		expected: "abc",
161	},
162	13: { // Binary Set
163		in: &dynamodb.AttributeValue{
164			M: map[string]*dynamodb.AttributeValue{
165				"Binarys": {BS: [][]byte{{48, 49}, {50, 51}}},
166			},
167		},
168		actual:   &testBinarySetStruct{},
169		expected: testBinarySetStruct{Binarys: [][]byte{{48, 49}, {50, 51}}},
170	},
171	14: { // Number Set
172		in: &dynamodb.AttributeValue{
173			M: map[string]*dynamodb.AttributeValue{
174				"Numbers": {NS: []*string{aws.String("123"), aws.String("321")}},
175			},
176		},
177		actual:   &testNumberSetStruct{},
178		expected: testNumberSetStruct{Numbers: []int{123, 321}},
179	},
180	15: { // String Set
181		in: &dynamodb.AttributeValue{
182			M: map[string]*dynamodb.AttributeValue{
183				"Strings": {SS: []*string{aws.String("abc"), aws.String("efg")}},
184			},
185		},
186		actual:   &testStringSetStruct{},
187		expected: testStringSetStruct{Strings: []string{"abc", "efg"}},
188	},
189	16: { // Int value as string
190		in: &dynamodb.AttributeValue{
191			M: map[string]*dynamodb.AttributeValue{
192				"Value": {S: aws.String("123")},
193			},
194		},
195		actual:   &testIntAsStringStruct{},
196		expected: testIntAsStringStruct{Value: 123},
197	},
198	17: { // Omitempty
199		in: &dynamodb.AttributeValue{
200			M: map[string]*dynamodb.AttributeValue{
201				"Value3": {N: aws.String("0")},
202			},
203		},
204		actual:   &testOmitEmptyStruct{},
205		expected: testOmitEmptyStruct{Value: "", Value2: nil, Value3: 0},
206	},
207	18: { // aliased type
208		in: &dynamodb.AttributeValue{
209			M: map[string]*dynamodb.AttributeValue{
210				"Value":  {S: aws.String("123")},
211				"Value2": {N: aws.String("123")},
212				"Value3": {M: map[string]*dynamodb.AttributeValue{
213					"Key": {N: aws.String("321")},
214				}},
215				"Value4": {L: []*dynamodb.AttributeValue{
216					{S: aws.String("1")},
217					{S: aws.String("2")},
218					{S: aws.String("3")},
219				}},
220				"Value5": {B: []byte{0, 1, 2}},
221				"Value6": {L: []*dynamodb.AttributeValue{
222					{N: aws.String("1")},
223					{N: aws.String("2")},
224					{N: aws.String("3")},
225				}},
226				"Value7": {L: []*dynamodb.AttributeValue{
227					{S: aws.String("1")},
228					{S: aws.String("2")},
229					{S: aws.String("3")},
230				}},
231				"Value8": {BS: [][]byte{
232					{0, 1, 2}, {3, 4, 5},
233				}},
234				"Value9": {NS: []*string{
235					aws.String("1"),
236					aws.String("2"),
237					aws.String("3"),
238				}},
239				"Value10": {SS: []*string{
240					aws.String("1"),
241					aws.String("2"),
242					aws.String("3"),
243				}},
244				"Value11": {L: []*dynamodb.AttributeValue{
245					{N: aws.String("1")},
246					{N: aws.String("2")},
247					{N: aws.String("3")},
248				}},
249				"Value12": {L: []*dynamodb.AttributeValue{
250					{S: aws.String("1")},
251					{S: aws.String("2")},
252					{S: aws.String("3")},
253				}},
254				"Value13": {BOOL: aws.Bool(true)},
255				"Value14": {L: []*dynamodb.AttributeValue{
256					{BOOL: aws.Bool(true)},
257					{BOOL: aws.Bool(false)},
258					{BOOL: aws.Bool(true)},
259				}},
260				"Value15": {M: map[string]*dynamodb.AttributeValue{
261					"TestKey": {S: aws.String("TestElement")},
262				}},
263			},
264		},
265		actual: &testAliasedStruct{},
266		expected: testAliasedStruct{
267			Value: "123", Value2: 123,
268			Value3: testAliasedMap{
269				"Key": 321,
270			},
271			Value4: testAliasedSlice{"1", "2", "3"},
272			Value5: testAliasedByteSlice{0, 1, 2},
273			Value6: []testAliasedInt{1, 2, 3},
274			Value7: []testAliasedString{"1", "2", "3"},
275			Value8: []testAliasedByteSlice{
276				{0, 1, 2},
277				{3, 4, 5},
278			},
279			Value9:  []testAliasedInt{1, 2, 3},
280			Value10: []testAliasedString{"1", "2", "3"},
281			Value11: testAliasedIntSlice{1, 2, 3},
282			Value12: testAliasedStringSlice{"1", "2", "3"},
283			Value13: true,
284			Value14: testAliasedBoolSlice{true, false, true},
285			Value15: map[testAliasedString]string{
286				"TestKey": "TestElement",
287			},
288		},
289	},
290	19: {
291		in:       &dynamodb.AttributeValue{N: aws.String("123")},
292		actual:   new(testNamedPointer),
293		expected: testNamedPointer(aws.Int(123)),
294	},
295	20: { // time.Time
296		in:       &dynamodb.AttributeValue{S: aws.String("2016-05-03T17:06:26.209072Z")},
297		actual:   new(time.Time),
298		expected: testDate,
299	},
300	21: { // time.Time List
301		in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
302			{S: aws.String("2016-05-03T17:06:26.209072Z")},
303			{S: aws.String("2016-05-04T17:06:26.209072Z")},
304		}},
305		actual:   new([]time.Time),
306		expected: []time.Time{testDate, testDate.Add(24 * time.Hour)},
307	},
308	22: { // time.Time struct
309		in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
310			"abc": {S: aws.String("2016-05-03T17:06:26.209072Z")},
311		}},
312		actual: &struct {
313			Abc time.Time `json:"abc" dynamodbav:"abc"`
314		}{},
315		expected: struct {
316			Abc time.Time `json:"abc" dynamodbav:"abc"`
317		}{Abc: testDate},
318	},
319	23: { // time.Time ptr struct
320		in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
321			"abc": {S: aws.String("2016-05-03T17:06:26.209072Z")},
322		}},
323		actual: &struct {
324			Abc *time.Time `json:"abc" dynamodbav:"abc"`
325		}{},
326		expected: struct {
327			Abc *time.Time `json:"abc" dynamodbav:"abc"`
328		}{Abc: &testDate},
329	},
330	24: { // empty string and NullEmptyString off
331		encoderOpts: func(encoder *Encoder) {
332			encoder.NullEmptyString = false
333		},
334		in:       &dynamodb.AttributeValue{S: aws.String("")},
335		actual:   new(string),
336		expected: "",
337	},
338	25: { // empty ptr string and NullEmptyString off
339		encoderOpts: func(encoder *Encoder) {
340			encoder.NullEmptyString = false
341		},
342		in:       &dynamodb.AttributeValue{S: aws.String("")},
343		actual:   new(*string),
344		expected: "",
345	},
346	26: { // string set with empty string and NullEmptyString off
347		encoderOpts: func(encoder *Encoder) {
348			encoder.NullEmptyString = false
349		},
350		in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
351			"Value": {SS: []*string{aws.String("one"), aws.String(""), aws.String("three")}},
352		}},
353		actual: &struct {
354			Value []string `dynamodbav:",stringset"`
355		}{},
356		expected: struct {
357			Value []string `dynamodbav:",stringset"`
358		}{
359			Value: []string{"one", "", "three"},
360		},
361	},
362	27: { // empty byte slice and NullEmptyString off
363		encoderOpts: func(encoder *Encoder) {
364			encoder.NullEmptyByteSlice = false
365		},
366		in:       &dynamodb.AttributeValue{B: []byte{}},
367		actual:   &[]byte{},
368		expected: []byte{},
369	},
370	28: { // byte slice set with empty values and NullEmptyString off
371		encoderOpts: func(encoder *Encoder) {
372			encoder.NullEmptyByteSlice = false
373		},
374		in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{"Value": {BS: [][]byte{{0x0}, {}, {0x2}}}}},
375		actual: &struct {
376			Value [][]byte `dynamodbav:",binaryset"`
377		}{},
378		expected: struct {
379			Value [][]byte `dynamodbav:",binaryset"`
380		}{
381			Value: [][]byte{{0x0}, {}, {0x2}},
382		},
383	},
384	29: { // empty byte slice and NullEmptyByteSlice disabled, and omitempty
385		encoderOpts: func(encoder *Encoder) {
386			encoder.NullEmptyByteSlice = false
387		},
388		in: &dynamodb.AttributeValue{
389			NULL: aws.Bool(true),
390		},
391		actual: &struct {
392			Value []byte `dynamodbav:",omitempty"`
393		}{
394			Value: []byte{},
395		},
396		expected: &struct {
397			Value []byte `dynamodbav:",omitempty"`
398		}{},
399	},
400}
401
402var sharedListTestCases = []struct {
403	in               []*dynamodb.AttributeValue
404	actual, expected interface{}
405	err              error
406}{
407	{
408		in: []*dynamodb.AttributeValue{
409			{B: []byte{48, 49}},
410			{BOOL: aws.Bool(true)},
411			{N: aws.String("123")},
412			{S: aws.String("123")},
413		},
414		actual: func() *[]interface{} {
415			v := []interface{}{}
416			return &v
417		}(),
418		expected: []interface{}{[]byte{48, 49}, true, 123., "123"},
419	},
420	{
421		in: []*dynamodb.AttributeValue{
422			{N: aws.String("1")},
423			{N: aws.String("2")},
424			{N: aws.String("3")},
425		},
426		actual:   &[]interface{}{},
427		expected: []interface{}{1., 2., 3.},
428	},
429}
430
431var sharedMapTestCases = []struct {
432	in               map[string]*dynamodb.AttributeValue
433	actual, expected interface{}
434	err              error
435}{
436	{
437		in: map[string]*dynamodb.AttributeValue{
438			"B":    {B: []byte{48, 49}},
439			"BOOL": {BOOL: aws.Bool(true)},
440			"N":    {N: aws.String("123")},
441			"S":    {S: aws.String("123")},
442		},
443		actual: &map[string]interface{}{},
444		expected: map[string]interface{}{
445			"B": []byte{48, 49}, "BOOL": true,
446			"N": 123., "S": "123",
447		},
448	},
449}
450
451func assertConvertTest(t *testing.T, i int, actual, expected interface{}, err, expectedErr error) {
452	if expectedErr != nil {
453		if err != nil {
454			if e, a := expectedErr, err; !strings.Contains(a.Error(), e.Error()) {
455				t.Errorf("case %d expect %v, got %v", i, e, a)
456			}
457		} else {
458			t.Fatalf("case %d, expected error, %v", i, expectedErr)
459		}
460	} else if err != nil {
461		t.Fatalf("case %d, expect no error, got %v", i, err)
462	} else {
463		if e, a := ptrToValue(expected), ptrToValue(actual); !reflect.DeepEqual(e, a) {
464			t.Errorf("case %d, expect %v, got %v", i, e, a)
465		}
466	}
467}
468
469func ptrToValue(in interface{}) interface{} {
470	v := reflect.ValueOf(in)
471	if v.Kind() == reflect.Ptr {
472		v = v.Elem()
473	}
474	if !v.IsValid() {
475		return nil
476	}
477	if v.Kind() == reflect.Ptr {
478		return ptrToValue(v.Interface())
479	}
480	return v.Interface()
481}
482