Skip to content

Commit

Permalink
service/dynamodb/dynamodbattribute: Support for configuring the marsh…
Browse files Browse the repository at this point in the history
…alling behavior of empty collections.
  • Loading branch information
skmcgrail committed Sep 17, 2019
1 parent 487e411 commit 3b13387
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 9 deletions.
12 changes: 6 additions & 6 deletions service/dynamodb/dynamodbattribute/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,23 +172,23 @@ func (d *Decoder) decode(av *dynamodb.AttributeValue, v reflect.Value, fieldTag
}

switch {
case len(av.B) != 0:
case len(av.B) != 0 || (av.B != nil && d.EmptyCollections):
return d.decodeBinary(av.B, v)
case av.BOOL != nil:
return d.decodeBool(av.BOOL, v)
case len(av.BS) != 0:
case len(av.BS) != 0 || (av.BS != nil && d.EmptyCollections):
return d.decodeBinarySet(av.BS, v)
case len(av.L) != 0:
case len(av.L) != 0 || (av.L != nil && d.EmptyCollections):
return d.decodeList(av.L, v)
case len(av.M) != 0:
case len(av.M) != 0 || (av.M != nil && d.EmptyCollections):
return d.decodeMap(av.M, v)
case av.N != nil:
return d.decodeNumber(av.N, v, fieldTag)
case len(av.NS) != 0:
case len(av.NS) != 0 || (av.NS != nil && d.EmptyCollections):
return d.decodeNumberSet(av.NS, v)
case av.S != nil:
return d.decodeString(av.S, v, fieldTag)
case len(av.SS) != 0:
case len(av.SS) != 0 || (av.SS != nil && d.EmptyCollections):
return d.decodeStringSet(av.SS, v)
}

Expand Down
137 changes: 137 additions & 0 deletions service/dynamodb/dynamodbattribute/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,140 @@ func TestDecodeAliasedUnixTime(t *testing.T) {
t.Errorf("expect %v, got %v", expect, actual)
}
}

func TestUnmarshalEmptyCollections(t *testing.T) {
null := dynamodb.AttributeValue{NULL: aws.Bool(true)}

type Empties struct {
Map map[string]string `dynamodbav:",omitempty"`
Slice []string `dynamodbav:",omitempty"`
ByteSlice []byte `dynamodbav:",omitempty"`
BinarySet [][]byte `dynamodbav:",omitempty"`
NumberSet []int `dynamodbav:",numberset,omitempty"`
StringSet []string `dynamodbav:",stringset,omitempty"`
}

type A struct {
Map map[string]string
Slice []string
ByteSlice []byte
ByteArray [4]byte
BinarySet [][]byte
NumberSet []int `dynamodbav:",numberset"`
StringSet []string `dynamodbav:",stringset"`
Empties Empties
}

cases := map[string]struct {
emptyCollections bool
value dynamodb.AttributeValue
expected A
}{
"nil default behavior": {
value: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": &null,
"Slice": &null,
"ByteSlice": &null,
"ByteArray": {B: make([]byte, 4)},
"BinarySet": &null,
"NumberSet": &null,
"StringSet": &null,
"Empties": &null,
},
},
},
"empty values with default behavior": {
value: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": {M: map[string]*dynamodb.AttributeValue{}},
"Slice": {L: []*dynamodb.AttributeValue{}},
"ByteSlice": {B: []byte{}},
"ByteArray": {B: make([]byte, 4)},
"BinarySet": {BS: [][]byte{}},
"NumberSet": {NS: []*string{}},
"StringSet": {SS: []*string{}},
"Empties": &null,
},
},
},
"nil values with empty collections option": {
emptyCollections: true,
value: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": &null,
"Slice": &null,
"ByteSlice": &null,
"ByteArray": {B: make([]byte, 4)},
"BinarySet": &null,
"NumberSet": &null,
"StringSet": &null,
},
},
},
"empty values with empty collections option": {
emptyCollections: true,
value: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": {M: map[string]*dynamodb.AttributeValue{}},
"Slice": {L: []*dynamodb.AttributeValue{}},
"ByteSlice": {B: []byte{}},
"ByteArray": {B: make([]byte, 4)},
"BinarySet": {BS: [][]byte{}},
"NumberSet": {NS: []*string{}},
"StringSet": {SS: []*string{}},
"Empties": &null,
},
},
expected: A{
Map: map[string]string{},
Slice: []string{},
ByteSlice: []byte{},
BinarySet: [][]byte{},
NumberSet: []int{},
StringSet: []string{},
},
},
"non-empty values with empty collections option": {
emptyCollections: true,
value: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": {M: map[string]*dynamodb.AttributeValue{"Test": {S: aws.String("test string")}}},
"Slice": {L: []*dynamodb.AttributeValue{{S: aws.String("test string")}}},
"ByteSlice": {B: []byte{0, 1}},
"ByteArray": {B: []byte{0, 1, 2, 3}},
"BinarySet": {BS: [][]byte{{0, 1}, {1, 2}}},
"NumberSet": {NS: []*string{aws.String("0"), aws.String("1")}},
"StringSet": {SS: []*string{aws.String("test string")}},
"Empties": &null,
},
},
expected: A{
Map: map[string]string{"Test": "test string"},
Slice: []string{"test string"},
ByteSlice: []byte{0, 1},
ByteArray: [4]byte{0, 1, 2, 3},
BinarySet: [][]byte{{0, 1}, {1, 2}},
NumberSet: []int{0, 1},
StringSet: []string{"test string"},
},
},
}

for name, tCase := range cases {
t.Log(name)
decoder := NewDecoder(func(d *Decoder) {
d.EmptyCollections = tCase.emptyCollections
})

actual := A{}
err := decoder.Decode(&tCase.value, &actual)
if err != nil {
t.Errorf("expect no err, got %v", err)
}

if e, a := tCase.expected, actual; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
}
15 changes: 12 additions & 3 deletions service/dynamodb/dynamodbattribute/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ type MarshalOptions struct {
// Note that values provided with a custom TagKey must also be supported
// by the (un)marshalers in this package.
TagKey string

// EmptyCollections controls how empty slices and maps are (un)marshalled.
// This will preserve an empty collection value as its respective
// empty DynamoDB AttributeValue type when set to true.
//
// Disabled by default.
EmptyCollections bool
}

// An Encoder provides marshaling Go value types to AttributeValues.
Expand Down Expand Up @@ -339,6 +346,7 @@ func (e *Encoder) encodeStruct(av *dynamodb.AttributeValue, v reflect.Value, fie

func (e *Encoder) encodeMap(av *dynamodb.AttributeValue, v reflect.Value, fieldTag tag) error {
av.M = map[string]*dynamodb.AttributeValue{}

for _, key := range v.MapKeys() {
keyName := fmt.Sprint(key.Interface())
if keyName == "" {
Expand All @@ -357,7 +365,8 @@ func (e *Encoder) encodeMap(av *dynamodb.AttributeValue, v reflect.Value, fieldT

av.M[keyName] = elem
}
if len(av.M) == 0 {

if v.IsNil() || (len(av.M) == 0 && !e.EmptyCollections) {
encodeNull(av)
}

Expand All @@ -371,7 +380,7 @@ func (e *Encoder) encodeSlice(av *dynamodb.AttributeValue, v reflect.Value, fiel
reflect.Copy(slice, v)

b := slice.Bytes()
if len(b) == 0 {
if (v.Kind() == reflect.Slice && v.IsNil()) || (len(b) == 0 && !e.EmptyCollections) {
encodeNull(av)
return nil
}
Expand Down Expand Up @@ -416,7 +425,7 @@ func (e *Encoder) encodeSlice(av *dynamodb.AttributeValue, v reflect.Value, fiel

if n, err := e.encodeList(v, fieldTag, elemFn); err != nil {
return err
} else if n == 0 {
} else if (v.Kind() == reflect.Slice && v.IsNil()) || (n == 0 && !e.EmptyCollections) {
encodeNull(av)
}
}
Expand Down
145 changes: 145 additions & 0 deletions service/dynamodb/dynamodbattribute/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,148 @@ func TestEncodeAliasedUnixTime(t *testing.T) {
t.Errorf("expect %v, got %v", e, a)
}
}

func TestMarshalEmptyCollections(t *testing.T) {
null := dynamodb.AttributeValue{NULL: aws.Bool(true)}

type Empties struct {
Map map[string]string `dynamodbav:",omitempty"`
Slice []string `dynamodbav:",omitempty"`
ByteSlice []byte `dynamodbav:",omitempty"`
BinarySet [][]byte `dynamodbav:",omitempty"`
NumberSet []int `dynamodbav:",numberset,omitempty"`
StringSet []string `dynamodbav:",stringset,omitempty"`
}

type A struct {
Map map[string]string
Slice []string
ByteSlice []byte
ByteArray [4]byte
BinarySet [][]byte
NumberSet []int `dynamodbav:",numberset"`
StringSet []string `dynamodbav:",stringset"`
Empties Empties
}

cases := map[string]struct {
emptyCollections bool
value A
expected dynamodb.AttributeValue
}{
"nil default behavior": {
expected: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": &null,
"Slice": &null,
"ByteSlice": &null,
"ByteArray": {B: make([]byte, 4)},
"BinarySet": &null,
"NumberSet": &null,
"StringSet": &null,
"Empties": &null,
},
},
},
"empty values with default behavior": {
value: A{
Map: map[string]string{},
Slice: []string{},
ByteSlice: []byte{},
BinarySet: [][]byte{},
NumberSet: []int{},
StringSet: []string{},
},
expected: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": &null,
"Slice": &null,
"ByteSlice": &null,
"ByteArray": {B: make([]byte, 4)},
"BinarySet": &null,
"NumberSet": &null,
"StringSet": &null,
"Empties": &null,
},
},
},
"nil values with empty collections option": {
emptyCollections: true,
expected: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": &null,
"Slice": &null,
"ByteSlice": &null,
"ByteArray": {B: make([]byte, 4)},
"BinarySet": &null,
"NumberSet": &null,
"StringSet": &null,
"Empties": &null,
},
},
},
"empty values with empty collections option": {
emptyCollections: true,
value: A{
Map: map[string]string{},
Slice: []string{},
ByteSlice: []byte{},
BinarySet: [][]byte{},
NumberSet: []int{},
StringSet: []string{},
},
expected: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": {M: map[string]*dynamodb.AttributeValue{}},
"Slice": {L: []*dynamodb.AttributeValue{}},
"ByteSlice": {B: []byte{}},
"ByteArray": {B: make([]byte, 4)},
"BinarySet": {BS: [][]byte{}},
"NumberSet": {NS: []*string{}},
"StringSet": {SS: []*string{}},
"Empties": &null,
},
},
},
"non-empty values with empty collections option": {
emptyCollections: true,
value: A{
Map: map[string]string{"Test": "test string"},
Slice: []string{"test string"},
ByteSlice: []byte{0, 1},
ByteArray: [4]byte{0, 1, 2, 3},
BinarySet: [][]byte{{0, 1}, {1, 2}},
NumberSet: []int{0, 1},
StringSet: []string{"test string"},
},
expected: dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Map": {M: map[string]*dynamodb.AttributeValue{"Test": {S: aws.String("test string")}}},
"Slice": {L: []*dynamodb.AttributeValue{{S: aws.String("test string")}}},
"ByteSlice": {B: []byte{0, 1}},
"ByteArray": {B: []byte{0, 1, 2, 3}},
"BinarySet": {BS: [][]byte{{0, 1}, {1, 2}}},
"NumberSet": {NS: []*string{aws.String("0"), aws.String("1")}},
"StringSet": {SS: []*string{aws.String("test string")}},
"Empties": &null,
},
},
},
}

for name, tCase := range cases {
t.Log(name)
encoder := NewEncoder(func(e *Encoder) {
e.EmptyCollections = tCase.emptyCollections
})

actual, err := encoder.Encode(&tCase.value)
if err != nil {
t.Errorf("expect no err, got %v", err)
}

if e, a := &tCase.expected, actual; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
}

0 comments on commit 3b13387

Please sign in to comment.