Switch to using the dep tool and update all the dependencies

This commit is contained in:
Nick Craig-Wood 2017-05-11 15:39:54 +01:00
parent 5135ff73cb
commit 98c2d2c41b
5321 changed files with 4483201 additions and 5922 deletions

View file

@ -0,0 +1,443 @@
package dynamodbattribute
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"runtime"
"strconv"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// ConvertToMap accepts a map[string]interface{} or struct and converts it to a
// map[string]*dynamodb.AttributeValue.
//
// If in contains any structs, it is first JSON encoded/decoded it to convert it
// to a map[string]interface{}, so `json` struct tags are respected.
//
// Deprecated: Use MarshalMap instead
func ConvertToMap(in interface{}) (item map[string]*dynamodb.AttributeValue, err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(runtime.Error); ok {
err = e
} else if s, ok := r.(string); ok {
err = fmt.Errorf(s)
} else {
err = r.(error)
}
item = nil
}
}()
if in == nil {
return nil, awserr.New("SerializationError",
"in must be a map[string]interface{} or struct, got <nil>", nil)
}
v := reflect.ValueOf(in)
if v.Kind() != reflect.Struct && !(v.Kind() == reflect.Map && v.Type().Key().Kind() == reflect.String) {
return nil, awserr.New("SerializationError",
fmt.Sprintf("in must be a map[string]interface{} or struct, got %s",
v.Type().String()),
nil)
}
if isTyped(reflect.TypeOf(in)) {
var out map[string]interface{}
in = convertToUntyped(in, out)
}
item = make(map[string]*dynamodb.AttributeValue)
for k, v := range in.(map[string]interface{}) {
item[k] = convertTo(v)
}
return item, nil
}
// ConvertFromMap accepts a map[string]*dynamodb.AttributeValue and converts it to a
// map[string]interface{} or struct.
//
// If v points to a struct, the result is first converted it to a
// map[string]interface{}, then JSON encoded/decoded it to convert to a struct,
// so `json` struct tags are respected.
//
// Deprecated: Use UnmarshalMap instead
func ConvertFromMap(item map[string]*dynamodb.AttributeValue, v interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(runtime.Error); ok {
err = e
} else if s, ok := r.(string); ok {
err = fmt.Errorf(s)
} else {
err = r.(error)
}
item = nil
}
}()
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return awserr.New("SerializationError",
fmt.Sprintf("v must be a non-nil pointer to a map[string]interface{} or struct, got %s",
rv.Type()),
nil)
}
if rv.Elem().Kind() != reflect.Struct && !(rv.Elem().Kind() == reflect.Map && rv.Elem().Type().Key().Kind() == reflect.String) {
return awserr.New("SerializationError",
fmt.Sprintf("v must be a non-nil pointer to a map[string]interface{} or struct, got %s",
rv.Type()),
nil)
}
m := make(map[string]interface{})
for k, v := range item {
m[k] = convertFrom(v)
}
if isTyped(reflect.TypeOf(v)) {
err = convertToTyped(m, v)
} else {
rv.Elem().Set(reflect.ValueOf(m))
}
return err
}
// ConvertToList accepts an array or slice and converts it to a
// []*dynamodb.AttributeValue.
//
// Converting []byte fields to dynamodb.AttributeValue are only currently supported
// if the input is a map[string]interface{} type. []byte within typed structs are not
// converted correctly and are converted into base64 strings. This is a known bug,
// and will be fixed in a later release.
//
// If in contains any structs, it is first JSON encoded/decoded it to convert it
// to a []interface{}, so `json` struct tags are respected.
//
// Deprecated: Use MarshalList instead
func ConvertToList(in interface{}) (item []*dynamodb.AttributeValue, err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(runtime.Error); ok {
err = e
} else if s, ok := r.(string); ok {
err = fmt.Errorf(s)
} else {
err = r.(error)
}
item = nil
}
}()
if in == nil {
return nil, awserr.New("SerializationError",
"in must be an array or slice, got <nil>",
nil)
}
v := reflect.ValueOf(in)
if v.Kind() != reflect.Array && v.Kind() != reflect.Slice {
return nil, awserr.New("SerializationError",
fmt.Sprintf("in must be an array or slice, got %s",
v.Type().String()),
nil)
}
if isTyped(reflect.TypeOf(in)) {
var out []interface{}
in = convertToUntyped(in, out)
}
item = make([]*dynamodb.AttributeValue, 0, len(in.([]interface{})))
for _, v := range in.([]interface{}) {
item = append(item, convertTo(v))
}
return item, nil
}
// ConvertFromList accepts a []*dynamodb.AttributeValue and converts it to an array or
// slice.
//
// If v contains any structs, the result is first converted it to a
// []interface{}, then JSON encoded/decoded it to convert to a typed array or
// slice, so `json` struct tags are respected.
//
// Deprecated: Use UnmarshalList instead
func ConvertFromList(item []*dynamodb.AttributeValue, v interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(runtime.Error); ok {
err = e
} else if s, ok := r.(string); ok {
err = fmt.Errorf(s)
} else {
err = r.(error)
}
item = nil
}
}()
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return awserr.New("SerializationError",
fmt.Sprintf("v must be a non-nil pointer to an array or slice, got %s",
rv.Type()),
nil)
}
if rv.Elem().Kind() != reflect.Array && rv.Elem().Kind() != reflect.Slice {
return awserr.New("SerializationError",
fmt.Sprintf("v must be a non-nil pointer to an array or slice, got %s",
rv.Type()),
nil)
}
l := make([]interface{}, 0, len(item))
for _, v := range item {
l = append(l, convertFrom(v))
}
if isTyped(reflect.TypeOf(v)) {
err = convertToTyped(l, v)
} else {
rv.Elem().Set(reflect.ValueOf(l))
}
return err
}
// ConvertTo accepts any interface{} and converts it to a *dynamodb.AttributeValue.
//
// If in contains any structs, it is first JSON encoded/decoded it to convert it
// to a interface{}, so `json` struct tags are respected.
//
// Deprecated: Use Marshal instead
func ConvertTo(in interface{}) (item *dynamodb.AttributeValue, err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(runtime.Error); ok {
err = e
} else if s, ok := r.(string); ok {
err = fmt.Errorf(s)
} else {
err = r.(error)
}
item = nil
}
}()
if in != nil && isTyped(reflect.TypeOf(in)) {
var out interface{}
in = convertToUntyped(in, out)
}
item = convertTo(in)
return item, nil
}
// ConvertFrom accepts a *dynamodb.AttributeValue and converts it to any interface{}.
//
// If v contains any structs, the result is first converted it to a interface{},
// then JSON encoded/decoded it to convert to a struct, so `json` struct tags
// are respected.
//
// Deprecated: Use Unmarshal instead
func ConvertFrom(item *dynamodb.AttributeValue, v interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(runtime.Error); ok {
err = e
} else if s, ok := r.(string); ok {
err = fmt.Errorf(s)
} else {
err = r.(error)
}
item = nil
}
}()
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return awserr.New("SerializationError",
fmt.Sprintf("v must be a non-nil pointer to an interface{} or struct, got %s",
rv.Type()),
nil)
}
if rv.Elem().Kind() != reflect.Interface && rv.Elem().Kind() != reflect.Struct {
return awserr.New("SerializationError",
fmt.Sprintf("v must be a non-nil pointer to an interface{} or struct, got %s",
rv.Type()),
nil)
}
res := convertFrom(item)
if isTyped(reflect.TypeOf(v)) {
err = convertToTyped(res, v)
} else if res != nil {
rv.Elem().Set(reflect.ValueOf(res))
}
return err
}
func isTyped(v reflect.Type) bool {
switch v.Kind() {
case reflect.Struct:
return true
case reflect.Array, reflect.Slice:
if isTyped(v.Elem()) {
return true
}
case reflect.Map:
if isTyped(v.Key()) {
return true
}
if isTyped(v.Elem()) {
return true
}
case reflect.Ptr:
return isTyped(v.Elem())
}
return false
}
func convertToUntyped(in, out interface{}) interface{} {
b, err := json.Marshal(in)
if err != nil {
panic(err)
}
decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
err = decoder.Decode(&out)
if err != nil {
panic(err)
}
return out
}
func convertToTyped(in, out interface{}) error {
b, err := json.Marshal(in)
if err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(b))
return decoder.Decode(&out)
}
func convertTo(in interface{}) *dynamodb.AttributeValue {
a := &dynamodb.AttributeValue{}
if in == nil {
a.NULL = new(bool)
*a.NULL = true
return a
}
if m, ok := in.(map[string]interface{}); ok {
a.M = make(map[string]*dynamodb.AttributeValue)
for k, v := range m {
a.M[k] = convertTo(v)
}
return a
}
v := reflect.ValueOf(in)
switch v.Kind() {
case reflect.Bool:
a.BOOL = new(bool)
*a.BOOL = v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
a.N = new(string)
*a.N = strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
a.N = new(string)
*a.N = strconv.FormatUint(v.Uint(), 10)
case reflect.Float32, reflect.Float64:
a.N = new(string)
*a.N = strconv.FormatFloat(v.Float(), 'f', -1, 64)
case reflect.String:
if n, ok := in.(json.Number); ok {
a.N = new(string)
*a.N = n.String()
} else {
a.S = new(string)
*a.S = v.String()
}
case reflect.Slice:
switch v.Type() {
case reflect.TypeOf(([]byte)(nil)):
a.B = v.Bytes()
default:
a.L = make([]*dynamodb.AttributeValue, v.Len())
for i := 0; i < v.Len(); i++ {
a.L[i] = convertTo(v.Index(i).Interface())
}
}
default:
panic(fmt.Sprintf("the type %s is not supported", v.Type().String()))
}
return a
}
func convertFrom(a *dynamodb.AttributeValue) interface{} {
if a.S != nil {
return *a.S
}
if a.N != nil {
// Number is tricky b/c we don't know which numeric type to use. Here we
// simply try the different types from most to least restrictive.
if n, err := strconv.ParseInt(*a.N, 10, 64); err == nil {
return int(n)
}
if n, err := strconv.ParseUint(*a.N, 10, 64); err == nil {
return uint(n)
}
n, err := strconv.ParseFloat(*a.N, 64)
if err != nil {
panic(err)
}
return n
}
if a.BOOL != nil {
return *a.BOOL
}
if a.NULL != nil {
return nil
}
if a.M != nil {
m := make(map[string]interface{})
for k, v := range a.M {
m[k] = convertFrom(v)
}
return m
}
if a.L != nil {
l := make([]interface{}, len(a.L))
for index, v := range a.L {
l[index] = convertFrom(v)
}
return l
}
if a.B != nil {
return a.B
}
panic(fmt.Sprintf("%#v is not a supported dynamodb.AttributeValue", a))
}

View file

@ -0,0 +1,80 @@
package dynamodbattribute_test
import (
"fmt"
"reflect"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
func ExampleConvertTo() {
type Record struct {
MyField string
Letters []string
Numbers []int
}
r := Record{
MyField: "MyFieldValue",
Letters: []string{"a", "b", "c", "d"},
Numbers: []int{1, 2, 3},
}
av, err := dynamodbattribute.ConvertTo(r)
fmt.Println("err", err)
fmt.Println("MyField", av.M["MyField"])
fmt.Println("Letters", av.M["Letters"])
fmt.Println("Numbers", av.M["Numbers"])
// Output:
// err <nil>
// MyField {
// S: "MyFieldValue"
// }
// Letters {
// L: [
// {
// S: "a"
// },
// {
// S: "b"
// },
// {
// S: "c"
// },
// {
// S: "d"
// }
// ]
// }
// Numbers {
// L: [{
// N: "1"
// },{
// N: "2"
// },{
// N: "3"
// }]
// }
}
func ExampleConvertFrom() {
type Record struct {
MyField string
Letters []string
A2Num map[string]int
}
r := Record{
MyField: "MyFieldValue",
Letters: []string{"a", "b", "c", "d"},
A2Num: map[string]int{"a": 1, "b": 2, "c": 3},
}
av, err := dynamodbattribute.ConvertTo(r)
r2 := Record{}
err = dynamodbattribute.ConvertFrom(av, &r2)
fmt.Println(err, reflect.DeepEqual(r, r2))
// Output:
// <nil> true
}

View file

@ -0,0 +1,498 @@
package dynamodbattribute
import (
"math"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
type mySimpleStruct struct {
String string
Int int
Uint uint
Float32 float32
Float64 float64
Bool bool
Null *interface{}
}
type myComplexStruct struct {
Simple []mySimpleStruct
}
type converterTestInput struct {
input interface{}
expected interface{}
err awserr.Error
inputType string // "enum" of types
}
var trueValue = true
var falseValue = false
var converterScalarInputs = []converterTestInput{
{
input: nil,
expected: &dynamodb.AttributeValue{NULL: &trueValue},
},
{
input: "some string",
expected: &dynamodb.AttributeValue{S: aws.String("some string")},
},
{
input: true,
expected: &dynamodb.AttributeValue{BOOL: &trueValue},
},
{
input: false,
expected: &dynamodb.AttributeValue{BOOL: &falseValue},
},
{
input: 3.14,
expected: &dynamodb.AttributeValue{N: aws.String("3.14")},
},
{
input: math.MaxFloat32,
expected: &dynamodb.AttributeValue{N: aws.String("340282346638528860000000000000000000000")},
},
{
input: math.MaxFloat64,
expected: &dynamodb.AttributeValue{N: aws.String("179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")},
},
{
input: 12,
expected: &dynamodb.AttributeValue{N: aws.String("12")},
},
{
input: mySimpleStruct{},
expected: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {S: aws.String("")},
"Uint": {N: aws.String("0")},
},
},
inputType: "mySimpleStruct",
},
}
var converterMapTestInputs = []converterTestInput{
// Scalar tests
{
input: nil,
err: awserr.New("SerializationError", "in must be a map[string]interface{} or struct, got <nil>", nil),
},
{
input: map[string]interface{}{"string": "some string"},
expected: map[string]*dynamodb.AttributeValue{"string": {S: aws.String("some string")}},
},
{
input: map[string]interface{}{"bool": true},
expected: map[string]*dynamodb.AttributeValue{"bool": {BOOL: &trueValue}},
},
{
input: map[string]interface{}{"bool": false},
expected: map[string]*dynamodb.AttributeValue{"bool": {BOOL: &falseValue}},
},
{
input: map[string]interface{}{"null": nil},
expected: map[string]*dynamodb.AttributeValue{"null": {NULL: &trueValue}},
},
{
input: map[string]interface{}{"float": 3.14},
expected: map[string]*dynamodb.AttributeValue{"float": {N: aws.String("3.14")}},
},
{
input: map[string]interface{}{"float": math.MaxFloat32},
expected: map[string]*dynamodb.AttributeValue{"float": {N: aws.String("340282346638528860000000000000000000000")}},
},
{
input: map[string]interface{}{"float": math.MaxFloat64},
expected: map[string]*dynamodb.AttributeValue{"float": {N: aws.String("179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")}},
},
{
input: map[string]interface{}{"int": int(12)},
expected: map[string]*dynamodb.AttributeValue{"int": {N: aws.String("12")}},
},
{
input: map[string]interface{}{"byte": []byte{48, 49}},
expected: map[string]*dynamodb.AttributeValue{"byte": {B: []byte{48, 49}}},
},
// List
{
input: map[string]interface{}{"list": []interface{}{"a string", 12, 3.14, true, nil, false}},
expected: map[string]*dynamodb.AttributeValue{
"list": {
L: []*dynamodb.AttributeValue{
{S: aws.String("a string")},
{N: aws.String("12")},
{N: aws.String("3.14")},
{BOOL: &trueValue},
{NULL: &trueValue},
{BOOL: &falseValue},
},
},
},
},
// Map
{
input: map[string]interface{}{"map": map[string]interface{}{"nestedint": 12}},
expected: map[string]*dynamodb.AttributeValue{
"map": {
M: map[string]*dynamodb.AttributeValue{
"nestedint": {
N: aws.String("12"),
},
},
},
},
},
// Structs
{
input: mySimpleStruct{},
expected: map[string]*dynamodb.AttributeValue{
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {S: aws.String("")},
"Uint": {N: aws.String("0")},
},
inputType: "mySimpleStruct",
},
{
input: myComplexStruct{},
expected: map[string]*dynamodb.AttributeValue{
"Simple": {NULL: &trueValue},
},
inputType: "myComplexStruct",
},
{
input: myComplexStruct{Simple: []mySimpleStruct{{Int: -2}, {Uint: 5}}},
expected: map[string]*dynamodb.AttributeValue{
"Simple": {
L: []*dynamodb.AttributeValue{
{
M: map[string]*dynamodb.AttributeValue{
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("-2")},
"Null": {NULL: &trueValue},
"String": {S: aws.String("")},
"Uint": {N: aws.String("0")},
},
},
{
M: map[string]*dynamodb.AttributeValue{
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {S: aws.String("")},
"Uint": {N: aws.String("5")},
},
},
},
},
},
inputType: "myComplexStruct",
},
}
var converterListTestInputs = []converterTestInput{
{
input: nil,
err: awserr.New("SerializationError", "in must be an array or slice, got <nil>", nil),
},
{
input: []interface{}{},
expected: []*dynamodb.AttributeValue{},
},
{
input: []interface{}{"a string", 12, 3.14, true, nil, false},
expected: []*dynamodb.AttributeValue{
{S: aws.String("a string")},
{N: aws.String("12")},
{N: aws.String("3.14")},
{BOOL: &trueValue},
{NULL: &trueValue},
{BOOL: &falseValue},
},
},
{
input: []mySimpleStruct{{}},
expected: []*dynamodb.AttributeValue{
{
M: map[string]*dynamodb.AttributeValue{
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {S: aws.String("")},
"Uint": {N: aws.String("0")},
},
},
},
inputType: "mySimpleStruct",
},
}
func TestConvertTo(t *testing.T) {
for _, test := range converterScalarInputs {
testConvertTo(t, test)
}
}
func testConvertTo(t *testing.T, test converterTestInput) {
actual, err := ConvertTo(test.input)
if test.err != nil {
if err == nil {
t.Errorf("ConvertTo with input %#v retured %#v, expected error `%s`", test.input, actual, test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("ConvertTo with input %#v retured error `%s`, expected error `%s`", test.input, err, test.err)
}
} else {
if err != nil {
t.Errorf("ConvertTo with input %#v retured error `%s`", test.input, err)
}
compareObjects(t, test.expected, actual)
}
}
func TestConvertFrom(t *testing.T) {
// Using the same inputs from TestConvertTo, test the reverse mapping.
for _, test := range converterScalarInputs {
if test.expected != nil {
testConvertFrom(t, test)
}
}
}
func testConvertFrom(t *testing.T, test converterTestInput) {
switch test.inputType {
case "mySimpleStruct":
var actual mySimpleStruct
if err := ConvertFrom(test.expected.(*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFrom with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
case "myComplexStruct":
var actual myComplexStruct
if err := ConvertFrom(test.expected.(*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFrom with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
default:
var actual interface{}
if err := ConvertFrom(test.expected.(*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFrom with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
}
}
func TestConvertFromError(t *testing.T) {
// Test that we get an error using ConvertFrom to convert to a map.
var actual map[string]interface{}
expected := awserr.New("SerializationError", `v must be a non-nil pointer to an interface{} or struct, got *map[string]interface {}`, nil).Error()
if err := ConvertFrom(nil, &actual); err == nil {
t.Errorf("ConvertFrom with input %#v returned no error, expected error `%s`", nil, expected)
} else if err.Error() != expected {
t.Errorf("ConvertFrom with input %#v returned error `%s`, expected error `%s`", nil, err, expected)
}
// Test that we get an error using ConvertFrom to convert to a list.
var actual2 []interface{}
expected = awserr.New("SerializationError", `v must be a non-nil pointer to an interface{} or struct, got *[]interface {}`, nil).Error()
if err := ConvertFrom(nil, &actual2); err == nil {
t.Errorf("ConvertFrom with input %#v returned no error, expected error `%s`", nil, expected)
} else if err.Error() != expected {
t.Errorf("ConvertFrom with input %#v returned error `%s`, expected error `%s`", nil, err, expected)
}
}
func TestConvertToMap(t *testing.T) {
for _, test := range converterMapTestInputs {
testConvertToMap(t, test)
}
}
func testConvertToMap(t *testing.T, test converterTestInput) {
actual, err := ConvertToMap(test.input)
if test.err != nil {
if err == nil {
t.Errorf("ConvertToMap with input %#v retured %#v, expected error `%s`", test.input, actual, test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("ConvertToMap with input %#v retured error `%s`, expected error `%s`", test.input, err, test.err)
}
} else {
if err != nil {
t.Errorf("ConvertToMap with input %#v retured error `%s`", test.input, err)
}
compareObjects(t, test.expected, actual)
}
}
func TestConvertFromMap(t *testing.T) {
// Using the same inputs from TestConvertToMap, test the reverse mapping.
for _, test := range converterMapTestInputs {
if test.expected != nil {
testConvertFromMap(t, test)
}
}
}
func testConvertFromMap(t *testing.T, test converterTestInput) {
switch test.inputType {
case "mySimpleStruct":
var actual mySimpleStruct
if err := ConvertFromMap(test.expected.(map[string]*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFromMap with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
case "myComplexStruct":
var actual myComplexStruct
if err := ConvertFromMap(test.expected.(map[string]*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFromMap with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
default:
var actual map[string]interface{}
if err := ConvertFromMap(test.expected.(map[string]*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFromMap with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
}
}
func TestConvertFromMapError(t *testing.T) {
// Test that we get an error using ConvertFromMap to convert to an interface{}.
var actual interface{}
expected := awserr.New("SerializationError", `v must be a non-nil pointer to a map[string]interface{} or struct, got *interface {}`, nil).Error()
if err := ConvertFromMap(nil, &actual); err == nil {
t.Errorf("ConvertFromMap with input %#v returned no error, expected error `%s`", nil, expected)
} else if err.Error() != expected {
t.Errorf("ConvertFromMap with input %#v returned error `%s`, expected error `%s`", nil, err, expected)
}
// Test that we get an error using ConvertFromMap to convert to a slice.
var actual2 []interface{}
expected = awserr.New("SerializationError", `v must be a non-nil pointer to a map[string]interface{} or struct, got *[]interface {}`, nil).Error()
if err := ConvertFromMap(nil, &actual2); err == nil {
t.Errorf("ConvertFromMap with input %#v returned no error, expected error `%s`", nil, expected)
} else if err.Error() != expected {
t.Errorf("ConvertFromMap with input %#v returned error `%s`, expected error `%s`", nil, err, expected)
}
}
func TestConvertToList(t *testing.T) {
for _, test := range converterListTestInputs {
testConvertToList(t, test)
}
}
func testConvertToList(t *testing.T, test converterTestInput) {
actual, err := ConvertToList(test.input)
if test.err != nil {
if err == nil {
t.Errorf("ConvertToList with input %#v retured %#v, expected error `%s`", test.input, actual, test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("ConvertToList with input %#v retured error `%s`, expected error `%s`", test.input, err, test.err)
}
} else {
if err != nil {
t.Errorf("ConvertToList with input %#v retured error `%s`", test.input, err)
}
compareObjects(t, test.expected, actual)
}
}
func TestConvertFromList(t *testing.T) {
// Using the same inputs from TestConvertToList, test the reverse mapping.
for _, test := range converterListTestInputs {
if test.expected != nil {
testConvertFromList(t, test)
}
}
}
func testConvertFromList(t *testing.T, test converterTestInput) {
switch test.inputType {
case "mySimpleStruct":
var actual []mySimpleStruct
if err := ConvertFromList(test.expected.([]*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFromList with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
case "myComplexStruct":
var actual []myComplexStruct
if err := ConvertFromList(test.expected.([]*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFromList with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
default:
var actual []interface{}
if err := ConvertFromList(test.expected.([]*dynamodb.AttributeValue), &actual); err != nil {
t.Errorf("ConvertFromList with input %#v retured error `%s`", test.expected, err)
}
compareObjects(t, test.input, actual)
}
}
func TestConvertFromListError(t *testing.T) {
// Test that we get an error using ConvertFromList to convert to a map.
var actual map[string]interface{}
expected := awserr.New("SerializationError", `v must be a non-nil pointer to an array or slice, got *map[string]interface {}`, nil).Error()
if err := ConvertFromList(nil, &actual); err == nil {
t.Errorf("ConvertFromList with input %#v returned no error, expected error `%s`", nil, expected)
} else if err.Error() != expected {
t.Errorf("ConvertFromList with input %#v returned error `%s`, expected error `%s`", nil, err, expected)
}
// Test that we get an error using ConvertFromList to convert to a struct.
var actual2 myComplexStruct
expected = awserr.New("SerializationError", `v must be a non-nil pointer to an array or slice, got *dynamodbattribute.myComplexStruct`, nil).Error()
if err := ConvertFromList(nil, &actual2); err == nil {
t.Errorf("ConvertFromList with input %#v returned no error, expected error `%s`", nil, expected)
} else if err.Error() != expected {
t.Errorf("ConvertFromList with input %#v returned error `%s`, expected error `%s`", nil, err, expected)
}
// Test that we get an error using ConvertFromList to convert to an interface{}.
var actual3 interface{}
expected = awserr.New("SerializationError", `v must be a non-nil pointer to an array or slice, got *interface {}`, nil).Error()
if err := ConvertFromList(nil, &actual3); err == nil {
t.Errorf("ConvertFromList with input %#v returned no error, expected error `%s`", nil, expected)
} else if err.Error() != expected {
t.Errorf("ConvertFromList with input %#v returned error `%s`, expected error `%s`", nil, err, expected)
}
}
func BenchmarkConvertTo(b *testing.B) {
d := mySimpleStruct{
String: "abc",
Int: 123,
Uint: 123,
Float32: 123.321,
Float64: 123.321,
Bool: true,
Null: nil,
}
for i := 0; i < b.N; i++ {
_, err := ConvertTo(d)
if err != nil {
b.Fatal("unexpected error", err)
}
}
}

View file

@ -0,0 +1,732 @@
package dynamodbattribute
import (
"fmt"
"reflect"
"strconv"
"time"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// An Unmarshaler is an interface to provide custom unmarshaling of
// AttributeValues. Use this to provide custom logic determining
// how AttributeValues should be unmarshaled.
// type ExampleUnmarshaler struct {
// Value int
// }
//
// type (u *exampleUnmarshaler) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
// if av.N == nil {
// return nil
// }
//
// n, err := strconv.ParseInt(*av.N, 10, 0)
// if err != nil {
// return err
// }
//
// u.Value = n
// return nil
// }
type Unmarshaler interface {
UnmarshalDynamoDBAttributeValue(*dynamodb.AttributeValue) error
}
// Unmarshal will unmarshal DynamoDB AttributeValues to Go value types.
// Both generic interface{} and concrete types are valid unmarshal
// destination types.
//
// Unmarshal will allocate maps, slices, and pointers as needed to
// unmarshal the AttributeValue into the provided type value.
//
// When unmarshaling AttributeValues into structs Unmarshal matches
// the field names of the struct to the AttributeValue Map keys.
// Initially it will look for exact field name matching, but will
// fall back to case insensitive if not exact match is found.
//
// With the exception of omitempty, omitemptyelem, binaryset, numberset
// and stringset all struct tags used by Marshal are also used by
// Unmarshal.
//
// When decoding AttributeValues to interfaces Unmarshal will use the
// following types.
//
// []byte, AV Binary (B)
// [][]byte, AV Binary Set (BS)
// bool, AV Boolean (BOOL)
// []interface{}, AV List (L)
// map[string]interface{}, AV Map (M)
// float64, AV Number (N)
// Number, AV Number (N) with UseNumber set
// []float64, AV Number Set (NS)
// []Number, AV Number Set (NS) with UseNumber set
// string, AV String (S)
// []string, AV String Set (SS)
//
// If the Decoder option, UseNumber is set numbers will be unmarshaled
// as Number values instead of float64. Use this to maintain the original
// string formating of the number as it was represented in the AttributeValue.
// In addition provides additional opportunities to parse the number
// string based on individual use cases.
//
// When unmarshaling any error that occurs will halt the unmarshal
// and return the error.
//
// The output value provided must be a non-nil pointer
func Unmarshal(av *dynamodb.AttributeValue, out interface{}) error {
return NewDecoder().Decode(av, out)
}
// UnmarshalMap is an alias for Unmarshal which unmarshals from
// a map of AttributeValues.
//
// The output value provided must be a non-nil pointer
func UnmarshalMap(m map[string]*dynamodb.AttributeValue, out interface{}) error {
return NewDecoder().Decode(&dynamodb.AttributeValue{M: m}, out)
}
// UnmarshalList is an alias for Unmarshal func which unmarshals
// a slice of AttributeValues.
//
// The output value provided must be a non-nil pointer
func UnmarshalList(l []*dynamodb.AttributeValue, out interface{}) error {
return NewDecoder().Decode(&dynamodb.AttributeValue{L: l}, out)
}
// UnmarshalListOfMaps is an alias for Unmarshal func which unmarshals a
// slice of maps of attribute values.
//
// This is useful for when you need to unmarshal the Items from a DynamoDB
// Query API call.
//
// The output value provided must be a non-nil pointer
func UnmarshalListOfMaps(l []map[string]*dynamodb.AttributeValue, out interface{}) error {
items := make([]*dynamodb.AttributeValue, len(l))
for i, m := range l {
items[i] = &dynamodb.AttributeValue{M: m}
}
return UnmarshalList(items, out)
}
// A Decoder provides unmarshaling AttributeValues to Go value types.
type Decoder struct {
MarshalOptions
// Instructs the decoder to decode AttributeValue Numbers as
// Number type instead of float64 when the destination type
// is interface{}. Similar to encoding/json.Number
UseNumber bool
}
// NewDecoder creates a new Decoder with default configuration. Use
// the `opts` functional options to override the default configuration.
func NewDecoder(opts ...func(*Decoder)) *Decoder {
d := &Decoder{
MarshalOptions: MarshalOptions{
SupportJSONTags: true,
},
}
for _, o := range opts {
o(d)
}
return d
}
// Decode will unmarshal an AttributeValue into a Go value type. An error
// will be return if the decoder is unable to unmarshal the AttributeValue
// to the provide Go value type.
//
// The output value provided must be a non-nil pointer
func (d *Decoder) Decode(av *dynamodb.AttributeValue, out interface{}, opts ...func(*Decoder)) error {
v := reflect.ValueOf(out)
if v.Kind() != reflect.Ptr || v.IsNil() || !v.IsValid() {
return &InvalidUnmarshalError{Type: reflect.TypeOf(out)}
}
return d.decode(av, v, tag{})
}
var stringInterfaceMapType = reflect.TypeOf(map[string]interface{}(nil))
var byteSliceType = reflect.TypeOf([]byte(nil))
var byteSliceSlicetype = reflect.TypeOf([][]byte(nil))
var numberType = reflect.TypeOf(Number(""))
func (d *Decoder) decode(av *dynamodb.AttributeValue, v reflect.Value, fieldTag tag) error {
var u Unmarshaler
if av == nil || av.NULL != nil {
u, v = indirect(v, true)
if u != nil {
return u.UnmarshalDynamoDBAttributeValue(av)
}
return d.decodeNull(v)
}
u, v = indirect(v, false)
if u != nil {
return u.UnmarshalDynamoDBAttributeValue(av)
}
switch {
case len(av.B) != 0:
return d.decodeBinary(av.B, v)
case av.BOOL != nil:
return d.decodeBool(av.BOOL, v)
case len(av.BS) != 0:
return d.decodeBinarySet(av.BS, v)
case len(av.L) != 0:
return d.decodeList(av.L, v)
case len(av.M) != 0:
return d.decodeMap(av.M, v)
case av.N != nil:
return d.decodeNumber(av.N, v, fieldTag)
case len(av.NS) != 0:
return d.decodeNumberSet(av.NS, v)
case av.S != nil:
return d.decodeString(av.S, v, fieldTag)
case len(av.SS) != 0:
return d.decodeStringSet(av.SS, v)
}
return nil
}
func (d *Decoder) decodeBinary(b []byte, v reflect.Value) error {
if v.Kind() == reflect.Interface {
buf := make([]byte, len(b))
copy(buf, b)
v.Set(reflect.ValueOf(buf))
return nil
}
if v.Kind() != reflect.Slice {
return &UnmarshalTypeError{Value: "binary", Type: v.Type()}
}
if v.Type() == byteSliceType {
// Optimization for []byte types
if v.IsNil() || v.Cap() < len(b) {
v.Set(reflect.MakeSlice(byteSliceType, len(b), len(b)))
} else if v.Len() != len(b) {
v.SetLen(len(b))
}
copy(v.Interface().([]byte), b)
return nil
}
switch v.Type().Elem().Kind() {
case reflect.Uint8:
// Fallback to reflection copy for type aliased of []byte type
if v.IsNil() || v.Cap() < len(b) {
v.Set(reflect.MakeSlice(v.Type(), len(b), len(b)))
} else if v.Len() != len(b) {
v.SetLen(len(b))
}
for i := 0; i < len(b); i++ {
v.Index(i).SetUint(uint64(b[i]))
}
default:
if v.Kind() == reflect.Array && v.Type().Elem().Kind() == reflect.Uint8 {
reflect.Copy(v, reflect.ValueOf(b))
break
}
return &UnmarshalTypeError{Value: "binary", Type: v.Type()}
}
return nil
}
func (d *Decoder) decodeBool(b *bool, v reflect.Value) error {
switch v.Kind() {
case reflect.Bool, reflect.Interface:
v.Set(reflect.ValueOf(*b).Convert(v.Type()))
default:
return &UnmarshalTypeError{Value: "bool", Type: v.Type()}
}
return nil
}
func (d *Decoder) decodeBinarySet(bs [][]byte, v reflect.Value) error {
switch v.Kind() {
case reflect.Slice:
// Make room for the slice elements if needed
if v.IsNil() || v.Cap() < len(bs) {
// What about if ignoring nil/empty values?
v.Set(reflect.MakeSlice(v.Type(), 0, len(bs)))
}
case reflect.Array:
// Limited to capacity of existing array.
case reflect.Interface:
set := make([][]byte, len(bs))
for i, b := range bs {
if err := d.decodeBinary(b, reflect.ValueOf(&set[i]).Elem()); err != nil {
return err
}
}
v.Set(reflect.ValueOf(set))
return nil
default:
return &UnmarshalTypeError{Value: "binary set", Type: v.Type()}
}
for i := 0; i < v.Cap() && i < len(bs); i++ {
v.SetLen(i + 1)
u, elem := indirect(v.Index(i), false)
if u != nil {
return u.UnmarshalDynamoDBAttributeValue(&dynamodb.AttributeValue{BS: bs})
}
if err := d.decodeBinary(bs[i], elem); err != nil {
return err
}
}
return nil
}
func (d *Decoder) decodeNumber(n *string, v reflect.Value, fieldTag tag) error {
switch v.Kind() {
case reflect.Interface:
i, err := d.decodeNumberToInterface(n)
if err != nil {
return err
}
v.Set(reflect.ValueOf(i))
return nil
case reflect.String:
if v.Type() == numberType { // Support Number value type
v.Set(reflect.ValueOf(Number(*n)))
return nil
}
v.Set(reflect.ValueOf(*n))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i, err := strconv.ParseInt(*n, 10, 64)
if err != nil {
return err
}
if v.OverflowInt(i) {
return &UnmarshalTypeError{
Value: fmt.Sprintf("number overflow, %s", *n),
Type: v.Type(),
}
}
v.SetInt(i)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
i, err := strconv.ParseUint(*n, 10, 64)
if err != nil {
return err
}
if v.OverflowUint(i) {
return &UnmarshalTypeError{
Value: fmt.Sprintf("number overflow, %s", *n),
Type: v.Type(),
}
}
v.SetUint(i)
case reflect.Float32, reflect.Float64:
i, err := strconv.ParseFloat(*n, 64)
if err != nil {
return err
}
if v.OverflowFloat(i) {
return &UnmarshalTypeError{
Value: fmt.Sprintf("number overflow, %s", *n),
Type: v.Type(),
}
}
v.SetFloat(i)
default:
if _, ok := v.Interface().(time.Time); ok && fieldTag.AsUnixTime {
t, err := decodeUnixTime(*n)
if err != nil {
return err
}
v.Set(reflect.ValueOf(t))
return nil
}
return &UnmarshalTypeError{Value: "number", Type: v.Type()}
}
return nil
}
func (d *Decoder) decodeNumberToInterface(n *string) (interface{}, error) {
if d.UseNumber {
return Number(*n), nil
}
// Default to float64 for all numbers
return strconv.ParseFloat(*n, 64)
}
func (d *Decoder) decodeNumberSet(ns []*string, v reflect.Value) error {
switch v.Kind() {
case reflect.Slice:
// Make room for the slice elements if needed
if v.IsNil() || v.Cap() < len(ns) {
// What about if ignoring nil/empty values?
v.Set(reflect.MakeSlice(v.Type(), 0, len(ns)))
}
case reflect.Array:
// Limited to capacity of existing array.
case reflect.Interface:
if d.UseNumber {
set := make([]Number, len(ns))
for i, n := range ns {
if err := d.decodeNumber(n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil {
return err
}
}
v.Set(reflect.ValueOf(set))
} else {
set := make([]float64, len(ns))
for i, n := range ns {
if err := d.decodeNumber(n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil {
return err
}
}
v.Set(reflect.ValueOf(set))
}
return nil
default:
return &UnmarshalTypeError{Value: "number set", Type: v.Type()}
}
for i := 0; i < v.Cap() && i < len(ns); i++ {
v.SetLen(i + 1)
u, elem := indirect(v.Index(i), false)
if u != nil {
return u.UnmarshalDynamoDBAttributeValue(&dynamodb.AttributeValue{NS: ns})
}
if err := d.decodeNumber(ns[i], elem, tag{}); err != nil {
return err
}
}
return nil
}
func (d *Decoder) decodeList(avList []*dynamodb.AttributeValue, v reflect.Value) error {
switch v.Kind() {
case reflect.Slice:
// Make room for the slice elements if needed
if v.IsNil() || v.Cap() < len(avList) {
// What about if ignoring nil/empty values?
v.Set(reflect.MakeSlice(v.Type(), 0, len(avList)))
}
case reflect.Array:
// Limited to capacity of existing array.
case reflect.Interface:
s := make([]interface{}, len(avList))
for i, av := range avList {
if err := d.decode(av, reflect.ValueOf(&s[i]).Elem(), tag{}); err != nil {
return err
}
}
v.Set(reflect.ValueOf(s))
return nil
default:
return &UnmarshalTypeError{Value: "list", Type: v.Type()}
}
// If v is not a slice, array
for i := 0; i < v.Cap() && i < len(avList); i++ {
v.SetLen(i + 1)
if err := d.decode(avList[i], v.Index(i), tag{}); err != nil {
return err
}
}
return nil
}
func (d *Decoder) decodeMap(avMap map[string]*dynamodb.AttributeValue, v reflect.Value) error {
switch v.Kind() {
case reflect.Map:
t := v.Type()
if t.Key().Kind() != reflect.String {
return &UnmarshalTypeError{Value: "map string key", Type: t.Key()}
}
if v.IsNil() {
v.Set(reflect.MakeMap(t))
}
case reflect.Struct:
case reflect.Interface:
v.Set(reflect.MakeMap(stringInterfaceMapType))
v = v.Elem()
default:
return &UnmarshalTypeError{Value: "map", Type: v.Type()}
}
if v.Kind() == reflect.Map {
for k, av := range avMap {
key := reflect.ValueOf(k)
elem := reflect.New(v.Type().Elem()).Elem()
if err := d.decode(av, elem, tag{}); err != nil {
return err
}
v.SetMapIndex(key, elem)
}
} else if v.Kind() == reflect.Struct {
fields := unionStructFields(v.Type(), d.MarshalOptions)
for k, av := range avMap {
if f, ok := fieldByName(fields, k); ok {
fv := fieldByIndex(v, f.Index, func(v *reflect.Value) bool {
v.Set(reflect.New(v.Type().Elem()))
return true // to continue the loop.
})
if err := d.decode(av, fv, f.tag); err != nil {
return err
}
}
}
}
return nil
}
func (d *Decoder) decodeNull(v reflect.Value) error {
if v.IsValid() && v.CanSet() {
v.Set(reflect.Zero(v.Type()))
}
return nil
}
func (d *Decoder) decodeString(s *string, v reflect.Value, fieldTag tag) error {
if fieldTag.AsString {
return d.decodeNumber(s, v, fieldTag)
}
// To maintain backwards compatibility with ConvertFrom family of methods which
// converted strings to time.Time structs
if _, ok := v.Interface().(time.Time); ok {
t, err := time.Parse(time.RFC3339, *s)
if err != nil {
return err
}
v.Set(reflect.ValueOf(t))
return nil
}
switch v.Kind() {
case reflect.String:
v.SetString(*s)
case reflect.Interface:
// Ensure type aliasing is handled properly
v.Set(reflect.ValueOf(*s).Convert(v.Type()))
default:
return &UnmarshalTypeError{Value: "string", Type: v.Type()}
}
return nil
}
func (d *Decoder) decodeStringSet(ss []*string, v reflect.Value) error {
switch v.Kind() {
case reflect.Slice:
// Make room for the slice elements if needed
if v.IsNil() || v.Cap() < len(ss) {
v.Set(reflect.MakeSlice(v.Type(), 0, len(ss)))
}
case reflect.Array:
// Limited to capacity of existing array.
case reflect.Interface:
set := make([]string, len(ss))
for i, s := range ss {
if err := d.decodeString(s, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil {
return err
}
}
v.Set(reflect.ValueOf(set))
return nil
default:
return &UnmarshalTypeError{Value: "string set", Type: v.Type()}
}
for i := 0; i < v.Cap() && i < len(ss); i++ {
v.SetLen(i + 1)
u, elem := indirect(v.Index(i), false)
if u != nil {
return u.UnmarshalDynamoDBAttributeValue(&dynamodb.AttributeValue{SS: ss})
}
if err := d.decodeString(ss[i], elem, tag{}); err != nil {
return err
}
}
return nil
}
func decodeUnixTime(n string) (time.Time, error) {
v, err := strconv.ParseInt(n, 10, 64)
if err != nil {
return time.Time{}, &UnmarshalError{
Err: err, Value: n, Type: reflect.TypeOf(time.Time{}),
}
}
return time.Unix(v, 0), nil
}
// indirect will walk a value's interface or pointer value types. Returning
// the final value or the value a unmarshaler is defined on.
//
// Based on the enoding/json type reflect value type indirection in Go Stdlib
// https://golang.org/src/encoding/json/decode.go indirect func.
func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, reflect.Value) {
if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() {
v = v.Addr()
}
for {
if v.Kind() == reflect.Interface && !v.IsNil() {
e := v.Elem()
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
v = e
continue
}
}
if v.Kind() != reflect.Ptr {
break
}
if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() {
break
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
if v.Type().NumMethod() > 0 {
if u, ok := v.Interface().(Unmarshaler); ok {
return u, reflect.Value{}
}
}
v = v.Elem()
}
return nil, v
}
// A Number represents a Attributevalue number literal.
type Number string
// Float64 attempts to cast the number ot a float64, returning
// the result of the case or error if the case failed.
func (n Number) Float64() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}
// Int64 attempts to cast the number ot a int64, returning
// the result of the case or error if the case failed.
func (n Number) Int64() (int64, error) {
return strconv.ParseInt(string(n), 10, 64)
}
// Uint64 attempts to cast the number ot a uint64, returning
// the result of the case or error if the case failed.
func (n Number) Uint64() (uint64, error) {
return strconv.ParseUint(string(n), 10, 64)
}
// String returns the raw number represented as a string
func (n Number) String() string {
return string(n)
}
type emptyOrigError struct{}
func (e emptyOrigError) OrigErr() error {
return nil
}
// An UnmarshalTypeError is an error type representing a error
// unmarshaling the AttributeValue's element to a Go value type.
// Includes details about the AttributeValue type and Go value type.
type UnmarshalTypeError struct {
emptyOrigError
Value string
Type reflect.Type
}
// Error returns the string representation of the error.
// satisfying the error interface
func (e *UnmarshalTypeError) Error() string {
return fmt.Sprintf("%s: %s", e.Code(), e.Message())
}
// Code returns the code of the error, satisfying the awserr.Error
// interface.
func (e *UnmarshalTypeError) Code() string {
return "UnmarshalTypeError"
}
// Message returns the detailed message of the error, satisfying
// the awserr.Error interface.
func (e *UnmarshalTypeError) Message() string {
return "cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
}
// An InvalidUnmarshalError is an error type representing an invalid type
// encountered while unmarshaling a AttributeValue to a Go value type.
type InvalidUnmarshalError struct {
emptyOrigError
Type reflect.Type
}
// Error returns the string representation of the error.
// satisfying the error interface
func (e *InvalidUnmarshalError) Error() string {
return fmt.Sprintf("%s: %s", e.Code(), e.Message())
}
// Code returns the code of the error, satisfying the awserr.Error
// interface.
func (e *InvalidUnmarshalError) Code() string {
return "InvalidUnmarshalError"
}
// Message returns the detailed message of the error, satisfying
// the awserr.Error interface.
func (e *InvalidUnmarshalError) Message() string {
if e.Type == nil {
return "cannot unmarshal to nil value"
}
if e.Type.Kind() != reflect.Ptr {
return "cannot unmarshal to non-pointer value, got " + e.Type.String()
}
return "cannot unmarshal to nil value, " + e.Type.String()
}
// An UnmarshalError wraps an error that occured while unmarshaling a DynamoDB
// AttributeValue element into a Go type. This is different from UnmarshalTypeError
// in that it wraps the underlying error that occured.
type UnmarshalError struct {
Err error
Value string
Type reflect.Type
}
// Error returns the string representation of the error.
// satisfying the error interface.
func (e *UnmarshalError) Error() string {
return fmt.Sprintf("%s: %s\ncaused by: %v", e.Code(), e.Message(), e.Err)
}
// OrigErr returns the original error that caused this issue.
func (e UnmarshalError) OrigErr() error {
return e.Err
}
// Code returns the code of the error, satisfying the awserr.Error
// interface.
func (e *UnmarshalError) Code() string {
return "UnmarshalError"
}
// Message returns the detailed message of the error, satisfying
// the awserr.Error interface.
func (e *UnmarshalError) Message() string {
return fmt.Sprintf("cannot unmarshal %q into %s.",
e.Value, e.Type.String())
}

View file

@ -0,0 +1,529 @@
package dynamodbattribute
import (
"fmt"
"reflect"
"strconv"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/stretchr/testify/assert"
)
func TestUnmarshalErrorTypes(t *testing.T) {
var _ awserr.Error = (*UnmarshalTypeError)(nil)
var _ awserr.Error = (*InvalidUnmarshalError)(nil)
}
func TestUnmarshalShared(t *testing.T) {
for i, c := range sharedTestCases {
err := Unmarshal(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
func TestUnmarshal(t *testing.T) {
cases := []struct {
in *dynamodb.AttributeValue
actual, expected interface{}
err error
}{
//------------
// Sets
//------------
{
in: &dynamodb.AttributeValue{BS: [][]byte{
{48, 49}, {50, 51},
}},
actual: &[][]byte{},
expected: [][]byte{{48, 49}, {50, 51}},
},
{
in: &dynamodb.AttributeValue{NS: []*string{
aws.String("123"), aws.String("321"),
}},
actual: &[]int{},
expected: []int{123, 321},
},
{
in: &dynamodb.AttributeValue{NS: []*string{
aws.String("123"), aws.String("321"),
}},
actual: &[]interface{}{},
expected: []interface{}{123., 321.},
},
{
in: &dynamodb.AttributeValue{SS: []*string{
aws.String("abc"), aws.String("123"),
}},
actual: &[]string{},
expected: &[]string{"abc", "123"},
},
{
in: &dynamodb.AttributeValue{SS: []*string{
aws.String("abc"), aws.String("123"),
}},
actual: &[]*string{},
expected: &[]*string{aws.String("abc"), aws.String("123")},
},
//------------
// Interfaces
//------------
{
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: []byte{48, 49},
},
{
in: &dynamodb.AttributeValue{BS: [][]byte{
{48, 49}, {50, 51},
}},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: [][]byte{{48, 49}, {50, 51}},
},
{
in: &dynamodb.AttributeValue{BOOL: aws.Bool(true)},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: bool(true),
},
{
in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
{S: aws.String("abc")}, {S: aws.String("123")},
}},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: []interface{}{"abc", "123"},
},
{
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"123": {S: aws.String("abc")},
"abc": {S: aws.String("123")},
}},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: map[string]interface{}{"123": "abc", "abc": "123"},
},
{
in: &dynamodb.AttributeValue{N: aws.String("123")},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: float64(123),
},
{
in: &dynamodb.AttributeValue{NS: []*string{
aws.String("123"), aws.String("321"),
}},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: []float64{123., 321.},
},
{
in: &dynamodb.AttributeValue{S: aws.String("123")},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: "123",
},
{
in: &dynamodb.AttributeValue{SS: []*string{
aws.String("123"), aws.String("321"),
}},
actual: func() interface{} {
var v interface{}
return &v
}(),
expected: []string{"123", "321"},
},
{
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("123")},
"Cba": {S: aws.String("321")},
}},
actual: &struct{ Abc, Cba string }{},
expected: struct{ Abc, Cba string }{Abc: "123", Cba: "321"},
},
{
in: &dynamodb.AttributeValue{N: aws.String("512")},
actual: new(uint8),
err: &UnmarshalTypeError{
Value: fmt.Sprintf("number overflow, 512"),
Type: reflect.TypeOf(uint8(0)),
},
},
}
for i, c := range cases {
err := Unmarshal(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
func TestInterfaceInput(t *testing.T) {
var v interface{}
expected := []interface{}{"abc", "123"}
err := Unmarshal(&dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
{S: aws.String("abc")}, {S: aws.String("123")},
}}, &v)
assertConvertTest(t, 0, v, expected, err, nil)
}
func TestUnmarshalError(t *testing.T) {
cases := []struct {
in *dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{
in: &dynamodb.AttributeValue{},
actual: int(0),
expected: nil,
err: &InvalidUnmarshalError{Type: reflect.TypeOf(int(0))},
},
}
for i, c := range cases {
err := Unmarshal(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
func TestUnmarshalListShared(t *testing.T) {
for i, c := range sharedListTestCases {
err := UnmarshalList(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
func TestUnmarshalListError(t *testing.T) {
cases := []struct {
in []*dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{
in: []*dynamodb.AttributeValue{},
actual: []interface{}{},
expected: nil,
err: &InvalidUnmarshalError{Type: reflect.TypeOf([]interface{}{})},
},
}
for i, c := range cases {
err := UnmarshalList(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
func TestUnmarshalMapShared(t *testing.T) {
for i, c := range sharedMapTestCases {
err := UnmarshalMap(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
func TestUnmarshalMapError(t *testing.T) {
cases := []struct {
in map[string]*dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{
in: map[string]*dynamodb.AttributeValue{},
actual: map[string]interface{}{},
expected: nil,
err: &InvalidUnmarshalError{Type: reflect.TypeOf(map[string]interface{}{})},
},
{
in: map[string]*dynamodb.AttributeValue{
"BOOL": {BOOL: aws.Bool(true)},
},
actual: &map[int]interface{}{},
expected: nil,
err: &UnmarshalTypeError{Value: "map string key", Type: reflect.TypeOf(int(0))},
},
}
for i, c := range cases {
err := UnmarshalMap(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
func TestUnmarshalListOfMaps(t *testing.T) {
type testItem struct {
Value string
Value2 int
}
cases := []struct {
in []map[string]*dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{ // Simple map conversion.
in: []map[string]*dynamodb.AttributeValue{
{
"Value": &dynamodb.AttributeValue{
BOOL: aws.Bool(true),
},
},
},
actual: &[]map[string]interface{}{},
expected: []map[string]interface{}{
{
"Value": true,
},
},
},
{ // attribute to struct.
in: []map[string]*dynamodb.AttributeValue{
{
"Value": &dynamodb.AttributeValue{
S: aws.String("abc"),
},
"Value2": &dynamodb.AttributeValue{
N: aws.String("123"),
},
},
},
actual: &[]testItem{},
expected: []testItem{
{
Value: "abc",
Value2: 123,
},
},
},
}
for i, c := range cases {
err := UnmarshalListOfMaps(c.in, c.actual)
assertConvertTest(t, i, c.actual, c.expected, err, c.err)
}
}
type unmarshalUnmarshaler struct {
Value string
Value2 int
Value3 bool
Value4 time.Time
}
func (u *unmarshalUnmarshaler) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
if av.M == nil {
return fmt.Errorf("expected AttributeValue to be map")
}
if v, ok := av.M["abc"]; !ok {
return fmt.Errorf("expected `abc` map key")
} else if v.S == nil {
return fmt.Errorf("expected `abc` map value string")
} else {
u.Value = *v.S
}
if v, ok := av.M["def"]; !ok {
return fmt.Errorf("expected `def` map key")
} else if v.N == nil {
return fmt.Errorf("expected `def` map value number")
} else {
n, err := strconv.ParseInt(*v.N, 10, 64)
if err != nil {
return err
}
u.Value2 = int(n)
}
if v, ok := av.M["ghi"]; !ok {
return fmt.Errorf("expected `ghi` map key")
} else if v.BOOL == nil {
return fmt.Errorf("expected `ghi` map value number")
} else {
u.Value3 = *v.BOOL
}
if v, ok := av.M["jkl"]; !ok {
return fmt.Errorf("expected `jkl` map key")
} else if v.S == nil {
return fmt.Errorf("expected `jkl` map value string")
} else {
t, err := time.Parse(time.RFC3339, *v.S)
if err != nil {
return err
}
u.Value4 = t
}
return nil
}
func TestUnmarshalUnmashaler(t *testing.T) {
u := &unmarshalUnmarshaler{}
av := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("value")},
"def": {N: aws.String("123")},
"ghi": {BOOL: aws.Bool(true)},
"jkl": {S: aws.String("2016-05-03T17:06:26.209072Z")},
},
}
err := Unmarshal(av, u)
assert.NoError(t, err)
assert.Equal(t, "value", u.Value)
assert.Equal(t, 123, u.Value2)
assert.Equal(t, true, u.Value3)
assert.Equal(t, testDate, u.Value4)
}
func TestDecodeUseNumber(t *testing.T) {
u := map[string]interface{}{}
av := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("value")},
"def": {N: aws.String("123")},
"ghi": {BOOL: aws.Bool(true)},
},
}
decoder := NewDecoder(func(d *Decoder) {
d.UseNumber = true
})
err := decoder.Decode(av, &u)
assert.NoError(t, err)
assert.Equal(t, "value", u["abc"])
n, ok := u["def"].(Number)
assert.True(t, ok)
assert.Equal(t, "123", n.String())
assert.Equal(t, true, u["ghi"])
}
func TestDecodeUseNumberNumberSet(t *testing.T) {
u := map[string]interface{}{}
av := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"ns": {
NS: []*string{
aws.String("123"), aws.String("321"),
},
},
},
}
decoder := NewDecoder(func(d *Decoder) {
d.UseNumber = true
})
err := decoder.Decode(av, &u)
assert.NoError(t, err)
ns, ok := u["ns"].([]Number)
assert.True(t, ok)
assert.Equal(t, "123", ns[0].String())
assert.Equal(t, "321", ns[1].String())
}
func TestDecodeEmbeddedPointerStruct(t *testing.T) {
type B struct {
Bint int
}
type C struct {
Cint int
}
type A struct {
Aint int
*B
*C
}
av := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Aint": {
N: aws.String("321"),
},
"Bint": {
N: aws.String("123"),
},
},
}
decoder := NewDecoder()
a := A{}
err := decoder.Decode(av, &a)
assert.NoError(t, err)
assert.Equal(t, 321, a.Aint)
// Embedded pointer struct can be created automatically.
assert.Equal(t, 123, a.Bint)
// But not for absent fields.
assert.Nil(t, a.C)
}
func TestDecodeBooleanOverlay(t *testing.T) {
type BooleanOverlay bool
av := &dynamodb.AttributeValue{
BOOL: aws.Bool(true),
}
decoder := NewDecoder()
var v BooleanOverlay
err := decoder.Decode(av, &v)
assert.NoError(t, err)
assert.Equal(t, BooleanOverlay(true), v)
}
func TestDecodeUnixTime(t *testing.T) {
type A struct {
Normal time.Time
Tagged time.Time `dynamodbav:",unixtime"`
Typed UnixTime
}
expect := A{
Normal: time.Unix(123, 0).UTC(),
Tagged: time.Unix(456, 0),
Typed: UnixTime(time.Unix(789, 0)),
}
input := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Normal": {
S: aws.String("1970-01-01T00:02:03Z"),
},
"Tagged": {
N: aws.String("456"),
},
"Typed": {
N: aws.String("789"),
},
},
}
actual := A{}
err := Unmarshal(input, &actual)
assert.NoError(t, err)
assert.Equal(t, expect, actual)
}

View file

@ -0,0 +1,95 @@
// Package dynamodbattribute provides marshaling and unmarshaling utilities to
// convert between Go types and dynamodb.AttributeValues.
//
// These utilities allow you to marshal slices, maps, structs, and scalar values
// to and from dynamodb.AttributeValue. These are useful when marshaling
// Go value tyes to dynamodb.AttributeValue for DynamoDB requests, or
// unmarshaling the dynamodb.AttributeValue back into a Go value type.
//
// AttributeValue Marshaling
//
// To marshal a Go type to a dynamodbAttributeValue you can use the Marshal
// functions in the dynamodbattribute package. There are specialized versions
// of these functions for collections of Attributevalue, such as maps and lists.
//
// The following example uses MarshalMap to convert the Record Go type to a
// dynamodb.AttributeValue type and use the value to make a PutItem API request.
//
// type Record struct {
// ID string
// URLs []string
// }
//
// //...
//
// r := Record{
// ID: "ABC123",
// URLs: []string{
// "https://example.com/first/link",
// "https://example.com/second/url",
// },
// }
// av, err := dynamodbattribute.MarshalMap(r)
// if err != nil {
// panic(fmt.Sprintf("failed to DynamoDB marshal Record, %v", err))
// }
//
// _, err := r.svc.PutItem(&dynamodb.PutItemInput{
// TableName: aws.String(myTableName),
// Item: av,
// })
// if err != nil {
// panic(fmt.Sprintf("failed to put Record to DynamoDB, %v", err))
// }
//
// AttributeValue Unmarshaling
//
// To unmarshal a dynamodb.AttributeValue to a Go type you can use the Unmarshal
// functions in the dynamodbattribute package. There are specialized versions
// of these functions for collections of Attributevalue, such as maps and lists.
//
// The following example will unmarshal the DynamoDB's Scan API operation. The
// Items returned by the operation will be unmarshaled into the slice of Records
// Go type.
//
// type Record struct {
// ID string
// URLs []string
// }
//
// //...
//
// var records []Record
//
// // Use the ScanPages method to perform the scan with pagination. Use
// // just Scan method to make the API call without pagination.
// err := svc.ScanPages(&dynamodb.ScanInput{
// TableName: aws.String(myTableName),
// }, func(page *dynamodb.ScanOutput, last bool) bool {
// recs := []Record{}
//
// err := dynamodbattribute.UnmarshalListOfMaps(page.Items, &recs)
// if err != nil {
// panic(fmt.Sprintf("failed to unmarshal Dynamodb Scan Items, %v", err))
// }
//
// records = append(records, recs...)
//
// return true // keep paging
// })
//
// The ConvertTo, ConvertToList, ConvertToMap, ConvertFrom, ConvertFromMap
// and ConvertFromList methods have been deprecated. The Marshal and Unmarshal
// functions should be used instead. The ConvertTo|From marshallers do not
// support BinarySet, NumberSet, nor StringSets, and will incorrect marshal
// binary data fields in structs as base64 strings.
//
// The Marshal and Unmarshal functions correct this behavior, and removes
// the reliance on encoding.json. `json` struct tags are still supported. In
// addition support for a new struct tag `dynamodbav` was added. Support for
// the json.Marshaler and json.Unmarshaler interfaces have been removed and
// replaced with have been replaced with dynamodbattribute.Marshaler and
// dynamodbattribute.Unmarshaler interfaces.
//
// `time.Time` is marshaled as RFC3339 format.
package dynamodbattribute

View file

@ -0,0 +1,636 @@
package dynamodbattribute
import (
"fmt"
"reflect"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// An UnixTime provides aliasing of time.Time into a type that when marshaled
// and unmarshaled with DynamoDB AttributeValues it will be done so as number
// instead of string in seconds since January 1, 1970 UTC.
//
// This type is useful as an alterntitive to the struct tag `unixtime` when you
// want to have your time value marshaled as Unix time in seconds intead of
// the default time.RFC3339.
//
// Important to note that zero value time as unixtime is not 0 seconds
// from January 1, 1970 UTC, but -62135596800. Which is seconds between
// January 1, 0001 UTC, and January 1, 0001 UTC.
type UnixTime time.Time
// MarshalDynamoDBAttributeValue implements the Marshaler interface so that
// the UnixTime can be marshaled from to a DynamoDB AttributeValue number
// value encoded in the number of seconds since January 1, 1970 UTC.
func (e UnixTime) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
t := time.Time(e)
s := strconv.FormatInt(t.Unix(), 10)
av.N = &s
return nil
}
// UnmarshalDynamoDBAttributeValue implements the Unmarshaler interface so that
// the UnixTime can be unmarshaled from a DynamoDB AttributeValue number representing
// the number of seconds since January 1, 1970 UTC.
//
// If an error parsing the AttributeValue number occurs UnmarshalError will be
// returned.
func (e *UnixTime) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
t, err := decodeUnixTime(aws.StringValue(av.N))
if err != nil {
return err
}
*e = UnixTime(t)
return nil
}
// A Marshaler is an interface to provide custom marshaling of Go value types
// to AttributeValues. Use this to provide custom logic determining how a
// Go Value type should be marshaled.
//
// type ExampleMarshaler struct {
// Value int
// }
// func (m *ExampleMarshaler) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
// n := fmt.Sprintf("%v", m.Value)
// av.N = &n
// return nil
// }
//
type Marshaler interface {
MarshalDynamoDBAttributeValue(*dynamodb.AttributeValue) error
}
// Marshal will serialize the passed in Go value type into a DynamoDB AttributeValue
// type. This value can be used in DynamoDB API operations to simplify marshaling
// your Go value types into AttributeValues.
//
// Marshal will recursively transverse the passed in value marshaling its
// contents into a AttributeValue. Marshal supports basic scalars
// (int,uint,float,bool,string), maps, slices, and structs. Anonymous
// nested types are flattened based on Go anonymous type visibility.
//
// Marshaling slices to AttributeValue will default to a List for all
// types except for []byte and [][]byte. []byte will be marshaled as
// Binary data (B), and [][]byte will be marshaled as binary data set
// (BS).
//
// `dynamodbav` struct tag can be used to control how the value will be
// marshaled into a AttributeValue.
//
// // Field is ignored
// Field int `dynamodbav:"-"`
//
// // Field AttributeValue map key "myName"
// Field int `dynamodbav:"myName"`
//
// // Field AttributeValue map key "myName", and
// // Field is omitted if it is empty
// Field int `dynamodbav:"myName,omitempty"`
//
// // Field AttributeValue map key "Field", and
// // Field is omitted if it is empty
// Field int `dynamodbav:",omitempty"`
//
// // Field's elems will be omitted if empty
// // only valid for slices, and maps.
// Field []string `dynamodbav:",omitemptyelem"`
//
// // Field will be marshaled as a AttributeValue string
// // only value for number types, (int,uint,float)
// Field int `dynamodbav:",string"`
//
// // Field will be marshaled as a binary set
// Field [][]byte `dynamodbav:",binaryset"`
//
// // Field will be marshaled as a number set
// Field []int `dynamodbav:",numberset"`
//
// // Field will be marshaled as a string set
// Field []string `dynamodbav:",stringset"`
//
// // Field will be marshaled as Unix time number in seconds.
// // This tag is only valid with time.Time typed struct fields.
// // Important to note that zero value time as unixtime is not 0 seconds
// // from January 1, 1970 UTC, but -62135596800. Which is seconds between
// // January 1, 0001 UTC, and January 1, 0001 UTC.
// Field time.Time `dynamodbav:",unixtime"`
//
// The omitempty tag is only used during Marshaling and is ignored for
// Unmarshal. Any zero value or a value when marshaled results in a
// AttributeValue NULL will be added to AttributeValue Maps during struct
// marshal. The omitemptyelem tag works the same as omitempty except it
// applies to maps and slices instead of struct fields, and will not be
// included in the marshaled AttributeValue Map, List, or Set.
//
// For convenience and backwards compatibility with ConvertTo functions
// json struct tags are supported by the Marshal and Unmarshal. If
// both json and dynamodbav struct tags are provided the json tag will
// be ignored in favor of dynamodbav.
//
// All struct fields and with anonymous fields, are marshaled unless the
// any of the following conditions are meet.
//
// - the field is not exported
// - json or dynamodbav field tag is "-"
// - json or dynamodbav field tag specifies "omitempty", and is empty.
//
// Pointer and interfaces values encode as the value pointed to or contained
// in the interface. A nil value encodes as the AttributeValue NULL value.
//
// Channel, complex, and function values are not encoded and will be skipped
// when walking the value to be marshaled.
//
// When marshaling any error that occurs will halt the marshal and return
// the error.
//
// Marshal cannot represent cyclic data structures and will not handle them.
// Passing cyclic structures to Marshal will result in an infinite recursion.
func Marshal(in interface{}) (*dynamodb.AttributeValue, error) {
return NewEncoder().Encode(in)
}
// MarshalMap is an alias for Marshal func which marshals Go value
// type to a map of AttributeValues.
//
// This is useful for DynamoDB APIs such as PutItem.
func MarshalMap(in interface{}) (map[string]*dynamodb.AttributeValue, error) {
av, err := NewEncoder().Encode(in)
if err != nil || av == nil || av.M == nil {
return map[string]*dynamodb.AttributeValue{}, err
}
return av.M, nil
}
// MarshalList is an alias for Marshal func which marshals Go value
// type to a slice of AttributeValues.
func MarshalList(in interface{}) ([]*dynamodb.AttributeValue, error) {
av, err := NewEncoder().Encode(in)
if err != nil || av == nil || av.L == nil {
return []*dynamodb.AttributeValue{}, err
}
return av.L, nil
}
// A MarshalOptions is a collection of options shared between marshaling
// and unmarshaling
type MarshalOptions struct {
// States that the encoding/json struct tags should be supported.
// if a `dynamodbav` struct tag is also provided the encoding/json
// tag will be ignored.
//
// Enabled by default.
SupportJSONTags bool
}
// An Encoder provides marshaling Go value types to AttributeValues.
type Encoder struct {
MarshalOptions
// Empty strings, "", will be marked as NULL AttributeValue types.
// Empty strings are not valid values for DynamoDB. Will not apply
// to lists, sets, or maps. Use the struct tag `omitemptyelem`
// to skip empty (zero) values in lists, sets and maps.
//
// Enabled by default.
NullEmptyString bool
}
// NewEncoder creates a new Encoder with default configuration. Use
// the `opts` functional options to override the default configuration.
func NewEncoder(opts ...func(*Encoder)) *Encoder {
e := &Encoder{
MarshalOptions: MarshalOptions{
SupportJSONTags: true,
},
NullEmptyString: true,
}
for _, o := range opts {
o(e)
}
return e
}
// Encode will marshal a Go value type to an AttributeValue. Returning
// the AttributeValue constructed or error.
func (e *Encoder) Encode(in interface{}) (*dynamodb.AttributeValue, error) {
av := &dynamodb.AttributeValue{}
if err := e.encode(av, reflect.ValueOf(in), tag{}); err != nil {
return nil, err
}
return av, nil
}
func fieldByIndex(v reflect.Value, index []int,
OnEmbeddedNilStruct func(*reflect.Value) bool) reflect.Value {
fv := v
for i, x := range index {
if i > 0 {
if fv.Kind() == reflect.Ptr && fv.Type().Elem().Kind() == reflect.Struct {
if fv.IsNil() && !OnEmbeddedNilStruct(&fv) {
break
}
fv = fv.Elem()
}
}
fv = fv.Field(x)
}
return fv
}
func (e *Encoder) encode(av *dynamodb.AttributeValue, v reflect.Value, fieldTag tag) error {
// We should check for omitted values first before dereferencing.
if fieldTag.OmitEmpty && emptyValue(v) {
encodeNull(av)
return nil
}
// Handle both pointers and interface conversion into types
v = valueElem(v)
if v.Kind() != reflect.Invalid {
if used, err := tryMarshaler(av, v); used {
return err
}
}
switch v.Kind() {
case reflect.Invalid:
encodeNull(av)
case reflect.Struct:
return e.encodeStruct(av, v, fieldTag)
case reflect.Map:
return e.encodeMap(av, v, fieldTag)
case reflect.Slice, reflect.Array:
return e.encodeSlice(av, v, fieldTag)
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
// do nothing for unsupported types
default:
return e.encodeScalar(av, v, fieldTag)
}
return nil
}
func (e *Encoder) encodeStruct(av *dynamodb.AttributeValue, v reflect.Value, fieldTag tag) error {
// To maintain backwards compatibility with ConvertTo family of methods which
// converted time.Time structs to strings
if t, ok := v.Interface().(time.Time); ok {
if fieldTag.AsUnixTime {
return UnixTime(t).MarshalDynamoDBAttributeValue(av)
}
s := t.Format(time.RFC3339Nano)
av.S = &s
return nil
}
av.M = map[string]*dynamodb.AttributeValue{}
fields := unionStructFields(v.Type(), e.MarshalOptions)
for _, f := range fields {
if f.Name == "" {
return &InvalidMarshalError{msg: "map key cannot be empty"}
}
found := true
fv := fieldByIndex(v, f.Index, func(v *reflect.Value) bool {
found = false
return false // to break the loop.
})
if !found {
continue
}
elem := &dynamodb.AttributeValue{}
err := e.encode(elem, fv, f.tag)
if err != nil {
return err
}
skip, err := keepOrOmitEmpty(f.OmitEmpty, elem, err)
if err != nil {
return err
} else if skip {
continue
}
av.M[f.Name] = elem
}
if len(av.M) == 0 {
encodeNull(av)
}
return nil
}
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 == "" {
return &InvalidMarshalError{msg: "map key cannot be empty"}
}
elemVal := v.MapIndex(key)
elem := &dynamodb.AttributeValue{}
err := e.encode(elem, elemVal, tag{})
skip, err := keepOrOmitEmpty(fieldTag.OmitEmptyElem, elem, err)
if err != nil {
return err
} else if skip {
continue
}
av.M[keyName] = elem
}
if len(av.M) == 0 {
encodeNull(av)
}
return nil
}
func (e *Encoder) encodeSlice(av *dynamodb.AttributeValue, v reflect.Value, fieldTag tag) error {
switch v.Type().Elem().Kind() {
case reflect.Uint8:
b := v.Bytes()
if len(b) == 0 {
encodeNull(av)
return nil
}
av.B = append([]byte{}, b...)
default:
var elemFn func(dynamodb.AttributeValue) error
if fieldTag.AsBinSet || v.Type() == byteSliceSlicetype { // Binary Set
av.BS = make([][]byte, 0, v.Len())
elemFn = func(elem dynamodb.AttributeValue) error {
if elem.B == nil {
return &InvalidMarshalError{msg: "binary set must only contain non-nil byte slices"}
}
av.BS = append(av.BS, elem.B)
return nil
}
} else if fieldTag.AsNumSet { // Number Set
av.NS = make([]*string, 0, v.Len())
elemFn = func(elem dynamodb.AttributeValue) error {
if elem.N == nil {
return &InvalidMarshalError{msg: "number set must only contain non-nil string numbers"}
}
av.NS = append(av.NS, elem.N)
return nil
}
} else if fieldTag.AsStrSet { // String Set
av.SS = make([]*string, 0, v.Len())
elemFn = func(elem dynamodb.AttributeValue) error {
if elem.S == nil {
return &InvalidMarshalError{msg: "string set must only contain non-nil strings"}
}
av.SS = append(av.SS, elem.S)
return nil
}
} else { // List
av.L = make([]*dynamodb.AttributeValue, 0, v.Len())
elemFn = func(elem dynamodb.AttributeValue) error {
av.L = append(av.L, &elem)
return nil
}
}
if n, err := e.encodeList(v, fieldTag, elemFn); err != nil {
return err
} else if n == 0 {
encodeNull(av)
}
}
return nil
}
func (e *Encoder) encodeList(v reflect.Value, fieldTag tag, elemFn func(dynamodb.AttributeValue) error) (int, error) {
count := 0
for i := 0; i < v.Len(); i++ {
elem := dynamodb.AttributeValue{}
err := e.encode(&elem, v.Index(i), tag{OmitEmpty: fieldTag.OmitEmptyElem})
skip, err := keepOrOmitEmpty(fieldTag.OmitEmptyElem, &elem, err)
if err != nil {
return 0, err
} else if skip {
continue
}
if err := elemFn(elem); err != nil {
return 0, err
}
count++
}
return count, nil
}
func (e *Encoder) encodeScalar(av *dynamodb.AttributeValue, v reflect.Value, fieldTag tag) error {
if v.Type() == numberType {
s := v.String()
if fieldTag.AsString {
av.S = &s
} else {
av.N = &s
}
return nil
}
switch v.Kind() {
case reflect.Bool:
av.BOOL = new(bool)
*av.BOOL = v.Bool()
case reflect.String:
if err := e.encodeString(av, v); err != nil {
return err
}
default:
// Fallback to encoding numbers, will return invalid type if not supported
if err := e.encodeNumber(av, v); err != nil {
return err
}
if fieldTag.AsString && av.NULL == nil && av.N != nil {
av.S = av.N
av.N = nil
}
}
return nil
}
func (e *Encoder) encodeNumber(av *dynamodb.AttributeValue, v reflect.Value) error {
if used, err := tryMarshaler(av, v); used {
return err
}
var out string
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
out = encodeInt(v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
out = encodeUint(v.Uint())
case reflect.Float32, reflect.Float64:
out = encodeFloat(v.Float())
default:
return &unsupportedMarshalTypeError{Type: v.Type()}
}
av.N = &out
return nil
}
func (e *Encoder) encodeString(av *dynamodb.AttributeValue, v reflect.Value) error {
if used, err := tryMarshaler(av, v); used {
return err
}
switch v.Kind() {
case reflect.String:
s := v.String()
if len(s) == 0 && e.NullEmptyString {
encodeNull(av)
} else {
av.S = &s
}
default:
return &unsupportedMarshalTypeError{Type: v.Type()}
}
return nil
}
func encodeInt(i int64) string {
return strconv.FormatInt(i, 10)
}
func encodeUint(u uint64) string {
return strconv.FormatUint(u, 10)
}
func encodeFloat(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
func encodeNull(av *dynamodb.AttributeValue) {
t := true
*av = dynamodb.AttributeValue{NULL: &t}
}
func valueElem(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Interface, reflect.Ptr:
for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
v = v.Elem()
}
}
return v
}
func emptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
}
func tryMarshaler(av *dynamodb.AttributeValue, v reflect.Value) (bool, error) {
if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() {
v = v.Addr()
}
if v.Type().NumMethod() == 0 {
return false, nil
}
if m, ok := v.Interface().(Marshaler); ok {
return true, m.MarshalDynamoDBAttributeValue(av)
}
return false, nil
}
func keepOrOmitEmpty(omitEmpty bool, av *dynamodb.AttributeValue, err error) (bool, error) {
if err != nil {
if _, ok := err.(*unsupportedMarshalTypeError); ok {
return true, nil
}
return false, err
}
if av.NULL != nil && omitEmpty {
return true, nil
}
return false, nil
}
// An InvalidMarshalError is an error type representing an error
// occurring when marshaling a Go value type to an AttributeValue.
type InvalidMarshalError struct {
emptyOrigError
msg string
}
// Error returns the string representation of the error.
// satisfying the error interface
func (e *InvalidMarshalError) Error() string {
return fmt.Sprintf("%s: %s", e.Code(), e.Message())
}
// Code returns the code of the error, satisfying the awserr.Error
// interface.
func (e *InvalidMarshalError) Code() string {
return "InvalidMarshalError"
}
// Message returns the detailed message of the error, satisfying
// the awserr.Error interface.
func (e *InvalidMarshalError) Message() string {
return e.msg
}
// An unsupportedMarshalTypeError represents a Go value type
// which cannot be marshaled into an AttributeValue and should
// be skipped by the marshaler.
type unsupportedMarshalTypeError struct {
emptyOrigError
Type reflect.Type
}
// Error returns the string representation of the error.
// satisfying the error interface
func (e *unsupportedMarshalTypeError) Error() string {
return fmt.Sprintf("%s: %s", e.Code(), e.Message())
}
// Code returns the code of the error, satisfying the awserr.Error
// interface.
func (e *unsupportedMarshalTypeError) Code() string {
return "unsupportedMarshalTypeError"
}
// Message returns the detailed message of the error, satisfying
// the awserr.Error interface.
func (e *unsupportedMarshalTypeError) Message() string {
return "Go value type " + e.Type.String() + " is not supported"
}

View file

@ -0,0 +1,209 @@
package dynamodbattribute
import (
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/stretchr/testify/assert"
)
func TestMarshalErrorTypes(t *testing.T) {
var _ awserr.Error = (*InvalidMarshalError)(nil)
var _ awserr.Error = (*unsupportedMarshalTypeError)(nil)
}
func TestMarshalShared(t *testing.T) {
for i, c := range sharedTestCases {
av, err := Marshal(c.expected)
assertConvertTest(t, i, av, c.in, err, c.err)
}
}
func TestMarshalListShared(t *testing.T) {
for i, c := range sharedListTestCases {
av, err := MarshalList(c.expected)
assertConvertTest(t, i, av, c.in, err, c.err)
}
}
func TestMarshalMapShared(t *testing.T) {
for i, c := range sharedMapTestCases {
av, err := MarshalMap(c.expected)
assertConvertTest(t, i, av, c.in, err, c.err)
}
}
type marshalMarshaler struct {
Value string
Value2 int
Value3 bool
Value4 time.Time
}
func (m *marshalMarshaler) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
av.M = map[string]*dynamodb.AttributeValue{
"abc": {S: &m.Value},
"def": {N: aws.String(fmt.Sprintf("%d", m.Value2))},
"ghi": {BOOL: &m.Value3},
"jkl": {S: aws.String(m.Value4.Format(time.RFC3339Nano))},
}
return nil
}
func TestMarshalMashaler(t *testing.T) {
m := &marshalMarshaler{
Value: "value",
Value2: 123,
Value3: true,
Value4: testDate,
}
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("value")},
"def": {N: aws.String("123")},
"ghi": {BOOL: aws.Bool(true)},
"jkl": {S: aws.String("2016-05-03T17:06:26.209072Z")},
},
}
actual, err := Marshal(m)
assert.NoError(t, err)
assert.Equal(t, expect, actual)
}
type testOmitEmptyElemListStruct struct {
Values []string `dynamodbav:",omitemptyelem"`
}
type testOmitEmptyElemMapStruct struct {
Values map[string]interface{} `dynamodbav:",omitemptyelem"`
}
func TestMarshalListOmitEmptyElem(t *testing.T) {
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Values": {L: []*dynamodb.AttributeValue{
{S: aws.String("abc")},
{S: aws.String("123")},
}},
},
}
m := testOmitEmptyElemListStruct{Values: []string{"abc", "", "123"}}
actual, err := Marshal(m)
assert.NoError(t, err)
assert.Equal(t, expect, actual)
}
func TestMarshalMapOmitEmptyElem(t *testing.T) {
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Values": {M: map[string]*dynamodb.AttributeValue{
"abc": {N: aws.String("123")},
"klm": {S: aws.String("abc")},
}},
},
}
m := testOmitEmptyElemMapStruct{Values: map[string]interface{}{
"abc": 123.,
"efg": nil,
"hij": "",
"klm": "abc",
}}
actual, err := Marshal(m)
assert.NoError(t, err)
assert.Equal(t, expect, actual)
}
type testOmitEmptyScalar struct {
IntZero int `dynamodbav:",omitempty"`
IntPtrNil *int `dynamodbav:",omitempty"`
IntPtrSetZero *int `dynamodbav:",omitempty"`
}
func TestMarshalOmitEmpty(t *testing.T) {
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"IntPtrSetZero": {N: aws.String("0")},
},
}
m := testOmitEmptyScalar{IntPtrSetZero: aws.Int(0)}
actual, err := Marshal(m)
assert.NoError(t, err)
assert.Equal(t, expect, actual)
}
func TestEncodeEmbeddedPointerStruct(t *testing.T) {
type B struct {
Bint int
}
type C struct {
Cint int
}
type A struct {
Aint int
*B
*C
}
a := A{Aint: 321, B: &B{123}}
assert.Equal(t, 321, a.Aint)
assert.Equal(t, 123, a.Bint)
assert.Nil(t, a.C)
actual, err := Marshal(a)
assert.NoError(t, err)
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Aint": {
N: aws.String("321"),
},
"Bint": {
N: aws.String("123"),
},
},
}
assert.Equal(t, expect, actual)
}
func TestEncodeUnixTime(t *testing.T) {
type A struct {
Normal time.Time
Tagged time.Time `dynamodbav:",unixtime"`
Typed UnixTime
}
a := A{
Normal: time.Unix(123, 0).UTC(),
Tagged: time.Unix(456, 0),
Typed: UnixTime(time.Unix(789, 0)),
}
actual, err := Marshal(a)
assert.NoError(t, err)
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Normal": {
S: aws.String("1970-01-01T00:02:03Z"),
},
"Tagged": {
N: aws.String("456"),
},
"Typed": {
N: aws.String("789"),
},
},
}
assert.Equal(t, expect, actual)
}

View file

@ -0,0 +1,269 @@
package dynamodbattribute
import (
"reflect"
"sort"
"strings"
)
type field struct {
tag
Name string
NameFromTag bool
Index []int
Type reflect.Type
}
func fieldByName(fields []field, name string) (field, bool) {
foldExists := false
foldField := field{}
for _, f := range fields {
if f.Name == name {
return f, true
}
if !foldExists && strings.EqualFold(f.Name, name) {
foldField = f
foldExists = true
}
}
return foldField, foldExists
}
func buildField(pIdx []int, i int, sf reflect.StructField, fieldTag tag) field {
f := field{
Name: sf.Name,
Type: sf.Type,
tag: fieldTag,
}
if len(fieldTag.Name) != 0 {
f.NameFromTag = true
f.Name = fieldTag.Name
}
f.Index = make([]int, len(pIdx)+1)
copy(f.Index, pIdx)
f.Index[len(pIdx)] = i
return f
}
func unionStructFields(t reflect.Type, opts MarshalOptions) []field {
fields := enumFields(t, opts)
sort.Sort(fieldsByName(fields))
fields = visibleFields(fields)
return fields
}
// enumFields will recursively iterate through a structure and its nested
// anonymous fields.
//
// Based on the enoding/json struct field enumeration of the Go Stdlib
// https://golang.org/src/encoding/json/encode.go typeField func.
func enumFields(t reflect.Type, opts MarshalOptions) []field {
// Fields to explore
current := []field{}
next := []field{{Type: t}}
// count of queued names
count := map[reflect.Type]int{}
nextCount := map[reflect.Type]int{}
visited := map[reflect.Type]struct{}{}
fields := []field{}
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, map[reflect.Type]int{}
for _, f := range current {
if _, ok := visited[f.Type]; ok {
continue
}
visited[f.Type] = struct{}{}
for i := 0; i < f.Type.NumField(); i++ {
sf := f.Type.Field(i)
if sf.PkgPath != "" && !sf.Anonymous {
// Ignore unexported and non-anonymous fields
// unexported but anonymous field may still be used if
// the type has exported nested fields
continue
}
fieldTag := tag{}
fieldTag.parseAVTag(sf.Tag)
if opts.SupportJSONTags && fieldTag == (tag{}) {
fieldTag.parseJSONTag(sf.Tag)
}
if fieldTag.Ignore {
continue
}
ft := sf.Type
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
structField := buildField(f.Index, i, sf, fieldTag)
structField.Type = ft
if !sf.Anonymous || ft.Kind() != reflect.Struct {
fields = append(fields, structField)
if count[f.Type] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, structField)
}
continue
}
// Record new anon struct to explore next round
nextCount[ft]++
if nextCount[ft] == 1 {
next = append(next, structField)
}
}
}
}
return fields
}
// visibleFields will return a slice of fields which are visible based on
// Go's standard visiblity rules with the exception of ties being broken
// by depth and struct tag naming.
//
// Based on the enoding/json field filtering of the Go Stdlib
// https://golang.org/src/encoding/json/encode.go typeField func.
func visibleFields(fields []field) []field {
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with JSON tags are promoted.
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0]
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.Name
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.Name != name {
break
}
}
if advance == 1 { // Only one field with this name
out = append(out, fi)
continue
}
dominant, ok := dominantField(fields[i : i+advance])
if ok {
out = append(out, dominant)
}
}
fields = out
sort.Sort(fieldsByIndex(fields))
return fields
}
// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
//
// Based on the enoding/json field filtering of the Go Stdlib
// https://golang.org/src/encoding/json/encode.go dominantField func.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order. The winner
// must therefore be one with the shortest index length. Drop all
// longer entries, which is easy: just truncate the slice.
length := len(fields[0].Index)
tagged := -1 // Index of first tagged field.
for i, f := range fields {
if len(f.Index) > length {
fields = fields[:i]
break
}
if f.NameFromTag {
if tagged >= 0 {
// Multiple tagged fields at the same level: conflict.
// Return no field.
return field{}, false
}
tagged = i
}
}
if tagged >= 0 {
return fields[tagged], true
}
// All remaining fields have the same length. If there's more than one,
// we have a conflict (two fields named "X" at the same level) and we
// return no field.
if len(fields) > 1 {
return field{}, false
}
return fields[0], true
}
// fieldsByName sorts field by name, breaking ties with depth,
// then breaking ties with "name came from json tag", then
// breaking ties with index sequence.
//
// Based on the enoding/json field filtering of the Go Stdlib
// https://golang.org/src/encoding/json/encode.go fieldsByName type.
type fieldsByName []field
func (x fieldsByName) Len() int { return len(x) }
func (x fieldsByName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x fieldsByName) Less(i, j int) bool {
if x[i].Name != x[j].Name {
return x[i].Name < x[j].Name
}
if len(x[i].Index) != len(x[j].Index) {
return len(x[i].Index) < len(x[j].Index)
}
if x[i].NameFromTag != x[j].NameFromTag {
return x[i].NameFromTag
}
return fieldsByIndex(x).Less(i, j)
}
// fieldsByIndex sorts field by index sequence.
//
// Based on the enoding/json field filtering of the Go Stdlib
// https://golang.org/src/encoding/json/encode.go fieldsByIndex type.
type fieldsByIndex []field
func (x fieldsByIndex) Len() int { return len(x) }
func (x fieldsByIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x fieldsByIndex) Less(i, j int) bool {
for k, xik := range x[i].Index {
if k >= len(x[j].Index) {
return false
}
if xik != x[j].Index[k] {
return xik < x[j].Index[k]
}
}
return len(x[i].Index) < len(x[j].Index)
}

View file

@ -0,0 +1,110 @@
package dynamodbattribute
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
type testUnionValues struct {
Name string
Value interface{}
}
type unionSimple struct {
A int
B string
C []string
}
type unionComplex struct {
unionSimple
A int
}
type unionTagged struct {
A int `json:"A"`
}
type unionTaggedComplex struct {
unionSimple
unionTagged
B string
}
func TestUnionStructFields(t *testing.T) {
var cases = []struct {
in interface{}
expect []testUnionValues
}{
{
in: unionSimple{1, "2", []string{"abc"}},
expect: []testUnionValues{
{"A", 1},
{"B", "2"},
{"C", []string{"abc"}},
},
},
{
in: unionComplex{
unionSimple: unionSimple{1, "2", []string{"abc"}},
A: 2,
},
expect: []testUnionValues{
{"B", "2"},
{"C", []string{"abc"}},
{"A", 2},
},
},
{
in: unionTaggedComplex{
unionSimple: unionSimple{1, "2", []string{"abc"}},
unionTagged: unionTagged{3},
B: "3",
},
expect: []testUnionValues{
{"C", []string{"abc"}},
{"A", 3},
{"B", "3"},
},
},
}
for i, c := range cases {
v := reflect.ValueOf(c.in)
fields := unionStructFields(v.Type(), MarshalOptions{SupportJSONTags: true})
for j, f := range fields {
expected := c.expect[j]
assert.Equal(t, expected.Name, f.Name, "case %d, field %d", i, j)
actual := v.FieldByIndex(f.Index).Interface()
assert.EqualValues(t, expected.Value, actual, "case %d, field %d", i, j)
}
}
}
func TestFieldByName(t *testing.T) {
fields := []field{
{Name: "Abc"}, {Name: "mixCase"}, {Name: "UPPERCASE"},
}
cases := []struct {
Name, FieldName string
Found bool
}{
{"abc", "Abc", true}, {"ABC", "Abc", true}, {"Abc", "Abc", true},
{"123", "", false},
{"ab", "", false},
{"MixCase", "mixCase", true},
{"uppercase", "UPPERCASE", true}, {"UPPERCASE", "UPPERCASE", true},
}
for _, c := range cases {
f, ok := fieldByName(fields, c.Name)
assert.Equal(t, c.Found, ok)
if ok {
assert.Equal(t, c.FieldName, f.Name)
}
}
}

View file

@ -0,0 +1,104 @@
package dynamodbattribute_test
import (
"fmt"
"reflect"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
func ExampleMarshal() {
type Record struct {
Bytes []byte
MyField string
Letters []string
Numbers []int
}
r := Record{
Bytes: []byte{48, 49},
MyField: "MyFieldValue",
Letters: []string{"a", "b", "c", "d"},
Numbers: []int{1, 2, 3},
}
av, err := dynamodbattribute.Marshal(r)
fmt.Println("err", err)
fmt.Println("Bytes", av.M["Bytes"])
fmt.Println("MyField", av.M["MyField"])
fmt.Println("Letters", av.M["Letters"])
fmt.Println("Numbers", av.M["Numbers"])
// Output:
// err <nil>
// Bytes {
// B: <binary> len 2
// }
// MyField {
// S: "MyFieldValue"
// }
// Letters {
// L: [
// {
// S: "a"
// },
// {
// S: "b"
// },
// {
// S: "c"
// },
// {
// S: "d"
// }
// ]
// }
// Numbers {
// L: [{
// N: "1"
// },{
// N: "2"
// },{
// N: "3"
// }]
// }
}
func ExampleUnmarshal() {
type Record struct {
Bytes []byte
MyField string
Letters []string
A2Num map[string]int
}
expect := Record{
Bytes: []byte{48, 49},
MyField: "MyFieldValue",
Letters: []string{"a", "b", "c", "d"},
A2Num: map[string]int{"a": 1, "b": 2, "c": 3},
}
av := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Bytes": {B: []byte{48, 49}},
"MyField": {S: aws.String("MyFieldValue")},
"Letters": {L: []*dynamodb.AttributeValue{
{S: aws.String("a")}, {S: aws.String("b")}, {S: aws.String("c")}, {S: aws.String("d")},
}},
"A2Num": {M: map[string]*dynamodb.AttributeValue{
"a": {N: aws.String("1")},
"b": {N: aws.String("2")},
"c": {N: aws.String("3")},
}},
},
}
actual := Record{}
err := dynamodbattribute.Unmarshal(av, &actual)
fmt.Println(err, reflect.DeepEqual(expect, actual))
// Output:
// <nil> true
}

View file

@ -0,0 +1,526 @@
package dynamodbattribute
import (
"math"
"reflect"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
type simpleMarshalStruct struct {
Byte []byte
String string
Int int
Uint uint
Float32 float32
Float64 float64
Bool bool
Null *interface{}
}
type complexMarshalStruct struct {
Simple []simpleMarshalStruct
}
type myByteStruct struct {
Byte []byte
}
type myByteSetStruct struct {
ByteSet [][]byte
}
type marshallerTestInput struct {
input interface{}
expected interface{}
err awserr.Error
}
var marshalerScalarInputs = []marshallerTestInput{
{
input: nil,
expected: &dynamodb.AttributeValue{NULL: &trueValue},
},
{
input: "some string",
expected: &dynamodb.AttributeValue{S: aws.String("some string")},
},
{
input: true,
expected: &dynamodb.AttributeValue{BOOL: &trueValue},
},
{
input: false,
expected: &dynamodb.AttributeValue{BOOL: &falseValue},
},
{
input: 3.14,
expected: &dynamodb.AttributeValue{N: aws.String("3.14")},
},
{
input: math.MaxFloat32,
expected: &dynamodb.AttributeValue{N: aws.String("340282346638528860000000000000000000000")},
},
{
input: math.MaxFloat64,
expected: &dynamodb.AttributeValue{N: aws.String("179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")},
},
{
input: 12,
expected: &dynamodb.AttributeValue{N: aws.String("12")},
},
{
input: Number("12"),
expected: &dynamodb.AttributeValue{N: aws.String("12")},
},
{
input: simpleMarshalStruct{},
expected: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Byte": {NULL: &trueValue},
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {NULL: &trueValue},
"Uint": {N: aws.String("0")},
},
},
},
}
var marshallerMapTestInputs = []marshallerTestInput{
// Scalar tests
{
input: nil,
expected: map[string]*dynamodb.AttributeValue{},
},
{
input: map[string]interface{}{"string": "some string"},
expected: map[string]*dynamodb.AttributeValue{"string": {S: aws.String("some string")}},
},
{
input: map[string]interface{}{"bool": true},
expected: map[string]*dynamodb.AttributeValue{"bool": {BOOL: &trueValue}},
},
{
input: map[string]interface{}{"bool": false},
expected: map[string]*dynamodb.AttributeValue{"bool": {BOOL: &falseValue}},
},
{
input: map[string]interface{}{"null": nil},
expected: map[string]*dynamodb.AttributeValue{"null": {NULL: &trueValue}},
},
{
input: map[string]interface{}{"float": 3.14},
expected: map[string]*dynamodb.AttributeValue{"float": {N: aws.String("3.14")}},
},
{
input: map[string]interface{}{"float": math.MaxFloat32},
expected: map[string]*dynamodb.AttributeValue{"float": {N: aws.String("340282346638528860000000000000000000000")}},
},
{
input: map[string]interface{}{"float": math.MaxFloat64},
expected: map[string]*dynamodb.AttributeValue{"float": {N: aws.String("179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")}},
},
{
input: map[string]interface{}{"num": 12.},
expected: map[string]*dynamodb.AttributeValue{"num": {N: aws.String("12")}},
},
{
input: map[string]interface{}{"byte": []byte{48, 49}},
expected: map[string]*dynamodb.AttributeValue{"byte": {B: []byte{48, 49}}},
},
{
input: struct{ Byte []byte }{Byte: []byte{48, 49}},
expected: map[string]*dynamodb.AttributeValue{"Byte": {B: []byte{48, 49}}},
},
{
input: map[string]interface{}{"byte_set": [][]byte{{48, 49}, {50, 51}}},
expected: map[string]*dynamodb.AttributeValue{"byte_set": {BS: [][]byte{{48, 49}, {50, 51}}}},
},
{
input: struct{ ByteSet [][]byte }{ByteSet: [][]byte{{48, 49}, {50, 51}}},
expected: map[string]*dynamodb.AttributeValue{"ByteSet": {BS: [][]byte{{48, 49}, {50, 51}}}},
},
// List
{
input: map[string]interface{}{"list": []interface{}{"a string", 12., 3.14, true, nil, false}},
expected: map[string]*dynamodb.AttributeValue{
"list": {
L: []*dynamodb.AttributeValue{
{S: aws.String("a string")},
{N: aws.String("12")},
{N: aws.String("3.14")},
{BOOL: &trueValue},
{NULL: &trueValue},
{BOOL: &falseValue},
},
},
},
},
// Map
{
input: map[string]interface{}{"map": map[string]interface{}{"nestednum": 12.}},
expected: map[string]*dynamodb.AttributeValue{
"map": {
M: map[string]*dynamodb.AttributeValue{
"nestednum": {
N: aws.String("12"),
},
},
},
},
},
// Structs
{
input: simpleMarshalStruct{},
expected: map[string]*dynamodb.AttributeValue{
"Byte": {NULL: &trueValue},
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {NULL: &trueValue},
"Uint": {N: aws.String("0")},
},
},
{
input: complexMarshalStruct{},
expected: map[string]*dynamodb.AttributeValue{
"Simple": {NULL: &trueValue},
},
},
{
input: struct {
Simple []string `json:"simple"`
}{},
expected: map[string]*dynamodb.AttributeValue{
"simple": {NULL: &trueValue},
},
},
{
input: struct {
Simple []string `json:"simple,omitempty"`
}{},
expected: map[string]*dynamodb.AttributeValue{},
},
{
input: struct {
Simple []string `json:"-"`
}{},
expected: map[string]*dynamodb.AttributeValue{},
},
{
input: complexMarshalStruct{Simple: []simpleMarshalStruct{{Int: -2}, {Uint: 5}}},
expected: map[string]*dynamodb.AttributeValue{
"Simple": {
L: []*dynamodb.AttributeValue{
{
M: map[string]*dynamodb.AttributeValue{
"Byte": {NULL: &trueValue},
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("-2")},
"Null": {NULL: &trueValue},
"String": {NULL: &trueValue},
"Uint": {N: aws.String("0")},
},
},
{
M: map[string]*dynamodb.AttributeValue{
"Byte": {NULL: &trueValue},
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {NULL: &trueValue},
"Uint": {N: aws.String("5")},
},
},
},
},
},
},
}
var marshallerListTestInputs = []marshallerTestInput{
{
input: nil,
expected: []*dynamodb.AttributeValue{},
},
{
input: []interface{}{},
expected: []*dynamodb.AttributeValue{},
},
{
input: []simpleMarshalStruct{},
expected: []*dynamodb.AttributeValue{},
},
{
input: []interface{}{"a string", 12., 3.14, true, nil, false},
expected: []*dynamodb.AttributeValue{
{S: aws.String("a string")},
{N: aws.String("12")},
{N: aws.String("3.14")},
{BOOL: &trueValue},
{NULL: &trueValue},
{BOOL: &falseValue},
},
},
{
input: []simpleMarshalStruct{{}},
expected: []*dynamodb.AttributeValue{
{
M: map[string]*dynamodb.AttributeValue{
"Byte": {NULL: &trueValue},
"Bool": {BOOL: &falseValue},
"Float32": {N: aws.String("0")},
"Float64": {N: aws.String("0")},
"Int": {N: aws.String("0")},
"Null": {NULL: &trueValue},
"String": {NULL: &trueValue},
"Uint": {N: aws.String("0")},
},
},
},
},
}
func Test_New_Marshal(t *testing.T) {
for _, test := range marshalerScalarInputs {
testMarshal(t, test)
}
}
func testMarshal(t *testing.T, test marshallerTestInput) {
actual, err := Marshal(test.input)
if test.err != nil {
if err == nil {
t.Errorf("Marshal with input %#v retured %#v, expected error `%s`", test.input, actual, test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("Marshal with input %#v retured error `%s`, expected error `%s`", test.input, err, test.err)
}
} else {
if err != nil {
t.Errorf("Marshal with input %#v retured error `%s`", test.input, err)
}
compareObjects(t, test.expected, actual)
}
}
func Test_New_Unmarshal(t *testing.T) {
// Using the same inputs from Marshal, test the reverse mapping.
for i, test := range marshalerScalarInputs {
if test.input == nil {
continue
}
actual := reflect.New(reflect.TypeOf(test.input)).Interface()
if err := Unmarshal(test.expected.(*dynamodb.AttributeValue), actual); err != nil {
t.Errorf("Unmarshal %d, with input %#v retured error `%s`", i+1, test.expected, err)
}
compareObjects(t, test.input, reflect.ValueOf(actual).Elem().Interface())
}
}
func Test_New_UnmarshalError(t *testing.T) {
// Test that we get an error using Unmarshal to convert to a nil value.
expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)}
if err := Unmarshal(nil, nil); err == nil {
t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", nil, expected)
} else if err.Error() != expected.Error() {
t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", nil, err, expected)
}
// Test that we get an error using Unmarshal to convert to a non-pointer value.
var actual map[string]interface{}
expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)}
if err := Unmarshal(nil, actual); err == nil {
t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", actual, expected)
} else if err.Error() != expected.Error() {
t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", actual, err, expected)
}
// Test that we get an error using Unmarshal to convert to nil struct.
var actual2 *struct{ A int }
expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)}
if err := Unmarshal(nil, actual2); err == nil {
t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", actual2, expected)
} else if err.Error() != expected.Error() {
t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", actual2, err, expected)
}
}
func Test_New_MarshalMap(t *testing.T) {
for _, test := range marshallerMapTestInputs {
testMarshalMap(t, test)
}
}
func testMarshalMap(t *testing.T, test marshallerTestInput) {
actual, err := MarshalMap(test.input)
if test.err != nil {
if err == nil {
t.Errorf("MarshalMap with input %#v retured %#v, expected error `%s`", test.input, actual, test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("MarshalMap with input %#v retured error `%s`, expected error `%s`", test.input, err, test.err)
}
} else {
if err != nil {
t.Errorf("MarshalMap with input %#v retured error `%s`", test.input, err)
}
compareObjects(t, test.expected, actual)
}
}
func Test_New_UnmarshalMap(t *testing.T) {
// Using the same inputs from MarshalMap, test the reverse mapping.
for i, test := range marshallerMapTestInputs {
if test.input == nil {
continue
}
actual := reflect.New(reflect.TypeOf(test.input)).Interface()
if err := UnmarshalMap(test.expected.(map[string]*dynamodb.AttributeValue), actual); err != nil {
t.Errorf("Unmarshal %d, with input %#v retured error `%s`", i+1, test.expected, err)
}
compareObjects(t, test.input, reflect.ValueOf(actual).Elem().Interface())
}
}
func Test_New_UnmarshalMapError(t *testing.T) {
// Test that we get an error using UnmarshalMap to convert to a nil value.
expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)}
if err := UnmarshalMap(nil, nil); err == nil {
t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", nil, expected)
} else if err.Error() != expected.Error() {
t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", nil, err, expected)
}
// Test that we get an error using UnmarshalMap to convert to a non-pointer value.
var actual map[string]interface{}
expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)}
if err := UnmarshalMap(nil, actual); err == nil {
t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", actual, expected)
} else if err.Error() != expected.Error() {
t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", actual, err, expected)
}
// Test that we get an error using UnmarshalMap to convert to nil struct.
var actual2 *struct{ A int }
expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)}
if err := UnmarshalMap(nil, actual2); err == nil {
t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", actual2, expected)
} else if err.Error() != expected.Error() {
t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", actual2, err, expected)
}
}
func Test_New_MarshalList(t *testing.T) {
for _, test := range marshallerListTestInputs {
testMarshalList(t, test)
}
}
func testMarshalList(t *testing.T, test marshallerTestInput) {
actual, err := MarshalList(test.input)
if test.err != nil {
if err == nil {
t.Errorf("MarshalList with input %#v retured %#v, expected error `%s`", test.input, actual, test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("MarshalList with input %#v retured error `%s`, expected error `%s`", test.input, err, test.err)
}
} else {
if err != nil {
t.Errorf("MarshalList with input %#v retured error `%s`", test.input, err)
}
compareObjects(t, test.expected, actual)
}
}
func Test_New_UnmarshalList(t *testing.T) {
// Using the same inputs from MarshalList, test the reverse mapping.
for i, test := range marshallerListTestInputs {
if test.input == nil {
continue
}
iv := reflect.ValueOf(test.input)
actual := reflect.New(iv.Type())
if iv.Kind() == reflect.Slice {
actual.Elem().Set(reflect.MakeSlice(iv.Type(), iv.Len(), iv.Cap()))
}
if err := UnmarshalList(test.expected.([]*dynamodb.AttributeValue), actual.Interface()); err != nil {
t.Errorf("Unmarshal %d, with input %#v retured error `%s`", i+1, test.expected, err)
}
compareObjects(t, test.input, actual.Elem().Interface())
}
}
func Test_New_UnmarshalListError(t *testing.T) {
// Test that we get an error using UnmarshalList to convert to a nil value.
expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)}
if err := UnmarshalList(nil, nil); err == nil {
t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", nil, expected)
} else if err.Error() != expected.Error() {
t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", nil, err, expected)
}
// Test that we get an error using UnmarshalList to convert to a non-pointer value.
var actual map[string]interface{}
expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)}
if err := UnmarshalList(nil, actual); err == nil {
t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", actual, expected)
} else if err.Error() != expected.Error() {
t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", actual, err, expected)
}
// Test that we get an error using UnmarshalList to convert to nil struct.
var actual2 *struct{ A int }
expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)}
if err := UnmarshalList(nil, actual2); err == nil {
t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", actual2, expected)
} else if err.Error() != expected.Error() {
t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", actual2, err, expected)
}
}
func compareObjects(t *testing.T, expected interface{}, actual interface{}) {
if !reflect.DeepEqual(expected, actual) {
ev := reflect.ValueOf(expected)
av := reflect.ValueOf(actual)
t.Errorf("\nExpected kind(%s,%T):\n%s\nActual kind(%s,%T):\n%s\n",
ev.Kind(),
ev.Interface(),
awsutil.Prettify(expected),
av.Kind(),
ev.Interface(),
awsutil.Prettify(actual))
}
}
func BenchmarkMarshal(b *testing.B) {
d := simpleMarshalStruct{
String: "abc",
Int: 123,
Uint: 123,
Float32: 123.321,
Float64: 123.321,
Bool: true,
Null: nil,
}
for i := 0; i < b.N; i++ {
_, err := Marshal(d)
if err != nil {
b.Fatal("unexpected error", err)
}
}
}

View file

@ -0,0 +1,402 @@
package dynamodbattribute
import (
"reflect"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/stretchr/testify/assert"
)
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
}
type testNamedPointer *int
var testDate, _ = time.Parse(time.RFC3339, "2016-05-03T17:06:26.209072Z")
var sharedTestCases = []struct {
in *dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{ // Binary slice
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: &[]byte{},
expected: []byte{48, 49},
},
{ // Binary slice
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: &[]byte{},
expected: []byte{48, 49},
},
{ // Binary slice oversized
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: func() *[]byte {
v := make([]byte, 0, 10)
return &v
}(),
expected: []byte{48, 49},
},
{ // Binary slice pointer
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: func() **[]byte {
v := make([]byte, 0, 10)
v2 := &v
return &v2
}(),
expected: []byte{48, 49},
},
{ // Bool
in: &dynamodb.AttributeValue{BOOL: aws.Bool(true)},
actual: new(bool),
expected: true,
},
{ // List
in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
{N: aws.String("123")},
}},
actual: &[]int{},
expected: []int{123},
},
{ // Map, interface
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {N: aws.String("123")},
}},
actual: &map[string]int{},
expected: map[string]int{"abc": 123},
},
{ // Map, struct
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"Abc": {N: aws.String("123")},
}},
actual: &struct{ Abc int }{},
expected: struct{ Abc int }{Abc: 123},
},
{ // Map, struct
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {N: aws.String("123")},
}},
actual: &struct {
Abc int `json:"abc" dynamodbav:"abc"`
}{},
expected: struct {
Abc int `json:"abc" dynamodbav:"abc"`
}{Abc: 123},
},
{ // Number, int
in: &dynamodb.AttributeValue{N: aws.String("123")},
actual: new(int),
expected: 123,
},
{ // Number, Float
in: &dynamodb.AttributeValue{N: aws.String("123.1")},
actual: new(float64),
expected: float64(123.1),
},
{ // Null
in: &dynamodb.AttributeValue{NULL: aws.Bool(true)},
actual: new(string),
expected: "",
},
{ // Null ptr
in: &dynamodb.AttributeValue{NULL: aws.Bool(true)},
actual: new(*string),
expected: nil,
},
{ // String
in: &dynamodb.AttributeValue{S: aws.String("abc")},
actual: new(string),
expected: "abc",
},
{ // Binary Set
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Binarys": {BS: [][]byte{{48, 49}, {50, 51}}},
},
},
actual: &testBinarySetStruct{},
expected: testBinarySetStruct{Binarys: [][]byte{{48, 49}, {50, 51}}},
},
{ // Number Set
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Numbers": {NS: []*string{aws.String("123"), aws.String("321")}},
},
},
actual: &testNumberSetStruct{},
expected: testNumberSetStruct{Numbers: []int{123, 321}},
},
{ // String Set
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Strings": {SS: []*string{aws.String("abc"), aws.String("efg")}},
},
},
actual: &testStringSetStruct{},
expected: testStringSetStruct{Strings: []string{"abc", "efg"}},
},
{ // Int value as string
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Value": {S: aws.String("123")},
},
},
actual: &testIntAsStringStruct{},
expected: testIntAsStringStruct{Value: 123},
},
{ // Omitempty
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Value3": {N: aws.String("0")},
},
},
actual: &testOmitEmptyStruct{},
expected: testOmitEmptyStruct{Value: "", Value2: nil, Value3: 0},
},
{ // aliased type
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Value": {S: aws.String("123")},
"Value2": {N: aws.String("123")},
"Value3": {M: map[string]*dynamodb.AttributeValue{
"Key": {N: aws.String("321")},
}},
"Value4": {L: []*dynamodb.AttributeValue{
{S: aws.String("1")},
{S: aws.String("2")},
{S: aws.String("3")},
}},
"Value5": {B: []byte{0, 1, 2}},
"Value6": {L: []*dynamodb.AttributeValue{
{N: aws.String("1")},
{N: aws.String("2")},
{N: aws.String("3")},
}},
"Value7": {L: []*dynamodb.AttributeValue{
{S: aws.String("1")},
{S: aws.String("2")},
{S: aws.String("3")},
}},
"Value8": {BS: [][]byte{
{0, 1, 2}, {3, 4, 5},
}},
"Value9": {NS: []*string{
aws.String("1"),
aws.String("2"),
aws.String("3"),
}},
"Value10": {SS: []*string{
aws.String("1"),
aws.String("2"),
aws.String("3"),
}},
"Value11": {L: []*dynamodb.AttributeValue{
{N: aws.String("1")},
{N: aws.String("2")},
{N: aws.String("3")},
}},
"Value12": {L: []*dynamodb.AttributeValue{
{S: aws.String("1")},
{S: aws.String("2")},
{S: aws.String("3")},
}},
"Value13": {BOOL: aws.Bool(true)},
"Value14": {L: []*dynamodb.AttributeValue{
{BOOL: aws.Bool(true)},
{BOOL: aws.Bool(false)},
{BOOL: aws.Bool(true)},
}},
},
},
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},
},
},
{
in: &dynamodb.AttributeValue{N: aws.String("123")},
actual: new(testNamedPointer),
expected: testNamedPointer(aws.Int(123)),
},
{ // time.Time
in: &dynamodb.AttributeValue{S: aws.String("2016-05-03T17:06:26.209072Z")},
actual: new(time.Time),
expected: testDate,
},
{ // time.Time List
in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
{S: aws.String("2016-05-03T17:06:26.209072Z")},
{S: aws.String("2016-05-04T17:06:26.209072Z")},
}},
actual: new([]time.Time),
expected: []time.Time{testDate, testDate.Add(24 * time.Hour)},
},
{ // time.Time struct
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("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: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("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 = []struct {
in []*dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{
in: []*dynamodb.AttributeValue{
{B: []byte{48, 49}},
{BOOL: aws.Bool(true)},
{N: aws.String("123")},
{S: aws.String("123")},
},
actual: func() *[]interface{} {
v := []interface{}{}
return &v
}(),
expected: []interface{}{[]byte{48, 49}, true, 123., "123"},
},
{
in: []*dynamodb.AttributeValue{
{N: aws.String("1")},
{N: aws.String("2")},
{N: aws.String("3")},
},
actual: &[]interface{}{},
expected: []interface{}{1., 2., 3.},
},
}
var sharedMapTestCases = []struct {
in map[string]*dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{
in: map[string]*dynamodb.AttributeValue{
"B": {B: []byte{48, 49}},
"BOOL": {BOOL: aws.Bool(true)},
"N": {N: aws.String("123")},
"S": {S: aws.String("123")},
},
actual: &map[string]interface{}{},
expected: map[string]interface{}{
"B": []byte{48, 49}, "BOOL": true,
"N": 123., "S": "123",
},
},
}
func assertConvertTest(t *testing.T, i int, actual, expected interface{}, err, expectedErr error) {
i++
if expectedErr != nil {
if err != nil {
assert.Equal(t, expectedErr, err, "case %d", i)
} else {
assert.Fail(t, "", "case %d, expected error, %v", i)
}
} else if err != nil {
assert.Fail(t, "", "case %d, expect no error, got %v", i, err)
} else {
assert.Equal(t, ptrToValue(expected), ptrToValue(actual), "case %d", i)
}
}
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()
}

View file

@ -0,0 +1,68 @@
package dynamodbattribute
import (
"reflect"
"strings"
)
type tag struct {
Name string
Ignore bool
OmitEmpty bool
OmitEmptyElem bool
AsString bool
AsBinSet, AsNumSet, AsStrSet bool
AsUnixTime bool
}
func (t *tag) parseAVTag(structTag reflect.StructTag) {
tagStr := structTag.Get("dynamodbav")
if len(tagStr) == 0 {
return
}
t.parseTagStr(tagStr)
}
func (t *tag) parseJSONTag(structTag reflect.StructTag) {
tagStr := structTag.Get("json")
if len(tagStr) == 0 {
return
}
t.parseTagStr(tagStr)
}
func (t *tag) parseTagStr(tagStr string) {
parts := strings.Split(tagStr, ",")
if len(parts) == 0 {
return
}
if name := parts[0]; name == "-" {
t.Name = ""
t.Ignore = true
} else {
t.Name = name
t.Ignore = false
}
for _, opt := range parts[1:] {
switch opt {
case "omitempty":
t.OmitEmpty = true
case "omitemptyelem":
t.OmitEmptyElem = true
case "string":
t.AsString = true
case "binaryset":
t.AsBinSet = true
case "numberset":
t.AsNumSet = true
case "stringset":
t.AsStrSet = true
case "unixtime":
t.AsUnixTime = true
}
}
}

View file

@ -0,0 +1,47 @@
package dynamodbattribute
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTagParse(t *testing.T) {
cases := []struct {
in reflect.StructTag
json, av bool
expect tag
}{
{`json:""`, true, false, tag{}},
{`json:"name"`, true, false, tag{Name: "name"}},
{`json:"name,omitempty"`, true, false, tag{Name: "name", OmitEmpty: true}},
{`json:"-"`, true, false, tag{Ignore: true}},
{`json:",omitempty"`, true, false, tag{OmitEmpty: true}},
{`json:",string"`, true, false, tag{AsString: true}},
{`dynamodbav:""`, false, true, tag{}},
{`dynamodbav:","`, false, true, tag{}},
{`dynamodbav:"name"`, false, true, tag{Name: "name"}},
{`dynamodbav:"name"`, false, true, tag{Name: "name"}},
{`dynamodbav:"-"`, false, true, tag{Ignore: true}},
{`dynamodbav:",omitempty"`, false, true, tag{OmitEmpty: true}},
{`dynamodbav:",omitemptyelem"`, false, true, tag{OmitEmptyElem: true}},
{`dynamodbav:",string"`, false, true, tag{AsString: true}},
{`dynamodbav:",binaryset"`, false, true, tag{AsBinSet: true}},
{`dynamodbav:",numberset"`, false, true, tag{AsNumSet: true}},
{`dynamodbav:",stringset"`, false, true, tag{AsStrSet: true}},
{`dynamodbav:",stringset,omitemptyelem"`, false, true, tag{AsStrSet: true, OmitEmptyElem: true}},
{`dynamodbav:"name,stringset,omitemptyelem"`, false, true, tag{Name: "name", AsStrSet: true, OmitEmptyElem: true}},
}
for i, c := range cases {
actual := tag{}
if c.json {
actual.parseJSONTag(c.in)
}
if c.av {
actual.parseAVTag(c.in)
}
assert.Equal(t, c.expect, actual, "case %d", i+1)
}
}