diff --git a/feature/dynamodb/attributevalue/convert.go b/feature/dynamodb/attributevalue/convert.go new file mode 100644 index 00000000000..283f89397b3 --- /dev/null +++ b/feature/dynamodb/attributevalue/convert.go @@ -0,0 +1,89 @@ +package attributevalue + +import ( + "fmt" + + ddb "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + streams "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" +) + +// FromDynamoDBStreamMap converts a map of Amazon DynamoDB Streams +// AttributeValues, and all nested members, to map of Amazon DynamoDB +// AttributeValues. +func FromDynamoDBStreamMap(from map[string]streams.AttributeValue) (to map[string]ddb.AttributeValue, err error) { + to = make(map[string]ddb.AttributeValue, len(from)) + for field, value := range from { + to[field], err = FromDynamoDBStream(value) + if err != nil { + return nil, err + } + } + + return to, nil +} + +// FromDynamoDBStreamList converts a slice of Amazon DynamoDB Streams +// AttributeValues, and all nested members, to slice of Amazon DynamoDB +// AttributeValues. +func FromDynamoDBStreamList(from []streams.AttributeValue) (to []ddb.AttributeValue, err error) { + to = make([]ddb.AttributeValue, len(from)) + for i := 0; i < len(from); i++ { + to[i], err = FromDynamoDBStream(from[i]) + if err != nil { + return nil, err + } + } + + return to, nil +} + +// FromDynamoDBStream converts an Amazon DynamoDB Streams AttributeValue, and +// all nested members, to an Amazon DynamoDB AttributeValue. +func FromDynamoDBStream(from streams.AttributeValue) (ddb.AttributeValue, error) { + switch tv := from.(type) { + case *streams.AttributeValueMemberNULL: + return &ddb.AttributeValueMemberNULL{Value: tv.Value}, nil + + case *streams.AttributeValueMemberBOOL: + return &ddb.AttributeValueMemberBOOL{Value: tv.Value}, nil + + case *streams.AttributeValueMemberB: + return &ddb.AttributeValueMemberB{Value: tv.Value}, nil + + case *streams.AttributeValueMemberBS: + bs := make([][]byte, len(tv.Value)) + for i := 0; i < len(tv.Value); i++ { + bs[i] = append([]byte{}, tv.Value[i]...) + } + return &ddb.AttributeValueMemberBS{Value: bs}, nil + + case *streams.AttributeValueMemberN: + return &ddb.AttributeValueMemberN{Value: tv.Value}, nil + + case *streams.AttributeValueMemberNS: + return &ddb.AttributeValueMemberNS{Value: append([]string{}, tv.Value...)}, nil + + case *streams.AttributeValueMemberS: + return &ddb.AttributeValueMemberS{Value: tv.Value}, nil + + case *streams.AttributeValueMemberSS: + return &ddb.AttributeValueMemberSS{Value: append([]string{}, tv.Value...)}, nil + + case *streams.AttributeValueMemberL: + values, err := FromDynamoDBStreamList(tv.Value) + if err != nil { + return nil, err + } + return &ddb.AttributeValueMemberL{Value: values}, nil + + case *streams.AttributeValueMemberM: + values, err := FromDynamoDBStreamMap(tv.Value) + if err != nil { + return nil, err + } + return &ddb.AttributeValueMemberM{Value: values}, nil + + default: + return nil, fmt.Errorf("unknown Amazon DynamoDB Streams AttributeValue union member, %T", from) + } +} diff --git a/feature/dynamodb/attributevalue/decode.go b/feature/dynamodb/attributevalue/decode.go index 3010752afbc..c701fdf72bc 100644 --- a/feature/dynamodb/attributevalue/decode.go +++ b/feature/dynamodb/attributevalue/decode.go @@ -30,7 +30,7 @@ import ( // return nil // } type Unmarshaler interface { - UnmarshalDynamoDBAttributeValue(*types.AttributeValue) error + UnmarshalDynamoDBAttributeValue(types.AttributeValue) error } // Unmarshal will unmarshal DynamoDB AttributeValues to Go value types. @@ -74,7 +74,7 @@ type Unmarshaler interface { // and return the error. // // The output value provided must be a non-nil pointer -func Unmarshal(av *types.AttributeValue, out interface{}) error { +func Unmarshal(av types.AttributeValue, out interface{}) error { return NewDecoder().Decode(av, out) } @@ -83,7 +83,7 @@ func Unmarshal(av *types.AttributeValue, out interface{}) error { // // The output value provided must be a non-nil pointer func UnmarshalMap(m map[string]types.AttributeValue, out interface{}) error { - return NewDecoder().Decode(&types.AttributeValue{M: m}, out) + return NewDecoder().Decode(&types.AttributeValueMemberM{Value: m}, out) } // UnmarshalList is an alias for Unmarshal func which unmarshals @@ -91,7 +91,7 @@ func UnmarshalMap(m map[string]types.AttributeValue, out interface{}) error { // // The output value provided must be a non-nil pointer func UnmarshalList(l []types.AttributeValue, out interface{}) error { - return NewDecoder().Decode(&types.AttributeValue{L: l}, out) + return NewDecoder().Decode(&types.AttributeValueMemberL{Value: l}, out) } // UnmarshalListOfMaps is an alias for Unmarshal func which unmarshals a @@ -104,15 +104,22 @@ func UnmarshalList(l []types.AttributeValue, out interface{}) error { func UnmarshalListOfMaps(l []map[string]types.AttributeValue, out interface{}) error { items := make([]types.AttributeValue, len(l)) for i, m := range l { - items[i] = types.AttributeValue{M: m} + items[i] = &types.AttributeValueMemberM{Value: m} } return UnmarshalList(items, out) } -// A Decoder provides unmarshaling AttributeValues to Go value types. -type Decoder struct { - MarshalOptions +// DecoderOptions is a collection of options to configure how the decoder +// unmarshalls the value. +type DecoderOptions struct { + // Support other custom struct tag keys, such as `yaml`, `json`, or `toml`. + // Note that values provided with a custom TagKey must also be supported + // by the (un)marshalers in this package. + // + // Tag key `dynamodbav` will always be read, but if custom tag key + // conflicts with `dynamodbav` the custom tag key value will be used. + TagKey string // Instructs the decoder to decode AttributeValue Numbers as // Number type instead of float64 when the destination type @@ -120,19 +127,22 @@ type Decoder struct { UseNumber bool } +// A Decoder provides unmarshaling AttributeValues to Go value types. +type Decoder struct { + options DecoderOptions +} + // 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) +func NewDecoder(optFns ...func(*DecoderOptions)) *Decoder { + var options DecoderOptions + for _, fn := range optFns { + fn(&options) } - return d + return &Decoder{ + options: options, + } } // Decode will unmarshal an AttributeValue into a Go value type. An error @@ -140,7 +150,7 @@ func NewDecoder(opts ...func(*Decoder)) *Decoder { // to the provide Go value type. // // The output value provided must be a non-nil pointer -func (d *Decoder) Decode(av *types.AttributeValue, out interface{}, opts ...func(*Decoder)) error { +func (d *Decoder) Decode(av types.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)} @@ -151,13 +161,13 @@ func (d *Decoder) Decode(av *types.AttributeValue, out interface{}, opts ...func 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("")) +var byteSliceSliceType = reflect.TypeOf([][]byte(nil)) var timeType = reflect.TypeOf(time.Time{}) -func (d *Decoder) decode(av *types.AttributeValue, v reflect.Value, fieldTag tag) error { +func (d *Decoder) decode(av types.AttributeValue, v reflect.Value, fieldTag tag) error { var u Unmarshaler - if av == nil || av.NULL != nil { + _, isNull := av.(*types.AttributeValueMemberNULL) + if av == nil || isNull { u, v = indirect(v, true) if u != nil { return u.UnmarshalDynamoDBAttributeValue(av) @@ -170,28 +180,41 @@ func (d *Decoder) decode(av *types.AttributeValue, v reflect.Value, fieldTag tag return u.UnmarshalDynamoDBAttributeValue(av) } - switch { - case av.B != nil: - return d.decodeBinary(av.B, v) - case av.BOOL != nil: - return d.decodeBool(av.BOOL, v) - case av.BS != nil: - return d.decodeBinarySet(av.BS, v) - case av.L != nil: - return d.decodeList(av.L, v) - case av.M != nil: - return d.decodeMap(av.M, v) - case av.N != nil: - return d.decodeNumber(av.N, v, fieldTag) - case av.NS != nil: - return d.decodeNumberSet(av.NS, v) - case av.S != nil: // DynamoDB does not allow for empty strings, so we do not consider the length or EnableEmptyCollections flag here - return d.decodeString(av.S, v, fieldTag) - case av.SS != nil: - return d.decodeStringSet(av.SS, v) - } + switch tv := av.(type) { + case *types.AttributeValueMemberB: + return d.decodeBinary(tv.Value, v) - return nil + case *types.AttributeValueMemberBOOL: + return d.decodeBool(tv.Value, v) + + case *types.AttributeValueMemberBS: + return d.decodeBinarySet(tv.Value, v) + + case *types.AttributeValueMemberL: + return d.decodeList(tv.Value, v) + + case *types.AttributeValueMemberM: + return d.decodeMap(tv.Value, v) + + case *types.AttributeValueMemberN: + return d.decodeNumber(tv.Value, v, fieldTag) + + case *types.AttributeValueMemberNS: + return d.decodeNumberSet(tv.Value, v) + + case *types.AttributeValueMemberS: + // TODO [ddbAV] is this comment still valid? + // DynamoDB does not allow for empty strings, so we do not consider the + // length or EnableEmptyCollections flag here + return d.decodeString(tv.Value, v, fieldTag) + + case *types.AttributeValueMemberSS: + return d.decodeStringSet(tv.Value, v) + + default: + // TODO [ddbAV] typed error + return fmt.Errorf("unsupported dynamoDB attribute value type, %T", av) + } } func (d *Decoder) decodeBinary(b []byte, v reflect.Value) error { @@ -239,10 +262,12 @@ func (d *Decoder) decodeBinary(b []byte, v reflect.Value) error { return nil } -func (d *Decoder) decodeBool(b *bool, v reflect.Value) error { +func (d *Decoder) decodeBool(b bool, v reflect.Value) error { switch v.Kind() { + // TODO [ddb] Why `reflect.interface` here? case reflect.Bool, reflect.Interface: - v.Set(reflect.ValueOf(*b).Convert(v.Type())) + v.Set(reflect.ValueOf(b).Convert(v.Type())) + default: return &UnmarshalTypeError{Value: "bool", Type: v.Type()} } @@ -277,7 +302,7 @@ func (d *Decoder) decodeBinarySet(bs [][]byte, v reflect.Value) error { v.SetLen(i + 1) u, elem := indirect(v.Index(i), false) if u != nil { - return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValue{BS: bs}) + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberBS{Value: bs}) } if err := d.decodeBinary(bs[i], elem); err != nil { return err @@ -287,7 +312,7 @@ func (d *Decoder) decodeBinarySet(bs [][]byte, v reflect.Value) error { return nil } -func (d *Decoder) decodeNumber(n *string, v reflect.Value, fieldTag tag) error { +func (d *Decoder) decodeNumber(n string, v reflect.Value, fieldTag tag) error { switch v.Kind() { case reflect.Interface: i, err := d.decodeNumberToInterface(n) @@ -297,50 +322,50 @@ func (d *Decoder) decodeNumber(n *string, v reflect.Value, fieldTag tag) error { v.Set(reflect.ValueOf(i)) return nil case reflect.String: - if v.Type() == numberType { // Support Number value type - v.Set(reflect.ValueOf(Number(*n))) + if isNumberValueType(v) { + v.SetString(n) return nil } - v.Set(reflect.ValueOf(*n)) + v.Set(reflect.ValueOf(n)) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - i, err := strconv.ParseInt(*n, 10, 64) + 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), + 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) + 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), + Value: fmt.Sprintf("number overflow, %s", n), Type: v.Type(), } } v.SetUint(i) case reflect.Float32, reflect.Float64: - i, err := strconv.ParseFloat(*n, 64) + i, err := strconv.ParseFloat(n, 64) if err != nil { return err } if v.OverflowFloat(i) { return &UnmarshalTypeError{ - Value: fmt.Sprintf("number overflow, %s", *n), + Value: fmt.Sprintf("number overflow, %s", n), Type: v.Type(), } } v.SetFloat(i) default: if v.Type().ConvertibleTo(timeType) && fieldTag.AsUnixTime { - t, err := decodeUnixTime(*n) + t, err := decodeUnixTime(n) if err != nil { return err } @@ -353,13 +378,13 @@ func (d *Decoder) decodeNumber(n *string, v reflect.Value, fieldTag tag) error { return nil } -func (d *Decoder) decodeNumberToInterface(n *string) (interface{}, error) { - if d.UseNumber { - return Number(*n), nil +func (d *Decoder) decodeNumberToInterface(n string) (interface{}, error) { + if d.options.UseNumber { + return Number(n), nil } // Default to float64 for all numbers - return strconv.ParseFloat(*n, 64) + return strconv.ParseFloat(n, 64) } func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { @@ -373,10 +398,10 @@ func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { case reflect.Array: // Limited to capacity of existing array. case reflect.Interface: - if d.UseNumber { + if d.options.UseNumber { set := make([]Number, len(ns)) for i, n := range ns { - if err := d.decodeNumber(&n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { + if err := d.decodeNumber(n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { return err } } @@ -384,7 +409,7 @@ func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { } else { set := make([]float64, len(ns)) for i, n := range ns { - if err := d.decodeNumber(&n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { + if err := d.decodeNumber(n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { return err } } @@ -399,9 +424,9 @@ func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { v.SetLen(i + 1) u, elem := indirect(v.Index(i), false) if u != nil { - return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValue{NS: ns}) + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberNS{Value: ns}) } - if err := d.decodeNumber(&ns[i], elem, tag{}); err != nil { + if err := d.decodeNumber(ns[i], elem, tag{}); err != nil { return err } } @@ -422,7 +447,7 @@ func (d *Decoder) decodeList(avList []types.AttributeValue, v reflect.Value) err 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 { + if err := d.decode(av, reflect.ValueOf(&s[i]).Elem(), tag{}); err != nil { return err } } @@ -435,7 +460,7 @@ func (d *Decoder) decodeList(avList []types.AttributeValue, v reflect.Value) err // 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 { + if err := d.decode(avList[i], v.Index(i), tag{}); err != nil { return err } } @@ -466,20 +491,22 @@ func (d *Decoder) decodeMap(avMap map[string]types.AttributeValue, v reflect.Val key := reflect.New(v.Type().Key()).Elem() key.SetString(k) elem := reflect.New(v.Type().Elem()).Elem() - if err := d.decode(&av, elem, tag{}); err != nil { + 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) + fields := unionStructFields(v.Type(), structFieldOptions{ + TagKey: d.options.TagKey, + }) 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 { + if err := d.decode(av, fv, f.tag); err != nil { return err } } @@ -497,7 +524,7 @@ func (d *Decoder) decodeNull(v reflect.Value) error { return nil } -func (d *Decoder) decodeString(s *string, v reflect.Value, fieldTag tag) error { +func (d *Decoder) decodeString(s string, v reflect.Value, fieldTag tag) error { if fieldTag.AsString { return d.decodeNumber(s, v, fieldTag) } @@ -505,7 +532,7 @@ func (d *Decoder) decodeString(s *string, v reflect.Value, fieldTag tag) error { // To maintain backwards compatibility with ConvertFrom family of methods which // converted strings to time.Time structs if v.Type().ConvertibleTo(timeType) { - t, err := time.Parse(time.RFC3339, *s) + t, err := time.Parse(time.RFC3339, s) if err != nil { return err } @@ -515,10 +542,10 @@ func (d *Decoder) decodeString(s *string, v reflect.Value, fieldTag tag) error { switch v.Kind() { case reflect.String: - v.SetString(*s) + v.SetString(s) case reflect.Interface: // Ensure type aliasing is handled properly - v.Set(reflect.ValueOf(*s).Convert(v.Type())) + v.Set(reflect.ValueOf(s).Convert(v.Type())) default: return &UnmarshalTypeError{Value: "string", Type: v.Type()} } @@ -538,7 +565,7 @@ func (d *Decoder) decodeStringSet(ss []string, v reflect.Value) error { 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 { + if err := d.decodeString(s, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { return err } } @@ -552,9 +579,9 @@ func (d *Decoder) decodeStringSet(ss []string, v reflect.Value) error { v.SetLen(i + 1) u, elem := indirect(v.Index(i), false) if u != nil { - return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValue{SS: ss}) + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberSS{Value: ss}) } - if err := d.decodeString(&ss[i], elem, tag{}); err != nil { + if err := d.decodeString(ss[i], elem, tag{}); err != nil { return err } } @@ -636,17 +663,10 @@ 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 } @@ -654,81 +674,43 @@ type UnmarshalTypeError struct { // 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() + return fmt.Sprintf("unmarshal failed, cannot unmarshal %s into Go value type %s", + e.Value, 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 { + var msg string if e.Type == nil { - return "cannot unmarshal to nil value" + msg = "cannot unmarshal to nil value" + } else if e.Type.Kind() != reflect.Ptr { + msg = fmt.Sprintf("cannot unmarshal to non-pointer value, got %s", e.Type.String()) + } else { + msg = fmt.Sprintf("cannot unmarshal to nil value, %s", e.Type.String()) } - 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() + + return fmt.Sprintf("unmarshal failed, %s", msg) } -// An UnmarshalError wraps an error that occured while unmarshaling a DynamoDB +// An UnmarshalError wraps an error that occurred while unmarshaling a DynamoDB // AttributeValue element into a Go type. This is different from UnmarshalTypeError -// in that it wraps the underlying error that occured. +// in that it wraps the underlying error that occurred. 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 +// Error returns the string representation of the error satisfying the 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()) +func (e *UnmarshalError) Error() string { + return fmt.Sprintf("unmarshal failed, cannot unmarshal %q into %s, %v", + e.Value, e.Type.String(), e.Err) } diff --git a/feature/dynamodb/attributevalue/decode_test.go b/feature/dynamodb/attributevalue/decode_test.go index 9ddc4d0b7df..59f05a9a449 100644 --- a/feature/dynamodb/attributevalue/decode_test.go +++ b/feature/dynamodb/attributevalue/decode_test.go @@ -12,8 +12,8 @@ import ( ) func TestUnmarshalShared(t *testing.T) { - for i, c := range sharedTestCases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range sharedTestCases { + t.Run(name, func(t *testing.T) { err := Unmarshal(c.in, c.actual) assertConvertTest(t, c.actual, c.expected, err, c.err) }) @@ -22,7 +22,7 @@ func TestUnmarshalShared(t *testing.T) { func TestUnmarshal(t *testing.T) { cases := []struct { - in *types.AttributeValue + in types.AttributeValue actual, expected interface{} err error }{ @@ -30,35 +30,35 @@ func TestUnmarshal(t *testing.T) { // Sets //------------ { - in: &types.AttributeValue{BS: [][]byte{ + in: &types.AttributeValueMemberBS{Value: [][]byte{ {48, 49}, {50, 51}, }}, actual: &[][]byte{}, expected: [][]byte{{48, 49}, {50, 51}}, }, { - in: &types.AttributeValue{NS: []string{ + in: &types.AttributeValueMemberNS{Value: []string{ "123", "321", }}, actual: &[]int{}, expected: []int{123, 321}, }, { - in: &types.AttributeValue{NS: []string{ + in: &types.AttributeValueMemberNS{Value: []string{ "123", "321", }}, actual: &[]interface{}{}, expected: []interface{}{123., 321.}, }, { - in: &types.AttributeValue{SS: []string{ + in: &types.AttributeValueMemberSS{Value: []string{ "abc", "123", }}, actual: &[]string{}, expected: &[]string{"abc", "123"}, }, { - in: &types.AttributeValue{SS: []string{ + in: &types.AttributeValueMemberSS{Value: []string{ "abc", "123", }}, actual: &[]*string{}, @@ -68,7 +68,7 @@ func TestUnmarshal(t *testing.T) { // Interfaces //------------ { - in: &types.AttributeValue{B: []byte{48, 49}}, + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, actual: func() interface{} { var v interface{} return &v @@ -76,7 +76,7 @@ func TestUnmarshal(t *testing.T) { expected: []byte{48, 49}, }, { - in: &types.AttributeValue{BS: [][]byte{ + in: &types.AttributeValueMemberBS{Value: [][]byte{ {48, 49}, {50, 51}, }}, actual: func() interface{} { @@ -86,7 +86,7 @@ func TestUnmarshal(t *testing.T) { expected: [][]byte{{48, 49}, {50, 51}}, }, { - in: &types.AttributeValue{BOOL: aws.Bool(true)}, + in: &types.AttributeValueMemberBOOL{Value: true}, actual: func() interface{} { var v interface{} return &v @@ -94,8 +94,9 @@ func TestUnmarshal(t *testing.T) { expected: bool(true), }, { - in: &types.AttributeValue{L: []types.AttributeValue{ - {S: aws.String("abc")}, {S: aws.String("123")}, + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberS{Value: "123"}, }}, actual: func() interface{} { var v interface{} @@ -104,9 +105,9 @@ func TestUnmarshal(t *testing.T) { expected: []interface{}{"abc", "123"}, }, { - in: &types.AttributeValue{M: map[string]types.AttributeValue{ - "123": {S: aws.String("abc")}, - "abc": {S: aws.String("123")}, + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "123": &types.AttributeValueMemberS{Value: "abc"}, + "abc": &types.AttributeValueMemberS{Value: "123"}, }}, actual: func() interface{} { var v interface{} @@ -115,7 +116,7 @@ func TestUnmarshal(t *testing.T) { expected: map[string]interface{}{"123": "abc", "abc": "123"}, }, { - in: &types.AttributeValue{N: aws.String("123")}, + in: &types.AttributeValueMemberN{Value: "123"}, actual: func() interface{} { var v interface{} return &v @@ -123,7 +124,7 @@ func TestUnmarshal(t *testing.T) { expected: float64(123), }, { - in: &types.AttributeValue{NS: []string{ + in: &types.AttributeValueMemberNS{Value: []string{ "123", "321", }}, actual: func() interface{} { @@ -133,7 +134,7 @@ func TestUnmarshal(t *testing.T) { expected: []float64{123., 321.}, }, { - in: &types.AttributeValue{S: aws.String("123")}, + in: &types.AttributeValueMemberS{Value: "123"}, actual: func() interface{} { var v interface{} return &v @@ -141,7 +142,31 @@ func TestUnmarshal(t *testing.T) { expected: "123", }, { - in: &types.AttributeValue{SS: []string{ + in: &types.AttributeValueMemberNULL{Value: true}, + actual: func() interface{} { + var v string + return &v + }(), + expected: "", + }, + { + in: &types.AttributeValueMemberNULL{Value: true}, + actual: func() interface{} { + v := new(string) + return &v + }(), + expected: nil, + }, + { + in: &types.AttributeValueMemberS{Value: ""}, + actual: func() interface{} { + v := new(string) + return &v + }(), + expected: aws.String(""), + }, + { + in: &types.AttributeValueMemberSS{Value: []string{ "123", "321", }}, actual: func() interface{} { @@ -151,15 +176,15 @@ func TestUnmarshal(t *testing.T) { expected: []string{"123", "321"}, }, { - in: &types.AttributeValue{M: map[string]types.AttributeValue{ - "abc": {S: aws.String("123")}, - "Cba": {S: aws.String("321")}, + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberS{Value: "123"}, + "Cba": &types.AttributeValueMemberS{Value: "321"}, }}, actual: &struct{ Abc, Cba string }{}, expected: struct{ Abc, Cba string }{Abc: "123", Cba: "321"}, }, { - in: &types.AttributeValue{N: aws.String("512")}, + in: &types.AttributeValueMemberN{Value: "512"}, actual: new(uint8), err: &UnmarshalTypeError{ Value: fmt.Sprintf("number overflow, 512"), @@ -179,28 +204,29 @@ func TestUnmarshal(t *testing.T) { func TestInterfaceInput(t *testing.T) { var v interface{} expected := []interface{}{"abc", "123"} - err := Unmarshal(&types.AttributeValue{L: []types.AttributeValue{ - {S: aws.String("abc")}, {S: aws.String("123")}, + err := Unmarshal(&types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberS{Value: "123"}, }}, &v) assertConvertTest(t, v, expected, err, nil) } func TestUnmarshalError(t *testing.T) { - cases := []struct { - in *types.AttributeValue + cases := map[string]struct { + in types.AttributeValue actual, expected interface{} err error }{ - { - in: &types.AttributeValue{}, + "invalid unmarshal": { + in: nil, actual: int(0), expected: nil, err: &InvalidUnmarshalError{Type: reflect.TypeOf(int(0))}, }, } - for i, c := range cases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { err := Unmarshal(c.in, c.actual) assertConvertTest(t, c.actual, c.expected, err, c.err) }) @@ -208,8 +234,8 @@ func TestUnmarshalError(t *testing.T) { } func TestUnmarshalListShared(t *testing.T) { - for i, c := range sharedListTestCases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range sharedListTestCases { + t.Run(name, func(t *testing.T) { err := UnmarshalList(c.in, c.actual) assertConvertTest(t, c.actual, c.expected, err, c.err) }) @@ -217,12 +243,12 @@ func TestUnmarshalListShared(t *testing.T) { } func TestUnmarshalListError(t *testing.T) { - cases := []struct { + cases := map[string]struct { in []types.AttributeValue actual, expected interface{} err error }{ - { + "invalid unmarshal": { in: []types.AttributeValue{}, actual: []interface{}{}, expected: nil, @@ -230,8 +256,8 @@ func TestUnmarshalListError(t *testing.T) { }, } - for i, c := range cases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { err := UnmarshalList(c.in, c.actual) assertConvertTest(t, c.actual, c.expected, err, c.err) }) @@ -239,8 +265,8 @@ func TestUnmarshalListError(t *testing.T) { } func TestUnmarshalMapShared(t *testing.T) { - for i, c := range sharedMapTestCases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range sharedMapTestCases { + t.Run(name, func(t *testing.T) { err := UnmarshalMap(c.in, c.actual) assertConvertTest(t, c.actual, c.expected, err, c.err) }) @@ -261,7 +287,7 @@ func TestUnmarshalMapError(t *testing.T) { }, { in: map[string]types.AttributeValue{ - "BOOL": {BOOL: aws.Bool(true)}, + "BOOL": &types.AttributeValueMemberBOOL{Value: true}, }, actual: &map[int]interface{}{}, expected: nil, @@ -283,17 +309,15 @@ func TestUnmarshalListOfMaps(t *testing.T) { Value2 int } - cases := []struct { + cases := map[string]struct { in []map[string]types.AttributeValue actual, expected interface{} err error }{ - { // Simple map conversion. + "simple map conversion": { in: []map[string]types.AttributeValue{ { - "Value": types.AttributeValue{ - BOOL: aws.Bool(true), - }, + "Value": &types.AttributeValueMemberBOOL{Value: true}, }, }, actual: &[]map[string]interface{}{}, @@ -303,15 +327,11 @@ func TestUnmarshalListOfMaps(t *testing.T) { }, }, }, - { // attribute to struct. + "attribute to struct": { in: []map[string]types.AttributeValue{ { - "Value": types.AttributeValue{ - S: aws.String("abc"), - }, - "Value2": types.AttributeValue{ - N: aws.String("123"), - }, + "Value": &types.AttributeValueMemberS{Value: "abc"}, + "Value2": &types.AttributeValueMemberN{Value: "123"}, }, }, actual: &[]testItem{}, @@ -324,8 +344,8 @@ func TestUnmarshalListOfMaps(t *testing.T) { }, } - for i, c := range cases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { err := UnmarshalListOfMaps(c.in, c.actual) assertConvertTest(t, c.actual, c.expected, err, c.err) }) @@ -339,45 +359,46 @@ type unmarshalUnmarshaler struct { Value4 time.Time } -func (u *unmarshalUnmarshaler) UnmarshalDynamoDBAttributeValue(av *types.AttributeValue) error { - if av.M == nil { +func (u *unmarshalUnmarshaler) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error { + m, ok := av.(*types.AttributeValueMemberM) + if !ok || m == nil { return fmt.Errorf("expected AttributeValue to be map") } - if v, ok := av.M["abc"]; !ok { + if v, ok := m.Value["abc"]; !ok { return fmt.Errorf("expected `abc` map key") - } else if v.S == nil { + } else if vv, kk := v.(*types.AttributeValueMemberS); !kk || vv == nil { return fmt.Errorf("expected `abc` map value string") } else { - u.Value = *v.S + u.Value = vv.Value } - if v, ok := av.M["def"]; !ok { + if v, ok := m.Value["def"]; !ok { return fmt.Errorf("expected `def` map key") - } else if v.N == nil { + } else if vv, kk := v.(*types.AttributeValueMemberN); !kk || vv == nil { return fmt.Errorf("expected `def` map value number") } else { - n, err := strconv.ParseInt(*v.N, 10, 64) + n, err := strconv.ParseInt(vv.Value, 10, 64) if err != nil { return err } u.Value2 = int(n) } - if v, ok := av.M["ghi"]; !ok { + if v, ok := m.Value["ghi"]; !ok { return fmt.Errorf("expected `ghi` map key") - } else if v.BOOL == nil { + } else if vv, kk := v.(*types.AttributeValueMemberBOOL); !kk || vv == nil { return fmt.Errorf("expected `ghi` map value number") } else { - u.Value3 = *v.BOOL + u.Value3 = vv.Value } - if v, ok := av.M["jkl"]; !ok { + if v, ok := m.Value["jkl"]; !ok { return fmt.Errorf("expected `jkl` map key") - } else if v.S == nil { + } else if vv, kk := v.(*types.AttributeValueMemberS); !kk || vv == nil { return fmt.Errorf("expected `jkl` map value string") } else { - t, err := time.Parse(time.RFC3339, *v.S) + t, err := time.Parse(time.RFC3339, vv.Value) if err != nil { return err } @@ -389,12 +410,12 @@ func (u *unmarshalUnmarshaler) UnmarshalDynamoDBAttributeValue(av *types.Attribu func TestUnmarshalUnmashaler(t *testing.T) { u := &unmarshalUnmarshaler{} - av := &types.AttributeValue{ - M: map[string]types.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")}, + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberS{Value: "value"}, + "def": &types.AttributeValueMemberN{Value: "123"}, + "ghi": &types.AttributeValueMemberBOOL{Value: true}, + "jkl": &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, }, } @@ -419,16 +440,16 @@ func TestUnmarshalUnmashaler(t *testing.T) { func TestDecodeUseNumber(t *testing.T) { u := map[string]interface{}{} - av := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "abc": {S: aws.String("value")}, - "def": {N: aws.String("123")}, - "ghi": {BOOL: aws.Bool(true)}, + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberS{Value: "value"}, + "def": &types.AttributeValueMemberN{Value: "123"}, + "ghi": &types.AttributeValueMemberBOOL{Value: true}, }, } - decoder := NewDecoder(func(d *Decoder) { - d.UseNumber = true + decoder := NewDecoder(func(o *DecoderOptions) { + o.UseNumber = true }) err := decoder.Decode(av, &u) if err != nil { @@ -449,18 +470,18 @@ func TestDecodeUseNumber(t *testing.T) { func TestDecodeUseNumberNumberSet(t *testing.T) { u := map[string]interface{}{} - av := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "ns": { - NS: []string{ + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "ns": &types.AttributeValueMemberNS{ + Value: []string{ "123", "321", }, }, }, } - decoder := NewDecoder(func(d *Decoder) { - d.UseNumber = true + decoder := NewDecoder(func(o *DecoderOptions) { + o.UseNumber = true }) err := decoder.Decode(av, &u) if err != nil { @@ -489,14 +510,10 @@ func TestDecodeEmbeddedPointerStruct(t *testing.T) { *B *C } - av := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Aint": { - N: aws.String("321"), - }, - "Bint": { - N: aws.String("123"), - }, + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Aint": &types.AttributeValueMemberN{Value: "321"}, + "Bint": &types.AttributeValueMemberN{Value: "123"}, }, } decoder := NewDecoder() @@ -521,9 +538,7 @@ func TestDecodeEmbeddedPointerStruct(t *testing.T) { func TestDecodeBooleanOverlay(t *testing.T) { type BooleanOverlay bool - av := &types.AttributeValue{ - BOOL: aws.Bool(true), - } + av := &types.AttributeValueMemberBOOL{Value: true} decoder := NewDecoder() @@ -551,17 +566,11 @@ func TestDecodeUnixTime(t *testing.T) { Typed: UnixTime(time.Unix(789, 0)), } - input := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Normal": { - S: aws.String("1970-01-01T00:02:03Z"), - }, - "Tagged": { - N: aws.String("456"), - }, - "Typed": { - N: aws.String("789"), - }, + input := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Normal": &types.AttributeValueMemberS{Value: "1970-01-01T00:02:03Z"}, + "Tagged": &types.AttributeValueMemberN{Value: "456"}, + "Typed": &types.AttributeValueMemberN{Value: "789"}, }, } actual := A{} @@ -586,14 +595,10 @@ func TestDecodeAliasedUnixTime(t *testing.T) { Tagged: AliasedTime(time.Unix(456, 0)), } - input := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Normal": { - S: aws.String("1970-01-01T00:02:03Z"), - }, - "Tagged": { - N: aws.String("456"), - }, + input := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Normal": &types.AttributeValueMemberS{Value: "1970-01-01T00:02:03Z"}, + "Tagged": &types.AttributeValueMemberN{Value: "456"}, }, } actual := A{} diff --git a/feature/dynamodb/attributevalue/doc.go b/feature/dynamodb/attributevalue/doc.go index 19c6650448c..c1cbd19d989 100644 --- a/feature/dynamodb/attributevalue/doc.go +++ b/feature/dynamodb/attributevalue/doc.go @@ -1,5 +1,5 @@ // Package attributevalue provides marshaling and unmarshaling utilities to -// convert between Go types and types.AttributeValues. +// convert between Go types and Amazon DynamoDB types.AttributeValues. // // These utilities allow you to marshal slices, maps, structs, and scalar values // to and from types.AttributeValue. These are useful when marshaling diff --git a/feature/dynamodb/attributevalue/empty_collections_test.go b/feature/dynamodb/attributevalue/empty_collections_test.go index d16db3bd0f0..588108907e3 100644 --- a/feature/dynamodb/attributevalue/empty_collections_test.go +++ b/feature/dynamodb/attributevalue/empty_collections_test.go @@ -1,11 +1,11 @@ package attributevalue import ( - "reflect" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/google/go-cmp/cmp" ) type testEmptyCollectionsNumericalScalars struct { @@ -105,43 +105,43 @@ type testEmptyCollectionStructOmitted struct { } var sharedEmptyCollectionsTestCases = map[string]struct { - in *types.AttributeValue + in types.AttributeValue actual, expected interface{} err error }{ "scalars with zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "String": {NULL: aws.Bool(true)}, - "Uint8": {N: aws.String("0")}, - "Uint16": {N: aws.String("0")}, - "Uint32": {N: aws.String("0")}, - "Uint64": {N: aws.String("0")}, - "Int8": {N: aws.String("0")}, - "Int16": {N: aws.String("0")}, - "Int32": {N: aws.String("0")}, - "Int64": {N: aws.String("0")}, - "Float32": {N: aws.String("0")}, - "Float64": {N: aws.String("0")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "String": &types.AttributeValueMemberS{Value: ""}, + "Uint8": &types.AttributeValueMemberN{Value: "0"}, + "Uint16": &types.AttributeValueMemberN{Value: "0"}, + "Uint32": &types.AttributeValueMemberN{Value: "0"}, + "Uint64": &types.AttributeValueMemberN{Value: "0"}, + "Int8": &types.AttributeValueMemberN{Value: "0"}, + "Int16": &types.AttributeValueMemberN{Value: "0"}, + "Int32": &types.AttributeValueMemberN{Value: "0"}, + "Int64": &types.AttributeValueMemberN{Value: "0"}, + "Float32": &types.AttributeValueMemberN{Value: "0"}, + "Float64": &types.AttributeValueMemberN{Value: "0"}, }, }, actual: &testEmptyCollectionsNumericalScalars{}, expected: testEmptyCollectionsNumericalScalars{}, }, "scalars with non-zero values": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "String": {S: aws.String("test string")}, - "Uint8": {N: aws.String("1")}, - "Uint16": {N: aws.String("2")}, - "Uint32": {N: aws.String("3")}, - "Uint64": {N: aws.String("4")}, - "Int8": {N: aws.String("-5")}, - "Int16": {N: aws.String("-6")}, - "Int32": {N: aws.String("-7")}, - "Int64": {N: aws.String("-8")}, - "Float32": {N: aws.String("9.9")}, - "Float64": {N: aws.String("10.1")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "String": &types.AttributeValueMemberS{Value: "test string"}, + "Uint8": &types.AttributeValueMemberN{Value: "1"}, + "Uint16": &types.AttributeValueMemberN{Value: "2"}, + "Uint32": &types.AttributeValueMemberN{Value: "3"}, + "Uint64": &types.AttributeValueMemberN{Value: "4"}, + "Int8": &types.AttributeValueMemberN{Value: "-5"}, + "Int16": &types.AttributeValueMemberN{Value: "-6"}, + "Int32": &types.AttributeValueMemberN{Value: "-7"}, + "Int64": &types.AttributeValueMemberN{Value: "-8"}, + "Float32": &types.AttributeValueMemberN{Value: "9.9"}, + "Float64": &types.AttributeValueMemberN{Value: "10.1"}, }, }, actual: &testEmptyCollectionsNumericalScalars{}, @@ -160,24 +160,24 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "omittable scalars with zero value": { - in: &types.AttributeValue{M: map[string]types.AttributeValue{}}, + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, actual: &testEmptyCollectionsOmittedNumericalScalars{}, expected: testEmptyCollectionsOmittedNumericalScalars{}, }, "omittable scalars with non-zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "String": {S: aws.String("test string")}, - "Uint8": {N: aws.String("1")}, - "Uint16": {N: aws.String("2")}, - "Uint32": {N: aws.String("3")}, - "Uint64": {N: aws.String("4")}, - "Int8": {N: aws.String("-5")}, - "Int16": {N: aws.String("-6")}, - "Int32": {N: aws.String("-7")}, - "Int64": {N: aws.String("-8")}, - "Float32": {N: aws.String("9.9")}, - "Float64": {N: aws.String("10.1")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "String": &types.AttributeValueMemberS{Value: "test string"}, + "Uint8": &types.AttributeValueMemberN{Value: "1"}, + "Uint16": &types.AttributeValueMemberN{Value: "2"}, + "Uint32": &types.AttributeValueMemberN{Value: "3"}, + "Uint64": &types.AttributeValueMemberN{Value: "4"}, + "Int8": &types.AttributeValueMemberN{Value: "-5"}, + "Int16": &types.AttributeValueMemberN{Value: "-6"}, + "Int32": &types.AttributeValueMemberN{Value: "-7"}, + "Int64": &types.AttributeValueMemberN{Value: "-8"}, + "Float32": &types.AttributeValueMemberN{Value: "9.9"}, + "Float64": &types.AttributeValueMemberN{Value: "10.1"}, }, }, actual: &testEmptyCollectionsOmittedNumericalScalars{}, @@ -196,38 +196,38 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "nil pointer scalars": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "PtrString": {NULL: aws.Bool(true)}, - "PtrUint8": {NULL: aws.Bool(true)}, - "PtrUint16": {NULL: aws.Bool(true)}, - "PtrUint32": {NULL: aws.Bool(true)}, - "PtrUint64": {NULL: aws.Bool(true)}, - "PtrInt8": {NULL: aws.Bool(true)}, - "PtrInt16": {NULL: aws.Bool(true)}, - "PtrInt32": {NULL: aws.Bool(true)}, - "PtrInt64": {NULL: aws.Bool(true)}, - "PtrFloat32": {NULL: aws.Bool(true)}, - "PtrFloat64": {NULL: aws.Bool(true)}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberNULL{Value: true}, + "PtrUint8": &types.AttributeValueMemberNULL{Value: true}, + "PtrUint16": &types.AttributeValueMemberNULL{Value: true}, + "PtrUint32": &types.AttributeValueMemberNULL{Value: true}, + "PtrUint64": &types.AttributeValueMemberNULL{Value: true}, + "PtrInt8": &types.AttributeValueMemberNULL{Value: true}, + "PtrInt16": &types.AttributeValueMemberNULL{Value: true}, + "PtrInt32": &types.AttributeValueMemberNULL{Value: true}, + "PtrInt64": &types.AttributeValueMemberNULL{Value: true}, + "PtrFloat32": &types.AttributeValueMemberNULL{Value: true}, + "PtrFloat64": &types.AttributeValueMemberNULL{Value: true}, }, }, actual: &testEmptyCollectionsPtrScalars{}, expected: testEmptyCollectionsPtrScalars{}, }, "non-nil pointer to scalars with zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "PtrString": {NULL: aws.Bool(true)}, - "PtrUint8": {N: aws.String("0")}, - "PtrUint16": {N: aws.String("0")}, - "PtrUint32": {N: aws.String("0")}, - "PtrUint64": {N: aws.String("0")}, - "PtrInt8": {N: aws.String("0")}, - "PtrInt16": {N: aws.String("0")}, - "PtrInt32": {N: aws.String("0")}, - "PtrInt64": {N: aws.String("0")}, - "PtrFloat32": {N: aws.String("0")}, - "PtrFloat64": {N: aws.String("0")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberNULL{Value: true}, + "PtrUint8": &types.AttributeValueMemberN{Value: "0"}, + "PtrUint16": &types.AttributeValueMemberN{Value: "0"}, + "PtrUint32": &types.AttributeValueMemberN{Value: "0"}, + "PtrUint64": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt8": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt16": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt32": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt64": &types.AttributeValueMemberN{Value: "0"}, + "PtrFloat32": &types.AttributeValueMemberN{Value: "0"}, + "PtrFloat64": &types.AttributeValueMemberN{Value: "0"}, }, }, actual: &testEmptyCollectionsPtrScalars{}, @@ -245,19 +245,19 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "pointer scalars non-nil non-zero": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "PtrString": {S: aws.String("test string")}, - "PtrUint8": {N: aws.String("1")}, - "PtrUint16": {N: aws.String("2")}, - "PtrUint32": {N: aws.String("3")}, - "PtrUint64": {N: aws.String("4")}, - "PtrInt8": {N: aws.String("-5")}, - "PtrInt16": {N: aws.String("-6")}, - "PtrInt32": {N: aws.String("-7")}, - "PtrInt64": {N: aws.String("-8")}, - "PtrFloat32": {N: aws.String("9.9")}, - "PtrFloat64": {N: aws.String("10.1")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberS{Value: "test string"}, + "PtrUint8": &types.AttributeValueMemberN{Value: "1"}, + "PtrUint16": &types.AttributeValueMemberN{Value: "2"}, + "PtrUint32": &types.AttributeValueMemberN{Value: "3"}, + "PtrUint64": &types.AttributeValueMemberN{Value: "4"}, + "PtrInt8": &types.AttributeValueMemberN{Value: "-5"}, + "PtrInt16": &types.AttributeValueMemberN{Value: "-6"}, + "PtrInt32": &types.AttributeValueMemberN{Value: "-7"}, + "PtrInt64": &types.AttributeValueMemberN{Value: "-8"}, + "PtrFloat32": &types.AttributeValueMemberN{Value: "9.9"}, + "PtrFloat64": &types.AttributeValueMemberN{Value: "10.1"}, }, }, actual: &testEmptyCollectionsPtrScalars{}, @@ -276,25 +276,25 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "omittable nil pointer scalars": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{}, }, actual: &testEmptyCollectionsOmittedPtrNumericalScalars{}, expected: testEmptyCollectionsOmittedPtrNumericalScalars{}, }, "omittable non-nil pointer to scalars with zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "PtrUint8": {N: aws.String("0")}, - "PtrUint16": {N: aws.String("0")}, - "PtrUint32": {N: aws.String("0")}, - "PtrUint64": {N: aws.String("0")}, - "PtrInt8": {N: aws.String("0")}, - "PtrInt16": {N: aws.String("0")}, - "PtrInt32": {N: aws.String("0")}, - "PtrInt64": {N: aws.String("0")}, - "PtrFloat32": {N: aws.String("0")}, - "PtrFloat64": {N: aws.String("0")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrUint8": &types.AttributeValueMemberN{Value: "0"}, + "PtrUint16": &types.AttributeValueMemberN{Value: "0"}, + "PtrUint32": &types.AttributeValueMemberN{Value: "0"}, + "PtrUint64": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt8": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt16": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt32": &types.AttributeValueMemberN{Value: "0"}, + "PtrInt64": &types.AttributeValueMemberN{Value: "0"}, + "PtrFloat32": &types.AttributeValueMemberN{Value: "0"}, + "PtrFloat64": &types.AttributeValueMemberN{Value: "0"}, }, }, actual: &testEmptyCollectionsOmittedPtrNumericalScalars{}, @@ -312,18 +312,18 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "omittable non-nil pointer to non-zero scalar": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "PtrUint8": {N: aws.String("1")}, - "PtrUint16": {N: aws.String("2")}, - "PtrUint32": {N: aws.String("3")}, - "PtrUint64": {N: aws.String("4")}, - "PtrInt8": {N: aws.String("-5")}, - "PtrInt16": {N: aws.String("-6")}, - "PtrInt32": {N: aws.String("-7")}, - "PtrInt64": {N: aws.String("-8")}, - "PtrFloat32": {N: aws.String("9.9")}, - "PtrFloat64": {N: aws.String("10.1")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrUint8": &types.AttributeValueMemberN{Value: "1"}, + "PtrUint16": &types.AttributeValueMemberN{Value: "2"}, + "PtrUint32": &types.AttributeValueMemberN{Value: "3"}, + "PtrUint64": &types.AttributeValueMemberN{Value: "4"}, + "PtrInt8": &types.AttributeValueMemberN{Value: "-5"}, + "PtrInt16": &types.AttributeValueMemberN{Value: "-6"}, + "PtrInt32": &types.AttributeValueMemberN{Value: "-7"}, + "PtrInt64": &types.AttributeValueMemberN{Value: "-8"}, + "PtrFloat32": &types.AttributeValueMemberN{Value: "9.9"}, + "PtrFloat64": &types.AttributeValueMemberN{Value: "10.1"}, }, }, actual: &testEmptyCollectionsOmittedPtrNumericalScalars{}, @@ -341,32 +341,32 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "maps slices nil values": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Map": {NULL: aws.Bool(true)}, - "Slice": {NULL: aws.Bool(true)}, - "ByteSlice": {NULL: aws.Bool(true)}, - "ByteArray": {B: make([]byte, 4)}, - "ZeroArray": {B: make([]byte, 0)}, - "BinarySet": {NULL: aws.Bool(true)}, - "NumberSet": {NULL: aws.Bool(true)}, - "StringSet": {NULL: aws.Bool(true)}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Map": &types.AttributeValueMemberNULL{Value: true}, + "Slice": &types.AttributeValueMemberNULL{Value: true}, + "ByteSlice": &types.AttributeValueMemberNULL{Value: true}, + "ByteArray": &types.AttributeValueMemberB{Value: make([]byte, 4)}, + "ZeroArray": &types.AttributeValueMemberB{Value: make([]byte, 0)}, + "BinarySet": &types.AttributeValueMemberNULL{Value: true}, + "NumberSet": &types.AttributeValueMemberNULL{Value: true}, + "StringSet": &types.AttributeValueMemberNULL{Value: true}, }, }, actual: &testEmptyCollectionTypes{}, expected: testEmptyCollectionTypes{}, }, "maps slices zero values": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Map": {M: map[string]types.AttributeValue{}}, - "Slice": {L: []types.AttributeValue{}}, - "ByteSlice": {B: []byte{}}, - "ByteArray": {B: make([]byte, 4)}, - "ZeroArray": {B: make([]byte, 0)}, - "BinarySet": {BS: [][]byte{}}, - "NumberSet": {NS: []string{}}, - "StringSet": {SS: []string{}}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Map": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{}}, + "ByteSlice": &types.AttributeValueMemberB{Value: []byte{}}, + "ByteArray": &types.AttributeValueMemberB{Value: make([]byte, 4)}, + "ZeroArray": &types.AttributeValueMemberB{Value: make([]byte, 0)}, + "BinarySet": &types.AttributeValueMemberBS{Value: [][]byte{}}, + "NumberSet": &types.AttributeValueMemberNS{Value: []string{}}, + "StringSet": &types.AttributeValueMemberSS{Value: []string{}}, }, }, actual: &testEmptyCollectionTypes{}, @@ -382,20 +382,23 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "maps slices non-zero values": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Map": { - M: map[string]types.AttributeValue{ - "key": {S: aws.String("value")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Map": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{Value: "value"}, }, }, - "Slice": {L: []types.AttributeValue{{S: aws.String("test")}, {S: aws.String("slice")}}}, - "ByteSlice": {B: []byte{0, 1}}, - "ByteArray": {B: []byte{0, 1, 2, 3}}, - "ZeroArray": {B: make([]byte, 0)}, - "BinarySet": {BS: [][]byte{{0, 1}, {2, 3}}}, - "NumberSet": {NS: []string{"0", "1"}}, - "StringSet": {SS: []string{"test", "slice"}}, + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "test"}, + &types.AttributeValueMemberS{Value: "slice"}, + }}, + "ByteSlice": &types.AttributeValueMemberB{Value: []byte{0, 1}}, + "ByteArray": &types.AttributeValueMemberB{Value: []byte{0, 1, 2, 3}}, + "ZeroArray": &types.AttributeValueMemberB{Value: make([]byte, 0)}, + "BinarySet": &types.AttributeValueMemberBS{Value: [][]byte{{0, 1}, {2, 3}}}, + "NumberSet": &types.AttributeValueMemberNS{Value: []string{"0", "1"}}, + "StringSet": &types.AttributeValueMemberSS{Value: []string{"test", "slice"}}, }, }, actual: &testEmptyCollectionTypes{}, @@ -411,24 +414,24 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "omittable maps slices nil values": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "ByteArray": {B: make([]byte, 4)}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "ByteArray": &types.AttributeValueMemberB{Value: make([]byte, 4)}, }, }, actual: &testEmptyCollectionTypesOmitted{}, expected: testEmptyCollectionTypesOmitted{}, }, "omittable maps slices zero values": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Map": {M: map[string]types.AttributeValue{}}, - "Slice": {L: []types.AttributeValue{}}, - "ByteSlice": {B: []byte{}}, - "ByteArray": {B: make([]byte, 4)}, - "BinarySet": {BS: [][]byte{}}, - "NumberSet": {NS: []string{}}, - "StringSet": {SS: []string{}}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Map": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{}}, + "ByteSlice": &types.AttributeValueMemberB{Value: []byte{}}, + "ByteArray": &types.AttributeValueMemberB{Value: make([]byte, 4)}, + "BinarySet": &types.AttributeValueMemberBS{Value: [][]byte{}}, + "NumberSet": &types.AttributeValueMemberNS{Value: []string{}}, + "StringSet": &types.AttributeValueMemberSS{Value: []string{}}, }, }, actual: &testEmptyCollectionTypesOmitted{}, @@ -443,19 +446,22 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "omittable maps slices non-zero values": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Map": { - M: map[string]types.AttributeValue{ - "key": {S: aws.String("value")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Map": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{Value: "value"}, }, }, - "Slice": {L: []types.AttributeValue{{S: aws.String("test")}, {S: aws.String("slice")}}}, - "ByteSlice": {B: []byte{0, 1}}, - "ByteArray": {B: []byte{0, 1, 2, 3}}, - "BinarySet": {BS: [][]byte{{0, 1}, {2, 3}}}, - "NumberSet": {NS: []string{"0", "1"}}, - "StringSet": {SS: []string{"test", "slice"}}, + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "test"}, + &types.AttributeValueMemberS{Value: "slice"}, + }}, + "ByteSlice": &types.AttributeValueMemberB{Value: []byte{0, 1}}, + "ByteArray": &types.AttributeValueMemberB{Value: []byte{0, 1, 2, 3}}, + "BinarySet": &types.AttributeValueMemberBS{Value: [][]byte{{0, 1}, {2, 3}}}, + "NumberSet": &types.AttributeValueMemberNS{Value: []string{"0", "1"}}, + "StringSet": &types.AttributeValueMemberSS{Value: []string{"test", "slice"}}, }, }, actual: &testEmptyCollectionTypesOmitted{}, @@ -471,14 +477,14 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "structs with members zero": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Struct": { - M: map[string]types.AttributeValue{ - "Int": {N: aws.String("0")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "0"}, }, }, - "PtrStruct": {NULL: aws.Bool(true)}, + "PtrStruct": &types.AttributeValueMemberNULL{Value: true}, }, }, actual: &struct { @@ -491,16 +497,16 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }{}, }, "structs with members non-zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Struct": { - M: map[string]types.AttributeValue{ - "Int": {N: aws.String("1")}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "1"}, }, }, - "PtrStruct": { - M: map[string]types.AttributeValue{ - "Int": {N: aws.String("1")}, + "PtrStruct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "1"}, }, }, }, @@ -518,10 +524,10 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }, }, "struct with omittable members zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Struct": {M: map[string]types.AttributeValue{}}, - "PtrStruct": {NULL: aws.Bool(true)}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + "PtrStruct": &types.AttributeValueMemberNULL{Value: true}, }, }, actual: &struct { @@ -534,9 +540,9 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }{}, }, "omittable struct with omittable members zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Struct": {M: map[string]types.AttributeValue{}}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, }, }, actual: &struct { @@ -549,16 +555,20 @@ var sharedEmptyCollectionsTestCases = map[string]struct { }{}, }, "omittable struct with omittable members non-zero value": { - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Struct": { - M: map[string]types.AttributeValue{ - "Slice": {L: []types.AttributeValue{{S: aws.String("test")}}}, + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "test"}, + }}, }, }, - "InitPtrStruct": { - M: map[string]types.AttributeValue{ - "Slice": {L: []types.AttributeValue{{S: aws.String("test")}}}, + "InitPtrStruct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "test"}, + }}, }, }, }, @@ -591,34 +601,37 @@ func TestEmptyCollectionsSpecialCases(t *testing.T) { type SpecialCases struct { PtrString *string + OmittedString string `dynamodbav:",omitempty"` OmittedPtrString *string `dynamodbav:",omitempty"` } - expectedEncode := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "PtrString": {NULL: aws.Bool(true)}, + expectedEncode := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberS{Value: ""}, }, } expectedDecode := SpecialCases{} actualEncode, err := Marshal(&SpecialCases{ PtrString: aws.String(""), - OmittedPtrString: aws.String(""), + OmittedString: "", + OmittedPtrString: nil, }) if err != nil { t.Fatalf("expected no err got %v", err) } - if e, a := expectedEncode, actualEncode; !reflect.DeepEqual(e, a) { - t.Errorf("expected %v, got %v", e, a) + if diff := cmp.Diff(expectedEncode, actualEncode); len(diff) != 0 { + t.Errorf("expected encode match\n%s", diff) } var actualDecode SpecialCases - err = Unmarshal(&types.AttributeValue{}, &actualDecode) + var av types.AttributeValue + err = Unmarshal(av, &actualDecode) if err != nil { t.Fatalf("expected no err got %v", err) } - if e, a := expectedDecode, actualDecode; !reflect.DeepEqual(e, a) { - t.Errorf("expected %v, got %v", e, a) + if diff := cmp.Diff(expectedDecode, actualDecode); len(diff) != 0 { + t.Errorf("expected dencode match\n%s", diff) } } diff --git a/feature/dynamodb/attributevalue/encode.go b/feature/dynamodb/attributevalue/encode.go index 6f2155e5263..c1c906a0206 100644 --- a/feature/dynamodb/attributevalue/encode.go +++ b/feature/dynamodb/attributevalue/encode.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" ) @@ -32,12 +31,10 @@ 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 *types.AttributeValue) error { - t := time.Time(e) - s := strconv.FormatInt(t.Unix(), 10) - av.N = &s - - return nil +func (e UnixTime) MarshalDynamoDBAttributeValue() (types.AttributeValue, error) { + return &types.AttributeValueMemberN{ + Value: strconv.FormatInt(time.Time(e).Unix(), 10), + }, nil } // UnmarshalDynamoDBAttributeValue implements the Unmarshaler interface so that @@ -46,8 +43,16 @@ func (e UnixTime) MarshalDynamoDBAttributeValue(av *types.AttributeValue) error // // If an error parsing the AttributeValue number occurs UnmarshalError will be // returned. -func (e *UnixTime) UnmarshalDynamoDBAttributeValue(av *types.AttributeValue) error { - t, err := decodeUnixTime(aws.ToString(av.N)) +func (e *UnixTime) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error { + tv, ok := av.(*types.AttributeValueMemberN) + if !ok { + return &UnmarshalTypeError{ + Value: fmt.Sprintf("%T", av), + Type: reflect.TypeOf((*UnixTime)(nil)), + } + } + + t, err := decodeUnixTime(tv.Value) if err != nil { return err } @@ -60,17 +65,17 @@ func (e *UnixTime) UnmarshalDynamoDBAttributeValue(av *types.AttributeValue) err // to AttributeValues. Use this to provide custom logic determining how a // Go Value type should be marshaled. // -// type ExampleMarshaler struct { -// Value int +// type CustomIntType struct { +// Value Int // } -// func (m *ExampleMarshaler) MarshalDynamoDBAttributeValue(av *types.AttributeValue) error { -// n := fmt.Sprintf("%v", m.Value) -// av.N = &n -// return nil +// func (m *CustomIntType) MarshalDynamoDBAttributeValue() (types.AttributeValue, error) { +// return &types.AttributeValueMemberN{ +// Value: strconv.Itoa(m.Value), +// }, nil // } // type Marshaler interface { - MarshalDynamoDBAttributeValue(*types.AttributeValue) error + MarshalDynamoDBAttributeValue() (types.AttributeValue, error) } // Marshal will serialize the passed in Go value type into a DynamoDB AttributeValue @@ -158,92 +163,79 @@ type Marshaler interface { // // 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{}) (*types.AttributeValue, error) { +func Marshal(in interface{}) (types.AttributeValue, error) { return NewEncoder().Encode(in) } -// MarshalMap is an alias for Marshal func which marshals Go value -// type to a map of AttributeValues. +// MarshalMap is an alias for Marshal func which marshals Go value type to a +// map of AttributeValues. If the in parameter does not serialize to a map, an +// empty AttributeValue map will be returned. // // This is useful for DynamoDB APIs such as PutItem. func MarshalMap(in interface{}) (map[string]types.AttributeValue, error) { av, err := NewEncoder().Encode(in) - if err != nil || av == nil || av.M == nil { + + asMap, ok := av.(*types.AttributeValueMemberM) + if err != nil || av == nil || !ok { return map[string]types.AttributeValue{}, err } - return av.M, nil + return asMap.Value, nil } // MarshalList is an alias for Marshal func which marshals Go value -// type to a slice of AttributeValues. +// type to a slice of AttributeValues. If the in parameter does not serialize +// to a slice, an empty AttributeValue slice will be returned. func MarshalList(in interface{}) ([]types.AttributeValue, error) { av, err := NewEncoder().Encode(in) - if err != nil || av == nil || av.L == nil { + + asList, ok := av.(*types.AttributeValueMemberL) + if err != nil || av == nil || !ok { return []types.AttributeValue{}, err } - return av.L, nil + return asList.Value, nil } -// A MarshalOptions is a collection of options shared between marshaling +// EncoderOptions 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 - - // Support other custom struct tag keys, such as `yaml` or `toml`. +type EncoderOptions struct { + // Support other custom struct tag keys, such as `yaml`, `json`, or `toml`. // Note that values provided with a custom TagKey must also be supported // by the (un)marshalers in this package. + // + // Tag key `dynamodbav` will always be read, but if custom tag key + // conflicts with `dynamodbav` the custom tag key value will be used. TagKey string } // 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 + options EncoderOptions } // 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) +func NewEncoder(optFns ...func(*EncoderOptions)) *Encoder { + var options EncoderOptions + for _, fn := range optFns { + fn(&options) } - return e + return &Encoder{ + options: options, + } } // Encode will marshal a Go value type to an AttributeValue. Returning // the AttributeValue constructed or error. -func (e *Encoder) Encode(in interface{}) (*types.AttributeValue, error) { - av := &types.AttributeValue{} - if err := e.encode(av, reflect.ValueOf(in), tag{}); err != nil { - return nil, err - } - - return av, nil +func (e *Encoder) Encode(in interface{}) (types.AttributeValue, error) { + return e.encode(reflect.ValueOf(in), tag{}) } -func fieldByIndex(v reflect.Value, index []int, - OnEmbeddedNilStruct func(*reflect.Value) bool) reflect.Value { +func fieldByIndex( + v reflect.Value, index []int, OnEmbeddedNilStruct func(*reflect.Value) bool, +) reflect.Value { fv := v for i, x := range index { if i > 0 { @@ -259,59 +251,74 @@ func fieldByIndex(v reflect.Value, index []int, return fv } -func (e *Encoder) encode(av *types.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 +func (e *Encoder) encode(v reflect.Value, fieldTag tag) (types.AttributeValue, error) { + // Ignore fields explicitly marked to be skipped. + if fieldTag.Ignore { + return nil, nil + } + + // Zero values are serialized as null, or skipped if omitEmpty. + if isZeroValue(v) { + if fieldTag.OmitEmpty { + return nil, nil + } else if isNullableZeroValue(v) { + return encodeNull(), 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 + if av, err := tryMarshaler(v); err != nil { + return nil, err + } else if av != nil { + return av, nil } } switch v.Kind() { case reflect.Invalid: - encodeNull(av) + // TODO [ddb] Is this duplicate of `izZeroValue` above? + return encodeNull(), nil + case reflect.Struct: - return e.encodeStruct(av, v, fieldTag) + return e.encodeStruct(v, fieldTag) + case reflect.Map: - return e.encodeMap(av, v, fieldTag) + return e.encodeMap(v, fieldTag) + case reflect.Slice, reflect.Array: - return e.encodeSlice(av, v, fieldTag) + return e.encodeSlice(v, fieldTag) + case reflect.Chan, reflect.Func, reflect.UnsafePointer: - // do nothing for unsupported types + // skip unsupported types + return nil, nil + default: - return e.encodeScalar(av, v, fieldTag) + return e.encodeScalar(v, fieldTag) } - - return nil } -func (e *Encoder) encodeStruct(av *types.AttributeValue, v reflect.Value, fieldTag tag) error { - // To maintain backwards compatibility with ConvertTo family of methods which - // converted time.Time structs to strings +func (e *Encoder) encodeStruct(v reflect.Value, fieldTag tag) (types.AttributeValue, error) { + // Time structs have no public members, and instead are converted to + // RFC3339Nano formatted string, unix time seconds number if struct tag is set. if v.Type().ConvertibleTo(timeType) { var t time.Time t = v.Convert(timeType).Interface().(time.Time) if fieldTag.AsUnixTime { - return UnixTime(t).MarshalDynamoDBAttributeValue(av) + return UnixTime(t).MarshalDynamoDBAttributeValue() } - s := t.Format(time.RFC3339Nano) - av.S = &s - return nil + return &types.AttributeValueMemberS{Value: t.Format(time.RFC3339Nano)}, nil } - av.M = map[string]types.AttributeValue{} - fields := unionStructFields(v.Type(), e.MarshalOptions) + m := &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}} + fields := unionStructFields(v.Type(), structFieldOptions{ + TagKey: e.options.TagKey, + }) for _, f := range fields { if f.Name == "" { - return &InvalidMarshalError{msg: "map key cannot be empty"} + return nil, &InvalidMarshalError{msg: "map key cannot be empty"} } found := true @@ -322,215 +329,211 @@ func (e *Encoder) encodeStruct(av *types.AttributeValue, v reflect.Value, fieldT if !found { continue } - elem := types.AttributeValue{} - err := e.encode(&elem, fv, f.tag) - if err != nil { - return err - } - skip, err := keepOrOmitEmpty(f.OmitEmpty, elem, err) + + elem, err := e.encode(fv, f.tag) if err != nil { - return err - } else if skip { + return nil, err + } else if elem == nil { continue } - av.M[f.Name] = elem + m.Value[f.Name] = elem } - return nil + return m, nil } -func (e *Encoder) encodeMap(av *types.AttributeValue, v reflect.Value, fieldTag tag) error { - av.M = map[string]types.AttributeValue{} +func (e *Encoder) encodeMap(v reflect.Value, fieldTag tag) (types.AttributeValue, error) { + m := &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}} for _, key := range v.MapKeys() { keyName := fmt.Sprint(key.Interface()) if keyName == "" { - return &InvalidMarshalError{msg: "map key cannot be empty"} + return nil, &InvalidMarshalError{msg: "map key cannot be empty"} } elemVal := v.MapIndex(key) - elem := types.AttributeValue{} - err := e.encode(&elem, elemVal, tag{}) - skip, err := keepOrOmitEmpty(fieldTag.OmitEmptyElem, elem, err) + elem, err := e.encode(elemVal, tag{OmitEmpty: fieldTag.OmitEmptyElem}) if err != nil { - return err - } else if skip { + return nil, err + } else if elem == nil { continue } - av.M[keyName] = elem - } - - if v.IsNil() { - encodeNull(av) + m.Value[keyName] = elem } - return nil + return m, nil } -func (e *Encoder) encodeSlice(av *types.AttributeValue, v reflect.Value, fieldTag tag) error { - if v.Kind() == reflect.Array && v.Len() == 0 && fieldTag.OmitEmpty { - encodeNull(av) - return nil - } - - switch v.Type().Elem().Kind() { - case reflect.Uint8: - if v.Kind() == reflect.Slice && v.IsNil() { - encodeNull(av) - return nil - } - +func (e *Encoder) encodeSlice(v reflect.Value, fieldTag tag) (types.AttributeValue, error) { + if v.Type().Elem().Kind() == reflect.Uint8 { slice := reflect.MakeSlice(byteSliceType, v.Len(), v.Len()) reflect.Copy(slice, v) - b := slice.Bytes() + return &types.AttributeValueMemberB{ + Value: append([]byte{}, slice.Bytes()...), + }, nil + } - av.B = append([]byte{}, b...) - default: - var elemFn func(types.AttributeValue) error + var setElemFn func(types.AttributeValue) error + var av types.AttributeValue - if fieldTag.AsBinSet || v.Type() == byteSliceSlicetype { // Binary Set - av.BS = make([][]byte, 0, v.Len()) - elemFn = func(elem types.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 types.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 + if fieldTag.AsBinSet || v.Type() == byteSliceSliceType { // Binary Set + bs := &types.AttributeValueMemberBS{Value: make([][]byte, 0, v.Len())} + av = bs + setElemFn = func(elem types.AttributeValue) error { + b, ok := elem.(*types.AttributeValueMemberB) + if !ok || b == nil || b.Value == nil { + return &InvalidMarshalError{msg: "binary set must only contain non-nil byte slices"} } - } else if fieldTag.AsStrSet { // String Set - av.SS = make([]string, 0, v.Len()) - elemFn = func(elem types.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 + bs.Value = append(bs.Value, b.Value) + return nil + } + + } else if fieldTag.AsNumSet { // Number Set + ns := &types.AttributeValueMemberNS{Value: make([]string, 0, v.Len())} + av = ns + setElemFn = func(elem types.AttributeValue) error { + n, ok := elem.(*types.AttributeValueMemberN) + if !ok || n == nil { + return &InvalidMarshalError{msg: "number set must only contain non-nil string numbers"} } - } else { // List - av.L = make([]types.AttributeValue, 0, v.Len()) - elemFn = func(elem types.AttributeValue) error { - av.L = append(av.L, elem) - return nil + ns.Value = append(ns.Value, n.Value) + return nil + } + + } else if fieldTag.AsStrSet { // String Set + ss := &types.AttributeValueMemberSS{Value: make([]string, 0, v.Len())} + av = ss + setElemFn = func(elem types.AttributeValue) error { + s, ok := elem.(*types.AttributeValueMemberS) + if !ok || s == nil { + return &InvalidMarshalError{msg: "string set must only contain non-nil strings"} } + ss.Value = append(ss.Value, s.Value) + return nil } - if _, err := e.encodeList(v, fieldTag, elemFn); err != nil { - return err - } else if v.Kind() == reflect.Slice && v.IsNil() { - encodeNull(av) + } else { // List + l := &types.AttributeValueMemberL{Value: make([]types.AttributeValue, 0, v.Len())} + av = l + setElemFn = func(elem types.AttributeValue) error { + l.Value = append(l.Value, elem) + return nil } } - return nil + if err := e.encodeListElems(v, fieldTag, setElemFn); err != nil { + return nil, err + } + + return av, nil } -func (e *Encoder) encodeList(v reflect.Value, fieldTag tag, elemFn func(types.AttributeValue) error) (int, error) { - count := 0 +func (e *Encoder) encodeListElems(v reflect.Value, fieldTag tag, setElem func(types.AttributeValue) error) error { for i := 0; i < v.Len(); i++ { - elem := types.AttributeValue{} - err := e.encode(&elem, v.Index(i), tag{OmitEmpty: fieldTag.OmitEmptyElem}) - skip, err := keepOrOmitEmpty(fieldTag.OmitEmptyElem, elem, err) + elem, err := e.encode(v.Index(i), tag{OmitEmpty: fieldTag.OmitEmptyElem}) if err != nil { - return 0, err - } else if skip { + return err + } else if elem == nil { continue } - if err := elemFn(elem); err != nil { - return 0, err + if err := setElem(elem); err != nil { + return err } - count++ } - return count, nil + return nil } -func (e *Encoder) encodeScalar(av *types.AttributeValue, v reflect.Value, fieldTag tag) error { - if v.Type() == numberType { - s := v.String() +// Returns if the type of the value satisfies an interface for number like the +// encoding/json#Number and feature/dynamodb/attributevalue#Number +func isNumberValueType(v reflect.Value) bool { + type numberer interface { + Float64() (float64, error) + Int64() (int64, error) + String() string + } + + _, ok := v.Interface().(numberer) + return ok && v.Kind() == reflect.String +} + +func (e *Encoder) encodeScalar(v reflect.Value, fieldTag tag) (types.AttributeValue, error) { + if isNumberValueType(v) { if fieldTag.AsString { - av.S = &s - } else { - av.N = &s + return &types.AttributeValueMemberS{Value: v.String()}, nil } - return nil + return &types.AttributeValueMemberN{Value: v.String()}, nil } switch v.Kind() { case reflect.Bool: - av.BOOL = new(bool) - *av.BOOL = v.Bool() + return &types.AttributeValueMemberBOOL{Value: v.Bool()}, nil + case reflect.String: - if err := e.encodeString(av, v); err != nil { - return err - } + return e.encodeString(v) + default: // Fallback to encoding numbers, will return invalid type if not supported - if err := e.encodeNumber(av, v); err != nil { - return err + av, err := e.encodeNumber(v) + if err != nil { + return nil, err } - if fieldTag.AsString && av.NULL == nil && av.N != nil { - av.S = av.N - av.N = nil + + // TODO [ddbAV] Can this be more optimized to convert from N to S? + n, isNumber := av.(*types.AttributeValueMemberN) + if fieldTag.AsString && isNumber { + return &types.AttributeValueMemberS{Value: n.Value}, nil } + return av, nil } - - return nil } -func (e *Encoder) encodeNumber(av *types.AttributeValue, v reflect.Value) error { - if used, err := tryMarshaler(av, v); used { - return err +func (e *Encoder) encodeNumber(v reflect.Value) (types.AttributeValue, error) { + if av, err := tryMarshaler(v); err != nil { + return nil, err + } else if av != nil { + return av, nil } 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: out = encodeFloat(v.Float(), 32) + case reflect.Float64: out = encodeFloat(v.Float(), 64) + default: - return &unsupportedMarshalTypeError{Type: v.Type()} + return nil, &unsupportedMarshalTypeError{Type: v.Type()} } - av.N = &out - - return nil + return &types.AttributeValueMemberN{Value: out}, nil } -func (e *Encoder) encodeString(av *types.AttributeValue, v reflect.Value) error { - if used, err := tryMarshaler(av, v); used { - return err +func (e *Encoder) encodeString(v reflect.Value) (types.AttributeValue, error) { + if av, err := tryMarshaler(v); err != nil { + return nil, err + } else if av != nil { + return av, nil } switch v.Kind() { case reflect.String: s := v.String() - if len(s) == 0 && e.NullEmptyString { - encodeNull(av) - } else { - av.S = &s - } + return &types.AttributeValueMemberS{Value: s}, nil + default: - return &unsupportedMarshalTypeError{Type: v.Type()} + return nil, &unsupportedMarshalTypeError{Type: v.Type()} } - - return nil } func encodeInt(i int64) string { @@ -542,9 +545,8 @@ func encodeUint(u uint64) string { func encodeFloat(f float64, bitSize int) string { return strconv.FormatFloat(f, 'f', -1, bitSize) } -func encodeNull(av *types.AttributeValue) { - t := true - *av = types.AttributeValue{NULL: &t} +func encodeNull() types.AttributeValue { + return &types.AttributeValueMemberNULL{Value: true} } func valueElem(v reflect.Value) reflect.Value { @@ -558,7 +560,7 @@ func valueElem(v reflect.Value) reflect.Value { return v } -func emptyValue(v reflect.Value) bool { +func isZeroValue(v reflect.Value) bool { switch v.Kind() { case reflect.Array: return v.Len() == 0 @@ -580,23 +582,54 @@ func emptyValue(v reflect.Value) bool { return false } -func tryMarshaler(av *types.AttributeValue, v reflect.Value) (bool, error) { +func isNullableZeroValue(v reflect.Value) bool { + switch v.Kind() { + //case reflect.Array: + // return v.Len() == 0 + case reflect.Map, reflect.Slice: + return v.IsNil() + //case 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(v reflect.Value) (types.AttributeValue, error) { if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { v = v.Addr() } if v.Type().NumMethod() == 0 { - return false, nil + return nil, nil } if m, ok := v.Interface().(Marshaler); ok { - return true, m.MarshalDynamoDBAttributeValue(av) + return m.MarshalDynamoDBAttributeValue() } - return false, nil + return nil, nil } -func keepOrOmitEmpty(omitEmpty bool, av types.AttributeValue, err error) (bool, error) { +// keepOrOmitEmpty returns if the element should be skipped. Elements will +// always returned as skip if the AttributeValue interface is nil, or is is +// AttributeValueMemberNULL and omitEmpty. +// +// TODO [ddb] can this be dropped, and always skip if nil? +func keepOrOmitEmpty(omitEmpty bool, av types.AttributeValue, err error) (skip bool, _ error) { + if av == nil { + return true, nil + } + if err != nil { if _, ok := err.(*unsupportedMarshalTypeError); ok { return true, nil @@ -604,7 +637,8 @@ func keepOrOmitEmpty(omitEmpty bool, av types.AttributeValue, err error) (bool, return false, err } - if av.NULL != nil && omitEmpty { + _, isNull := av.(*types.AttributeValueMemberNULL) + if isNull && omitEmpty { return true, nil } @@ -614,50 +648,26 @@ func keepOrOmitEmpty(omitEmpty bool, av types.AttributeValue, err error) (bool, // 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 + return fmt.Sprintf("marshal failed, %s", e.msg) } +// TODO [ddb] Is this really needed? // 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" + return fmt.Sprintf("marshal failed, Go value type %s is not supported", + e.Type.String()) } diff --git a/feature/dynamodb/attributevalue/encode_test.go b/feature/dynamodb/attributevalue/encode_test.go index c9369ecb531..9eb1b909e87 100644 --- a/feature/dynamodb/attributevalue/encode_test.go +++ b/feature/dynamodb/attributevalue/encode_test.go @@ -1,18 +1,19 @@ package attributevalue import ( - "fmt" "reflect" + "strconv" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/google/go-cmp/cmp" ) func TestMarshalShared(t *testing.T) { - for i, c := range sharedTestCases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range sharedTestCases { + t.Run(name, func(t *testing.T) { av, err := Marshal(c.expected) assertConvertTest(t, av, c.in, err, c.err) }) @@ -20,8 +21,8 @@ func TestMarshalShared(t *testing.T) { } func TestMarshalListShared(t *testing.T) { - for i, c := range sharedListTestCases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range sharedListTestCases { + t.Run(name, func(t *testing.T) { av, err := MarshalList(c.expected) assertConvertTest(t, av, c.in, err, c.err) }) @@ -29,8 +30,8 @@ func TestMarshalListShared(t *testing.T) { } func TestMarshalMapShared(t *testing.T) { - for i, c := range sharedMapTestCases { - t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + for name, c := range sharedMapTestCases { + t.Run(name, func(t *testing.T) { av, err := MarshalMap(c.expected) assertConvertTest(t, av, c.in, err, c.err) }) @@ -44,15 +45,15 @@ type marshalMarshaler struct { Value4 time.Time } -func (m *marshalMarshaler) MarshalDynamoDBAttributeValue(av *types.AttributeValue) error { - av.M = map[string]types.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 (m *marshalMarshaler) MarshalDynamoDBAttributeValue() (types.AttributeValue, error) { + return &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberS{Value: m.Value}, + "def": &types.AttributeValueMemberN{Value: strconv.Itoa(m.Value2)}, + "ghi": &types.AttributeValueMemberBOOL{Value: m.Value3}, + "jkl": &types.AttributeValueMemberS{Value: m.Value4.Format(time.RFC3339Nano)}, + }, + }, nil } func TestMarshalMashaler(t *testing.T) { @@ -63,12 +64,12 @@ func TestMarshalMashaler(t *testing.T) { Value4: testDate, } - expect := &types.AttributeValue{ - M: map[string]types.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")}, + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberS{Value: "value"}, + "def": &types.AttributeValueMemberN{Value: "123"}, + "ghi": &types.AttributeValueMemberBOOL{Value: true}, + "jkl": &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, }, } @@ -91,11 +92,11 @@ type testOmitEmptyElemMapStruct struct { } func TestMarshalListOmitEmptyElem(t *testing.T) { - expect := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Values": {L: []types.AttributeValue{ - {S: aws.String("abc")}, - {S: aws.String("123")}, + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Values": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberS{Value: "123"}, }}, }, } @@ -112,11 +113,12 @@ func TestMarshalListOmitEmptyElem(t *testing.T) { } func TestMarshalMapOmitEmptyElem(t *testing.T) { - expect := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Values": {M: map[string]types.AttributeValue{ - "abc": {N: aws.String("123")}, - "klm": {S: aws.String("abc")}, + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Values": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberN{Value: "123"}, + "hij": &types.AttributeValueMemberS{Value: ""}, + "klm": &types.AttributeValueMemberS{Value: "abc"}, }}, }, } @@ -132,8 +134,8 @@ func TestMarshalMapOmitEmptyElem(t *testing.T) { if err != nil { t.Errorf("expect nil, got %v", err) } - if e, a := expect, actual; !reflect.DeepEqual(e, a) { - t.Errorf("expect %v, got %v", e, a) + if diff := cmp.Diff(expect, actual); len(diff) != 0 { + t.Errorf("expect match\n%s", diff) } } @@ -144,9 +146,9 @@ type testOmitEmptyScalar struct { } func TestMarshalOmitEmpty(t *testing.T) { - expect := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "IntPtrSetZero": {N: aws.String("0")}, + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "IntPtrSetZero": &types.AttributeValueMemberN{Value: "0"}, }, } @@ -188,14 +190,10 @@ func TestEncodeEmbeddedPointerStruct(t *testing.T) { if err != nil { t.Errorf("expect nil, got %v", err) } - expect := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Aint": { - N: aws.String("321"), - }, - "Bint": { - N: aws.String("123"), - }, + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Aint": &types.AttributeValueMemberN{Value: "321"}, + "Bint": &types.AttributeValueMemberN{Value: "123"}, }, } if e, a := expect, actual; !reflect.DeepEqual(e, a) { @@ -220,17 +218,11 @@ func TestEncodeUnixTime(t *testing.T) { if err != nil { t.Errorf("expect nil, got %v", err) } - expect := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Normal": { - S: aws.String("1970-01-01T00:02:03Z"), - }, - "Tagged": { - N: aws.String("456"), - }, - "Typed": { - N: aws.String("789"), - }, + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Normal": &types.AttributeValueMemberS{Value: "1970-01-01T00:02:03Z"}, + "Tagged": &types.AttributeValueMemberN{Value: "456"}, + "Typed": &types.AttributeValueMemberN{Value: "789"}, }, } if e, a := expect, actual; !reflect.DeepEqual(e, a) { @@ -255,14 +247,10 @@ func TestEncodeAliasedUnixTime(t *testing.T) { if err != nil { t.Errorf("expect no err, got %v", err) } - expect := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Normal": { - S: aws.String("1970-01-01T00:02:03Z"), - }, - "Tagged": { - N: aws.String("456"), - }, + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Normal": &types.AttributeValueMemberS{Value: "1970-01-01T00:02:03Z"}, + "Tagged": &types.AttributeValueMemberN{Value: "456"}, }, } if e, a := expect, actual; !reflect.DeepEqual(e, a) { diff --git a/feature/dynamodb/attributevalue/field.go b/feature/dynamodb/attributevalue/field.go index cc4ab37886e..332b59cccc4 100644 --- a/feature/dynamodb/attributevalue/field.go +++ b/feature/dynamodb/attributevalue/field.go @@ -51,7 +51,17 @@ func buildField(pIdx []int, i int, sf reflect.StructField, fieldTag tag) field { return f } -func unionStructFields(t reflect.Type, opts MarshalOptions) []field { +type structFieldOptions struct { + // Support other custom struct tag keys, such as `yaml`, `json`, or `toml`. + // Note that values provided with a custom TagKey must also be supported + // by the (un)marshalers in this package. + // + // Tag key `dynamodbav` will always be read, but if custom tag key + // conflicts with `dynamodbav` the custom tag key value will be used. + TagKey string +} + +func unionStructFields(t reflect.Type, opts structFieldOptions) []field { fields := enumFields(t, opts) sort.Sort(fieldsByName(fields)) @@ -66,7 +76,7 @@ func unionStructFields(t reflect.Type, opts MarshalOptions) []field { // // 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 { +func enumFields(t reflect.Type, opts structFieldOptions) []field { // Fields to explore current := []field{} next := []field{{Type: t}} @@ -99,12 +109,9 @@ func enumFields(t reflect.Type, opts MarshalOptions) []field { fieldTag := tag{} fieldTag.parseAVTag(sf.Tag) - // Because MarshalOptions.TagKey must be explicitly set, use it - // over JSON, which is enabled by default. + // Because TagKey must be explicitly set. if opts.TagKey != "" && fieldTag == (tag{}) { fieldTag.parseStructTag(opts.TagKey, sf.Tag) - } else if opts.SupportJSONTags && fieldTag == (tag{}) { - fieldTag.parseStructTag("json", sf.Tag) } if fieldTag.Ignore { diff --git a/feature/dynamodb/attributevalue/field_test.go b/feature/dynamodb/attributevalue/field_test.go index 5a567348a2b..70a7bdeceb8 100644 --- a/feature/dynamodb/attributevalue/field_test.go +++ b/feature/dynamodb/attributevalue/field_test.go @@ -72,7 +72,7 @@ func TestUnionStructFields(t *testing.T) { for i, c := range cases { v := reflect.ValueOf(c.in) - fields := unionStructFields(v.Type(), MarshalOptions{SupportJSONTags: true}) + fields := unionStructFields(v.Type(), structFieldOptions{TagKey: "json"}) for j, f := range fields { expected := c.expect[j] if e, a := expected.Name, f.Name; e != a { diff --git a/feature/dynamodb/attributevalue/go.mod b/feature/dynamodb/attributevalue/go.mod index 7ded3fa0eee..a076e9cc375 100644 --- a/feature/dynamodb/attributevalue/go.mod +++ b/feature/dynamodb/attributevalue/go.mod @@ -3,10 +3,17 @@ module github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue go 1.15 require ( - github.com/aws/aws-sdk-go v1.35.28 - github.com/aws/aws-sdk-go-v2 v0.29.1-0.20201115205015-a82264590e72 - github.com/aws/aws-sdk-go-v2/service/dynamodb v0.29.1-0.20201115205015-a82264590e72 - github.com/google/go-cmp v0.5.3 + github.com/aws/aws-sdk-go v1.35.37 + github.com/aws/aws-sdk-go-v2 v0.30.0 + github.com/aws/aws-sdk-go-v2/service/dynamodb v0.30.0 + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v0.30.0 + github.com/google/go-cmp v0.5.4 ) -replace github.com/aws/aws-sdk-go-v2/service/dynamodb => ../../../service/dynamodb +replace github.com/aws/aws-sdk-go-v2/service/dynamodb => ../../../service/dynamodb/ + +replace github.com/aws/aws-sdk-go-v2 => ../../../ + +replace github.com/aws/aws-sdk-go-v2/service/dynamodbstreams => ../../../service/dynamodbstreams/ + +replace github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding => ../../../service/internal/accept-encoding/ diff --git a/feature/dynamodb/attributevalue/go.sum b/feature/dynamodb/attributevalue/go.sum index db96cdd12de..f6b35c2744f 100644 --- a/feature/dynamodb/attributevalue/go.sum +++ b/feature/dynamodb/attributevalue/go.sum @@ -1,24 +1,35 @@ github.com/aws/aws-sdk-go v1.35.28 h1:S2LuRnfC8X05zgZLC8gy/Sb82TGv2Cpytzbzz7tkeHc= github.com/aws/aws-sdk-go v1.35.28/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= +github.com/aws/aws-sdk-go v1.35.37 h1:XA71k5PofXJ/eeXdWrTQiuWPEEyq8liguR+Y/QUELhI= +github.com/aws/aws-sdk-go v1.35.37/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.29.1-0.20201113222241-726e4a15683d h1:93Jm1o822AjAxUxa1iInxkJpn3yyWV7MD4+2nq85Zu8= github.com/aws/aws-sdk-go-v2 v0.29.1-0.20201113222241-726e4a15683d/go.mod h1:sfvlyefY54joy/2ohHP8LRSXhkE7A/Cw1uU8ypOMdj0= github.com/aws/aws-sdk-go-v2 v0.29.1-0.20201115205015-a82264590e72 h1:u/GixDbJUDfhxfSy7ytlbdtSmWUQ3qIdYEZhCVmlnsc= github.com/aws/aws-sdk-go-v2 v0.29.1-0.20201115205015-a82264590e72/go.mod h1:sfvlyefY54joy/2ohHP8LRSXhkE7A/Cw1uU8ypOMdj0= +github.com/aws/aws-sdk-go-v2 v0.30.0 h1:/CjXUnWXnvdgOqHa65UIs2TODa5D5lm3ty7O0wWuYHY= +github.com/aws/aws-sdk-go-v2 v0.30.0/go.mod h1:vEDjzdktTH+FoEOV6BWgYLEzvPti13Onp5/qQrSiLPg= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v0.30.0 h1:HyTCk6LXC0DgbkqePqSNWZknKsgXaG3r2A1ESphibo0= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v0.30.0/go.mod h1:fVJvwprTXAdghnOlJNWneke0LxVOgRONRPdZQG+1uos= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.0 h1:OGNwNNeQvOZsa+zAK5nE7r6e0serfSAFznoXqbvbzFE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.0/go.mod h1:bMiNrEKNefchodwRJnuwaiAZj2NJq8ZHAYASve6mbFs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1-0.20201113222241-726e4a15683d h1:cnCSbwLs1cqUcDaK6uJ03wFeKnfzB9URzvP7gkBq4to= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1-0.20201113222241-726e4a15683d/go.mod h1:8ssQ+eALAh5+Z5uix7Ku/rzM1uDVNQrAQx7cNiq1Rwo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v0.3.1/go.mod h1:NPTV0PtdITqVGH/e7Xo1Z0TnnKCn2pIH+ZT23dl5Ut0= github.com/awslabs/smithy-go v0.2.1/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= github.com/awslabs/smithy-go v0.3.0/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= github.com/awslabs/smithy-go v0.3.1-0.20201104233911-38864709e183/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4 h1:Aj5dOF+lDoEhU92no7YZF0IokuWGjiNrcm/DGIG3iII= github.com/awslabs/smithy-go v0.3.1-0.20201108010311-62c2a93810b4/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.4.0 h1:El0KyKn4zdM3pLuWJlgoeitQuu/mjwUPssr7L3xu3vs= +github.com/awslabs/smithy-go v0.4.0/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -28,9 +39,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/feature/dynamodb/attributevalue/marshaler_examples_test.go b/feature/dynamodb/attributevalue/marshaler_examples_test.go index ff458d226e1..94fbd5a60b5 100644 --- a/feature/dynamodb/attributevalue/marshaler_examples_test.go +++ b/feature/dynamodb/attributevalue/marshaler_examples_test.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go/aws/awsutil" @@ -25,44 +24,31 @@ func ExampleMarshal() { Numbers: []int{1, 2, 3}, } av, err := attributevalue.Marshal(r) + m := av.(*types.AttributeValueMemberM) fmt.Println("err", err) - fmt.Println("Bytes", awsutil.Prettify(av.M["Bytes"])) - fmt.Println("MyField", awsutil.Prettify(av.M["MyField"])) - fmt.Println("Letters", awsutil.Prettify(av.M["Letters"])) - fmt.Println("Numbers", awsutil.Prettify(av.M["Numbers"])) + fmt.Println("Bytes", awsutil.Prettify(m.Value["Bytes"])) + fmt.Println("MyField", awsutil.Prettify(m.Value["MyField"])) + fmt.Println("Letters", awsutil.Prettify(m.Value["Letters"])) + fmt.Println("Numbers", awsutil.Prettify(m.Value["Numbers"])) // Output: // err // Bytes { - // B: len 2 + // Value: len 2 // } // MyField { - // S: "MyFieldValue" + // Value: "MyFieldValue" // } // Letters { - // L: [ - // { - // S: "a" - // }, - // { - // S: "b" - // }, - // { - // S: "c" - // }, - // { - // S: "d" - // } + // Value: [ + // &{a}, + // &{b}, + // &{c}, + // &{d} // ] // } // Numbers { - // L: [{ - // N: "1" - // },{ - // N: "2" - // },{ - // N: "3" - // }] + // Value: [&{1},&{2},&{3}] // } } @@ -81,17 +67,20 @@ func ExampleUnmarshal() { A2Num: map[string]int{"a": 1, "b": 2, "c": 3}, } - av := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Bytes": {B: []byte{48, 49}}, - "MyField": {S: aws.String("MyFieldValue")}, - "Letters": {L: []types.AttributeValue{ - {S: aws.String("a")}, {S: aws.String("b")}, {S: aws.String("c")}, {S: aws.String("d")}, + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Bytes": &types.AttributeValueMemberB{Value: []byte{48, 49}}, + "MyField": &types.AttributeValueMemberS{Value: "MyFieldValue"}, + "Letters": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "a"}, + &types.AttributeValueMemberS{Value: "b"}, + &types.AttributeValueMemberS{Value: "c"}, + &types.AttributeValueMemberS{Value: "d"}, }}, - "A2Num": {M: map[string]types.AttributeValue{ - "a": {N: aws.String("1")}, - "b": {N: aws.String("2")}, - "c": {N: aws.String("3")}, + "A2Num": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "a": &types.AttributeValueMemberN{Value: "1"}, + "b": &types.AttributeValueMemberN{Value: "2"}, + "c": &types.AttributeValueMemberN{Value: "3"}, }}, }, } diff --git a/feature/dynamodb/attributevalue/marshaler_test.go b/feature/dynamodb/attributevalue/marshaler_test.go index d29bb09e7b1..801e379f2d1 100644 --- a/feature/dynamodb/attributevalue/marshaler_test.go +++ b/feature/dynamodb/attributevalue/marshaler_test.go @@ -5,20 +5,20 @@ import ( "reflect" "testing" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/google/go-cmp/cmp" ) type simpleMarshalStruct struct { - Byte []byte - String string - Int int - Uint uint - Float32 float32 - Float64 float64 - Bool bool - Null *interface{} + Byte []byte + String string + PtrString *string + Int int + Uint uint + Float32 float32 + Float64 float64 + Bool bool + Null *interface{} } type complexMarshalStruct struct { @@ -42,210 +42,209 @@ type marshallerTestInput struct { var trueValue = true var falseValue = false -var marshalerScalarInputs = []marshallerTestInput{ - { +var marshalerScalarInputs = map[string]marshallerTestInput{ + "nil": { input: nil, - expected: &types.AttributeValue{NULL: &trueValue}, + expected: &types.AttributeValueMemberNULL{Value: true}, }, - { + "string": { input: "some string", - expected: &types.AttributeValue{S: aws.String("some string")}, + expected: &types.AttributeValueMemberS{Value: "some string"}, }, - { + "bool": { input: true, - expected: &types.AttributeValue{BOOL: &trueValue}, + expected: &types.AttributeValueMemberBOOL{Value: true}, }, - { + "bool false": { input: false, - expected: &types.AttributeValue{BOOL: &falseValue}, + expected: &types.AttributeValueMemberBOOL{Value: false}, }, - { + "float": { input: 3.14, - expected: &types.AttributeValue{N: aws.String("3.14")}, + expected: &types.AttributeValueMemberN{Value: "3.14"}, }, - { + "max float32": { input: math.MaxFloat32, - expected: &types.AttributeValue{N: aws.String("340282346638528860000000000000000000000")}, + expected: &types.AttributeValueMemberN{Value: "340282346638528860000000000000000000000"}, }, - { + "max float64": { input: math.MaxFloat64, - expected: &types.AttributeValue{N: aws.String("179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")}, + expected: &types.AttributeValueMemberN{Value: "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, }, - { + "integer": { input: 12, - expected: &types.AttributeValue{N: aws.String("12")}, + expected: &types.AttributeValueMemberN{Value: "12"}, }, - { + "number integer": { input: Number("12"), - expected: &types.AttributeValue{N: aws.String("12")}, + expected: &types.AttributeValueMemberN{Value: "12"}, }, - { + "zero values": { input: simpleMarshalStruct{}, - expected: &types.AttributeValue{ - M: map[string]types.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")}, + expected: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Byte": &types.AttributeValueMemberNULL{Value: true}, + "Bool": &types.AttributeValueMemberBOOL{Value: false}, + "Float32": &types.AttributeValueMemberN{Value: "0"}, + "Float64": &types.AttributeValueMemberN{Value: "0"}, + "Int": &types.AttributeValueMemberN{Value: "0"}, + "Null": &types.AttributeValueMemberNULL{Value: true}, + "String": &types.AttributeValueMemberS{Value: ""}, + "PtrString": &types.AttributeValueMemberNULL{Value: true}, + "Uint": &types.AttributeValueMemberN{Value: "0"}, }, }, }, } -var marshallerMapTestInputs = []marshallerTestInput{ +var marshallerMapTestInputs = map[string]marshallerTestInput{ // Scalar tests - { + "nil": { input: nil, expected: map[string]types.AttributeValue{}, }, - { + "string": { input: map[string]interface{}{"string": "some string"}, - expected: map[string]types.AttributeValue{"string": {S: aws.String("some string")}}, + expected: map[string]types.AttributeValue{"string": &types.AttributeValueMemberS{Value: "some string"}}, }, - { + "bool": { input: map[string]interface{}{"bool": true}, - expected: map[string]types.AttributeValue{"bool": {BOOL: &trueValue}}, + expected: map[string]types.AttributeValue{"bool": &types.AttributeValueMemberBOOL{Value: true}}, }, - { + "bool false": { input: map[string]interface{}{"bool": false}, - expected: map[string]types.AttributeValue{"bool": {BOOL: &falseValue}}, + expected: map[string]types.AttributeValue{"bool": &types.AttributeValueMemberBOOL{Value: false}}, }, - { + "null": { input: map[string]interface{}{"null": nil}, - expected: map[string]types.AttributeValue{"null": {NULL: &trueValue}}, + expected: map[string]types.AttributeValue{"null": &types.AttributeValueMemberNULL{Value: true}}, }, - { + "float": { input: map[string]interface{}{"float": 3.14}, - expected: map[string]types.AttributeValue{"float": {N: aws.String("3.14")}}, + expected: map[string]types.AttributeValue{"float": &types.AttributeValueMemberN{Value: "3.14"}}, }, - { + "float32": { input: map[string]interface{}{"float": math.MaxFloat32}, - expected: map[string]types.AttributeValue{"float": {N: aws.String("340282346638528860000000000000000000000")}}, + expected: map[string]types.AttributeValue{"float": &types.AttributeValueMemberN{Value: "340282346638528860000000000000000000000"}}, }, - { + "float64": { input: map[string]interface{}{"float": math.MaxFloat64}, - expected: map[string]types.AttributeValue{"float": {N: aws.String("179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")}}, + expected: map[string]types.AttributeValue{"float": &types.AttributeValueMemberN{Value: "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}, }, - { + "decimal number": { input: map[string]interface{}{"num": 12.}, - expected: map[string]types.AttributeValue{"num": {N: aws.String("12")}}, + expected: map[string]types.AttributeValue{"num": &types.AttributeValueMemberN{Value: "12"}}, }, - { + "byte": { input: map[string]interface{}{"byte": []byte{48, 49}}, - expected: map[string]types.AttributeValue{"byte": {B: []byte{48, 49}}}, + expected: map[string]types.AttributeValue{"byte": &types.AttributeValueMemberB{Value: []byte{48, 49}}}, }, - { + "nested blob": { input: struct{ Byte []byte }{Byte: []byte{48, 49}}, - expected: map[string]types.AttributeValue{"Byte": {B: []byte{48, 49}}}, + expected: map[string]types.AttributeValue{"Byte": &types.AttributeValueMemberB{Value: []byte{48, 49}}}, }, - { + "map nested blob": { input: map[string]interface{}{"byte_set": [][]byte{{48, 49}, {50, 51}}}, - expected: map[string]types.AttributeValue{"byte_set": {BS: [][]byte{{48, 49}, {50, 51}}}}, + expected: map[string]types.AttributeValue{"byte_set": &types.AttributeValueMemberBS{Value: [][]byte{{48, 49}, {50, 51}}}}, }, - { + "bytes set": { input: struct{ ByteSet [][]byte }{ByteSet: [][]byte{{48, 49}, {50, 51}}}, - expected: map[string]types.AttributeValue{"ByteSet": {BS: [][]byte{{48, 49}, {50, 51}}}}, + expected: map[string]types.AttributeValue{"ByteSet": &types.AttributeValueMemberBS{Value: [][]byte{{48, 49}, {50, 51}}}}, }, - // List - { + "list": { input: map[string]interface{}{"list": []interface{}{"a string", 12., 3.14, true, nil, false}}, expected: map[string]types.AttributeValue{ - "list": { - L: []types.AttributeValue{ - {S: aws.String("a string")}, - {N: aws.String("12")}, - {N: aws.String("3.14")}, - {BOOL: &trueValue}, - {NULL: &trueValue}, - {BOOL: &falseValue}, + "list": &types.AttributeValueMemberL{ + Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "a string"}, + &types.AttributeValueMemberN{Value: "12"}, + &types.AttributeValueMemberN{Value: "3.14"}, + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberNULL{Value: true}, + &types.AttributeValueMemberBOOL{Value: false}, }, }, }, }, - // Map - { + "map": { input: map[string]interface{}{"map": map[string]interface{}{"nestednum": 12.}}, expected: map[string]types.AttributeValue{ - "map": { - M: map[string]types.AttributeValue{ - "nestednum": { - N: aws.String("12"), - }, + "map": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "nestednum": &types.AttributeValueMemberN{Value: "12"}, }, }, }, }, - // Structs - { + "struct": { input: simpleMarshalStruct{}, expected: map[string]types.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")}, + "Byte": &types.AttributeValueMemberNULL{Value: true}, + "Bool": &types.AttributeValueMemberBOOL{Value: false}, + "Float32": &types.AttributeValueMemberN{Value: "0"}, + "Float64": &types.AttributeValueMemberN{Value: "0"}, + "Int": &types.AttributeValueMemberN{Value: "0"}, + "Null": &types.AttributeValueMemberNULL{Value: true}, + "String": &types.AttributeValueMemberS{Value: ""}, + "PtrString": &types.AttributeValueMemberNULL{Value: true}, + "Uint": &types.AttributeValueMemberN{Value: "0"}, }, }, - { + "nested struct": { input: complexMarshalStruct{}, expected: map[string]types.AttributeValue{ - "Simple": {NULL: &trueValue}, + "Simple": &types.AttributeValueMemberNULL{Value: true}, }, }, - { + "nested nil slice": { input: struct { - Simple []string `json:"simple"` + Simple []string `dynamodbav:"simple"` }{}, expected: map[string]types.AttributeValue{ - "simple": {NULL: &trueValue}, + "simple": &types.AttributeValueMemberNULL{Value: true}, }, }, - { + "nested nil slice omit empty": { input: struct { - Simple []string `json:"simple,omitempty"` + Simple []string `dynamodbav:"simple,omitempty"` }{}, expected: map[string]types.AttributeValue{}, }, - { + "nested ignored field": { input: struct { - Simple []string `json:"-"` + Simple []string `dynamodbav:"-"` }{}, expected: map[string]types.AttributeValue{}, }, - { + "complex struct members with zero": { input: complexMarshalStruct{Simple: []simpleMarshalStruct{{Int: -2}, {Uint: 5}}}, expected: map[string]types.AttributeValue{ - "Simple": { - L: []types.AttributeValue{ - { - M: map[string]types.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")}, + "Simple": &types.AttributeValueMemberL{ + Value: []types.AttributeValue{ + &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Byte": &types.AttributeValueMemberNULL{Value: true}, + "Bool": &types.AttributeValueMemberBOOL{Value: false}, + "Float32": &types.AttributeValueMemberN{Value: "0"}, + "Float64": &types.AttributeValueMemberN{Value: "0"}, + "Int": &types.AttributeValueMemberN{Value: "-2"}, + "Null": &types.AttributeValueMemberNULL{Value: true}, + "String": &types.AttributeValueMemberS{Value: ""}, + "PtrString": &types.AttributeValueMemberNULL{Value: true}, + "Uint": &types.AttributeValueMemberN{Value: "0"}, }, }, - { - M: map[string]types.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")}, + &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Byte": &types.AttributeValueMemberNULL{Value: true}, + "Bool": &types.AttributeValueMemberBOOL{Value: false}, + "Float32": &types.AttributeValueMemberN{Value: "0"}, + "Float64": &types.AttributeValueMemberN{Value: "0"}, + "Int": &types.AttributeValueMemberN{Value: "0"}, + "Null": &types.AttributeValueMemberNULL{Value: true}, + "String": &types.AttributeValueMemberS{Value: ""}, + "PtrString": &types.AttributeValueMemberNULL{Value: true}, + "Uint": &types.AttributeValueMemberN{Value: "5"}, }, }, }, @@ -254,43 +253,44 @@ var marshallerMapTestInputs = []marshallerTestInput{ }, } -var marshallerListTestInputs = []marshallerTestInput{ - { +var marshallerListTestInputs = map[string]marshallerTestInput{ + "nil": { input: nil, expected: []types.AttributeValue{}, }, - { + "empty interface": { input: []interface{}{}, expected: []types.AttributeValue{}, }, - { + "empty struct": { input: []simpleMarshalStruct{}, expected: []types.AttributeValue{}, }, - { + "various types": { input: []interface{}{"a string", 12., 3.14, true, nil, false}, expected: []types.AttributeValue{ - {S: aws.String("a string")}, - {N: aws.String("12")}, - {N: aws.String("3.14")}, - {BOOL: &trueValue}, - {NULL: &trueValue}, - {BOOL: &falseValue}, + &types.AttributeValueMemberS{Value: "a string"}, + &types.AttributeValueMemberN{Value: "12"}, + &types.AttributeValueMemberN{Value: "3.14"}, + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberNULL{Value: true}, + &types.AttributeValueMemberBOOL{Value: false}, }, }, - { + "nested zero values": { input: []simpleMarshalStruct{{}}, expected: []types.AttributeValue{ - { - M: map[string]types.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")}, + &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Byte": &types.AttributeValueMemberNULL{Value: true}, + "Bool": &types.AttributeValueMemberBOOL{Value: false}, + "Float32": &types.AttributeValueMemberN{Value: "0"}, + "Float64": &types.AttributeValueMemberN{Value: "0"}, + "Int": &types.AttributeValueMemberN{Value: "0"}, + "Null": &types.AttributeValueMemberNULL{Value: true}, + "String": &types.AttributeValueMemberS{Value: ""}, + "PtrString": &types.AttributeValueMemberNULL{Value: true}, + "Uint": &types.AttributeValueMemberN{Value: "0"}, }, }, }, @@ -298,38 +298,43 @@ var marshallerListTestInputs = []marshallerTestInput{ } func Test_New_Marshal(t *testing.T) { - for _, test := range marshalerScalarInputs { - testMarshal(t, test) + for name, test := range marshalerScalarInputs { + t.Run(name, func(t *testing.T) { + actual, err := Marshal(test.input) + if test.err != nil { + if err == nil { + t.Errorf("Marshal with input %#v returned %#v, expected error `%s`", + test.input, actual, test.err) + } else if err.Error() != test.err.Error() { + t.Errorf("Marshal with input %#v returned error `%s`, expected error `%s`", + test.input, err, test.err) + } + } else { + if err != nil { + t.Errorf("Marshal with input %#v returned error `%s`", test.input, err) + } + compareObjects(t, test.expected, actual) + } + }) } } 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.(*types.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()) + for name, test := range marshalerScalarInputs { + t.Run(name, func(t *testing.T) { + if test.input == nil { + t.Skip() + } + actual := reflect.New(reflect.TypeOf(test.input)).Interface() + if err := Unmarshal(test.expected.(types.AttributeValue), actual); err != nil { + t.Errorf("Unmarshal with input %#v returned error `%s`", test.expected, err) + } + compareObjects(t, test.input, reflect.ValueOf(actual).Elem().Interface()) + }) } } @@ -362,38 +367,40 @@ func Test_New_UnmarshalError(t *testing.T) { } 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) + for name, test := range marshallerMapTestInputs { + t.Run(name, func(t *testing.T) { + actual, err := MarshalMap(test.input) + if test.err != nil { + if err == nil { + t.Errorf("MarshalMap with input %#v returned %#v, expected error `%s`", + test.input, actual, test.err) + } else if err.Error() != test.err.Error() { + t.Errorf("MarshalMap with input %#v returned error `%s`, expected error `%s`", + test.input, err, test.err) + } + } else { + if err != nil { + t.Errorf("MarshalMap with input %#v returned 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]types.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()) + for name, test := range marshallerMapTestInputs { + t.Run(name, func(t *testing.T) { + if test.input == nil { + t.Skip() + } + actual := reflect.New(reflect.TypeOf(test.input)).Interface() + if err := UnmarshalMap(test.expected.(map[string]types.AttributeValue), actual); err != nil { + t.Errorf("Unmarshal with input %#v returned error `%s`", test.expected, err) + } + compareObjects(t, test.input, reflect.ValueOf(actual).Elem().Interface()) + }) } } @@ -426,44 +433,48 @@ func Test_New_UnmarshalMapError(t *testing.T) { } 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) + for name, c := range marshallerListTestInputs { + t.Run(name, func(t *testing.T) { + actual, err := MarshalList(c.input) + if c.err != nil { + if err == nil { + t.Errorf("marshalList with input %#v returned %#v, expected error `%s`", + c.input, actual, c.err) + } else if err.Error() != c.err.Error() { + t.Errorf("marshalList with input %#v returned error `%s`, expected error `%s`", + c.input, err, c.err) + } + return + } else { + if err != nil { + t.Errorf("MarshalList with input %#v returned error `%s`", c.input, err) + } + compareObjects(t, c.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.([]types.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()) + for name, c := range marshallerListTestInputs { + t.Run(name, func(t *testing.T) { + if c.input == nil { + t.Skip() + } + + iv := reflect.ValueOf(c.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(c.expected.([]types.AttributeValue), actual.Interface()); err != nil { + t.Errorf("unmarshal with input %#v returned error `%s`", c.expected, err) + } + compareObjects(t, c.input, actual.Elem().Interface()) + }) } } @@ -496,6 +507,7 @@ func Test_New_UnmarshalListError(t *testing.T) { } func compareObjects(t *testing.T, expected interface{}, actual interface{}) { + t.Helper() if !reflect.DeepEqual(expected, actual) { ev := reflect.ValueOf(expected) av := reflect.ValueOf(actual) @@ -549,38 +561,38 @@ func Test_Encode_YAML_TagKey(t *testing.T) { NoTag: "NoTag", } - expected := &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "string": {S: aws.String("String")}, - "empty": {NULL: &trueValue}, - "byte": {NULL: &trueValue}, - "float32": {N: aws.String("0")}, - "float64": {N: aws.String("0")}, - "int": {N: aws.String("0")}, - "uint": {N: aws.String("0")}, - "slice": { - L: []types.AttributeValue{ - {S: aws.String("one")}, - {S: aws.String("two")}, + expected := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "string": &types.AttributeValueMemberS{Value: "String"}, + "empty": &types.AttributeValueMemberS{Value: ""}, + "byte": &types.AttributeValueMemberNULL{Value: true}, + "float32": &types.AttributeValueMemberN{Value: "0"}, + "float64": &types.AttributeValueMemberN{Value: "0"}, + "int": &types.AttributeValueMemberN{Value: "0"}, + "uint": &types.AttributeValueMemberN{Value: "0"}, + "slice": &types.AttributeValueMemberL{ + Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "one"}, + &types.AttributeValueMemberS{Value: "two"}, }, }, - "map": { - M: map[string]types.AttributeValue{ - "one": {N: aws.String("1")}, - "two": {N: aws.String("2")}, + "map": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "one": &types.AttributeValueMemberN{Value: "1"}, + "two": &types.AttributeValueMemberN{Value: "2"}, }, }, - "NoTag": {S: aws.String("NoTag")}, + "NoTag": &types.AttributeValueMemberS{Value: "NoTag"}, }, } - enc := NewEncoder(func(e *Encoder) { - e.TagKey = "yaml" + enc := NewEncoder(func(o *EncoderOptions) { + o.TagKey = "yaml" }) actual, err := enc.Encode(input) if err != nil { - t.Errorf("Encode with input %#v retured error `%s`, expected nil", input, err) + t.Errorf("Encode with input %#v returned error `%s`, expected nil", input, err) } compareObjects(t, expected, actual) diff --git a/feature/dynamodb/attributevalue/shared_test.go b/feature/dynamodb/attributevalue/shared_test.go index fb7f462ca89..c9d6b7cc318 100644 --- a/feature/dynamodb/attributevalue/shared_test.go +++ b/feature/dynamodb/attributevalue/shared_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/google/go-cmp/cmp" ) type testBinarySetStruct struct { @@ -66,31 +67,26 @@ type testNamedPointer *int var testDate, _ = time.Parse(time.RFC3339, "2016-05-03T17:06:26.209072Z") -var sharedTestCases = []struct { - in *types.AttributeValue +var sharedTestCases = map[string]struct { + in types.AttributeValue actual, expected interface{} err error }{ - { // Binary slice - in: &types.AttributeValue{B: []byte{48, 49}}, + "binary slice": { + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, actual: &[]byte{}, expected: []byte{48, 49}, }, - { // Binary slice - in: &types.AttributeValue{B: []byte{48, 49}}, - actual: &[]byte{}, - expected: []byte{48, 49}, - }, - { // Binary slice oversized - in: &types.AttributeValue{B: []byte{48, 49}}, + "Binary slice oversized": { + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, actual: func() *[]byte { v := make([]byte, 0, 10) return &v }(), expected: []byte{48, 49}, }, - { // Binary slice pointer - in: &types.AttributeValue{B: []byte{48, 49}}, + "binary slice pointer": { + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, actual: func() **[]byte { v := make([]byte, 0, 10) v2 := &v @@ -98,35 +94,35 @@ var sharedTestCases = []struct { }(), expected: []byte{48, 49}, }, - { // Bool - in: &types.AttributeValue{BOOL: aws.Bool(true)}, + "bool": { + in: &types.AttributeValueMemberBOOL{Value: true}, actual: new(bool), expected: true, }, - { // List - in: &types.AttributeValue{L: []types.AttributeValue{ - {N: aws.String("123")}, + "list": { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "123"}, }}, actual: &[]int{}, expected: []int{123}, }, - { // Map, interface - in: &types.AttributeValue{M: map[string]types.AttributeValue{ - "abc": {N: aws.String("123")}, + "map, interface": { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberN{Value: "123"}, }}, actual: &map[string]int{}, expected: map[string]int{"abc": 123}, }, - { // Map, struct - in: &types.AttributeValue{M: map[string]types.AttributeValue{ - "Abc": {N: aws.String("123")}, + "map, struct": { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "Abc": &types.AttributeValueMemberN{Value: "123"}, }}, actual: &struct{ Abc int }{}, expected: struct{ Abc int }{Abc: 123}, }, - { // Map, struct - in: &types.AttributeValue{M: map[string]types.AttributeValue{ - "abc": {N: aws.String("123")}, + "map, struct with tags": { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberN{Value: "123"}, }}, actual: &struct { Abc int `json:"abc" dynamodbav:"abc"` @@ -135,133 +131,131 @@ var sharedTestCases = []struct { Abc int `json:"abc" dynamodbav:"abc"` }{Abc: 123}, }, - { // Number, int - in: &types.AttributeValue{N: aws.String("123")}, + "number, int": { + in: &types.AttributeValueMemberN{Value: "123"}, actual: new(int), expected: 123, }, - { // Number, Float - in: &types.AttributeValue{N: aws.String("123.1")}, + "number, Float": { + in: &types.AttributeValueMemberN{Value: "123.1"}, actual: new(float64), expected: float64(123.1), }, - { // Null - in: &types.AttributeValue{NULL: aws.Bool(true)}, - actual: new(string), - expected: "", - }, - { // Null ptr - in: &types.AttributeValue{NULL: aws.Bool(true)}, + "null ptr": { + in: &types.AttributeValueMemberNULL{Value: true}, actual: new(*string), expected: nil, }, - { // String - in: &types.AttributeValue{S: aws.String("abc")}, + "string": { + in: &types.AttributeValueMemberS{Value: "abc"}, actual: new(string), expected: "abc", }, - { // Binary Set - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Binarys": {BS: [][]byte{{48, 49}, {50, 51}}}, + "empty string": { + in: &types.AttributeValueMemberS{Value: ""}, + actual: new(string), + expected: "", + }, + "binary Set": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Binarys": &types.AttributeValueMemberBS{Value: [][]byte{{48, 49}, {50, 51}}}, }, }, actual: &testBinarySetStruct{}, expected: testBinarySetStruct{Binarys: [][]byte{{48, 49}, {50, 51}}}, }, - { // Number Set - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Numbers": {NS: []string{"123", "321"}}, + "number Set": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Numbers": &types.AttributeValueMemberNS{Value: []string{"123", "321"}}, }, }, actual: &testNumberSetStruct{}, expected: testNumberSetStruct{Numbers: []int{123, 321}}, }, - { // String Set - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Strings": {SS: []string{"abc", "efg"}}, + "string Set": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Strings": &types.AttributeValueMemberSS{Value: []string{"abc", "efg"}}, }, }, actual: &testStringSetStruct{}, expected: testStringSetStruct{Strings: []string{"abc", "efg"}}, }, - { // Int value as string - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Value": {S: aws.String("123")}, + "int value as string": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Value": &types.AttributeValueMemberS{Value: "123"}, }, }, actual: &testIntAsStringStruct{}, expected: testIntAsStringStruct{Value: 123}, }, - { // Omitempty - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Value3": {N: aws.String("0")}, + "omitempty": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Value3": &types.AttributeValueMemberN{Value: "0"}, }, }, actual: &testOmitEmptyStruct{}, expected: testOmitEmptyStruct{Value: "", Value2: nil, Value3: 0}, }, - { // aliased type - in: &types.AttributeValue{ - M: map[string]types.AttributeValue{ - "Value": {S: aws.String("123")}, - "Value2": {N: aws.String("123")}, - "Value3": {M: map[string]types.AttributeValue{ - "Key": {N: aws.String("321")}, + "aliased type": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Value": &types.AttributeValueMemberS{Value: "123"}, + "Value2": &types.AttributeValueMemberN{Value: "123"}, + "Value3": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "Key": &types.AttributeValueMemberN{Value: "321"}, }}, - "Value4": {L: []types.AttributeValue{ - {S: aws.String("1")}, - {S: aws.String("2")}, - {S: aws.String("3")}, + "Value4": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "3"}, }}, - "Value5": {B: []byte{0, 1, 2}}, - "Value6": {L: []types.AttributeValue{ - {N: aws.String("1")}, - {N: aws.String("2")}, - {N: aws.String("3")}, + "Value5": &types.AttributeValueMemberB{Value: []byte{0, 1, 2}}, + "Value6": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, }}, - "Value7": {L: []types.AttributeValue{ - {S: aws.String("1")}, - {S: aws.String("2")}, - {S: aws.String("3")}, + "Value7": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "3"}, }}, - "Value8": {BS: [][]byte{ + "Value8": &types.AttributeValueMemberBS{Value: [][]byte{ {0, 1, 2}, {3, 4, 5}, }}, - "Value9": {NS: []string{ + "Value9": &types.AttributeValueMemberNS{Value: []string{ "1", "2", "3", }}, - "Value10": {SS: []string{ + "Value10": &types.AttributeValueMemberSS{Value: []string{ "1", "2", "3", }}, - "Value11": {L: []types.AttributeValue{ - {N: aws.String("1")}, - {N: aws.String("2")}, - {N: aws.String("3")}, + "Value11": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, }}, - "Value12": {L: []types.AttributeValue{ - {S: aws.String("1")}, - {S: aws.String("2")}, - {S: aws.String("3")}, + "Value12": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "3"}, }}, - "Value13": {BOOL: aws.Bool(true)}, - "Value14": {L: []types.AttributeValue{ - {BOOL: aws.Bool(true)}, - {BOOL: aws.Bool(false)}, - {BOOL: aws.Bool(true)}, + "Value13": &types.AttributeValueMemberBOOL{Value: true}, + "Value14": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberBOOL{Value: false}, + &types.AttributeValueMemberBOOL{Value: true}, }}, - "Value15": {M: map[string]types.AttributeValue{ - "TestKey": { - S: aws.String("TestElement"), - }, + "Value15": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "TestKey": &types.AttributeValueMemberS{Value: "TestElement"}, }}, }, }, @@ -288,27 +282,27 @@ var sharedTestCases = []struct { Value15: map[testAliasedString]string{"TestKey": "TestElement"}, }, }, - { - in: &types.AttributeValue{N: aws.String("123")}, + "number named pointer": { + in: &types.AttributeValueMemberN{Value: "123"}, actual: new(testNamedPointer), expected: testNamedPointer(aws.Int(123)), }, - { // time.Time - in: &types.AttributeValue{S: aws.String("2016-05-03T17:06:26.209072Z")}, + "time.Time": { + in: &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, actual: new(time.Time), expected: testDate, }, - { // time.Time List - in: &types.AttributeValue{L: []types.AttributeValue{ - {S: aws.String("2016-05-03T17:06:26.209072Z")}, - {S: aws.String("2016-05-04T17:06:26.209072Z")}, + "time.Time List": { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, + &types.AttributeValueMemberS{Value: "2016-05-04T17:06:26.209072Z"}, }}, actual: new([]time.Time), expected: []time.Time{testDate, testDate.Add(24 * time.Hour)}, }, - { // time.Time struct - in: &types.AttributeValue{M: map[string]types.AttributeValue{ - "abc": {S: aws.String("2016-05-03T17:06:26.209072Z")}, + "time.Time struct": { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, }}, actual: &struct { Abc time.Time `json:"abc" dynamodbav:"abc"` @@ -317,9 +311,9 @@ var sharedTestCases = []struct { Abc time.Time `json:"abc" dynamodbav:"abc"` }{Abc: testDate}, }, - { // time.Time ptr struct - in: &types.AttributeValue{M: map[string]types.AttributeValue{ - "abc": {S: aws.String("2016-05-03T17:06:26.209072Z")}, + "time.Time ptr struct": { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, }}, actual: &struct { Abc *time.Time `json:"abc" dynamodbav:"abc"` @@ -330,17 +324,17 @@ var sharedTestCases = []struct { }, } -var sharedListTestCases = []struct { +var sharedListTestCases = map[string]struct { in []types.AttributeValue actual, expected interface{} err error }{ - { + "union members": { in: []types.AttributeValue{ - {B: []byte{48, 49}}, - {BOOL: aws.Bool(true)}, - {N: aws.String("123")}, - {S: aws.String("123")}, + &types.AttributeValueMemberB{Value: []byte{48, 49}}, + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberN{Value: "123"}, + &types.AttributeValueMemberS{Value: "123"}, }, actual: func() *[]interface{} { v := []interface{}{} @@ -348,28 +342,28 @@ var sharedListTestCases = []struct { }(), expected: []interface{}{[]byte{48, 49}, true, 123., "123"}, }, - { + "numbers": { in: []types.AttributeValue{ - {N: aws.String("1")}, - {N: aws.String("2")}, - {N: aws.String("3")}, + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, }, actual: &[]interface{}{}, expected: []interface{}{1., 2., 3.}, }, } -var sharedMapTestCases = []struct { +var sharedMapTestCases = map[string]struct { in map[string]types.AttributeValue actual, expected interface{} err error }{ - { + "union members": { in: map[string]types.AttributeValue{ - "B": {B: []byte{48, 49}}, - "BOOL": {BOOL: aws.Bool(true)}, - "N": {N: aws.String("123")}, - "S": {S: aws.String("123")}, + "B": &types.AttributeValueMemberB{Value: []byte{48, 49}}, + "BOOL": &types.AttributeValueMemberBOOL{Value: true}, + "N": &types.AttributeValueMemberN{Value: "123"}, + "S": &types.AttributeValueMemberS{Value: "123"}, }, actual: &map[string]interface{}{}, expected: map[string]interface{}{ @@ -380,6 +374,8 @@ var sharedMapTestCases = []struct { } func assertConvertTest(t *testing.T, actual, expected interface{}, err, expectedErr error) { + t.Helper() + if expectedErr != nil { if err != nil { if e, a := expectedErr, err; !reflect.DeepEqual(e, a) { @@ -391,8 +387,8 @@ func assertConvertTest(t *testing.T, actual, expected interface{}, err, expected } else if err != nil { t.Fatalf("expect no error, got %v", err) } else { - if e, a := ptrToValue(expected), ptrToValue(actual); !reflect.DeepEqual(e, a) { - t.Errorf("expect %v, got %v", e, a) + if diff := cmp.Diff(ptrToValue(expected), ptrToValue(actual)); len(diff) != 0 { + t.Errorf("expect match\n%s", diff) } } }