diff --git a/feature/dynamodb/attributevalue/convert.go b/feature/dynamodb/attributevalue/convert.go new file mode 100644 index 00000000000..bee71e96d5a --- /dev/null +++ b/feature/dynamodb/attributevalue/convert.go @@ -0,0 +1,87 @@ +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" +) + +// FromDynamoDBStreamsMap converts a map of Amazon DynamoDB Streams +// AttributeValues, and all nested members. +func FromDynamoDBStreamsMap(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 = FromDynamoDBStreams(value) + if err != nil { + return nil, err + } + } + + return to, nil +} + +// FromDynamoDBStreamsList converts a slice of Amazon DynamoDB Streams +// AttributeValues, and all nested members. +func FromDynamoDBStreamsList(from []streams.AttributeValue) (to []ddb.AttributeValue, err error) { + to = make([]ddb.AttributeValue, len(from)) + for i := 0; i < len(from); i++ { + to[i], err = FromDynamoDBStreams(from[i]) + if err != nil { + return nil, err + } + } + + return to, nil +} + +// FromDynamoDBStreams converts an Amazon DynamoDB Streams AttributeValue, and +// all nested members. +func FromDynamoDBStreams(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 := FromDynamoDBStreamsList(tv.Value) + if err != nil { + return nil, err + } + return &ddb.AttributeValueMemberL{Value: values}, nil + + case *streams.AttributeValueMemberM: + values, err := FromDynamoDBStreamsMap(tv.Value) + if err != nil { + return nil, err + } + return &ddb.AttributeValueMemberM{Value: values}, nil + + default: + return nil, fmt.Errorf("unknown AttributeValue union member, %T", from) + } +} diff --git a/feature/dynamodb/attributevalue/decode.go b/feature/dynamodb/attributevalue/decode.go index 3010752afbc..1bda3693b44 100644 --- a/feature/dynamodb/attributevalue/decode.go +++ b/feature/dynamodb/attributevalue/decode.go @@ -30,10 +30,10 @@ import ( // return nil // } type Unmarshaler interface { - UnmarshalDynamoDBAttributeValue(*types.AttributeValue) error + UnmarshalDynamoDBAttributeValue(types.AttributeValue) error } -// Unmarshal will unmarshal DynamoDB AttributeValues to Go value types. +// Unmarshal will unmarshal AttributeValues to Go value types. // Both generic interface{} and concrete types are valid unmarshal // destination 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,28 +91,35 @@ 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 // slice of maps of attribute values. // -// This is useful for when you need to unmarshal the Items from a DynamoDB -// Query API call. +// This is useful for when you need to unmarshal the Items from a Query API +// call. // // The output value provided must be a non-nil pointer 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,37 @@ 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: + return d.decodeString(tv.Value, v, fieldTag) + + case *types.AttributeValueMemberSS: + return d.decodeStringSet(tv.Value, v) + + default: + return fmt.Errorf("unsupported AttributeValue type, %T", av) + } } func (d *Decoder) decodeBinary(b []byte, v reflect.Value) error { @@ -239,10 +258,11 @@ 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() { 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 +297,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 +307,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 +317,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 +373,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 +393,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 +404,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 +419,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 +442,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 +455,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 +486,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 +519,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 +527,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 +537,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 +560,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 +574,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 +658,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 +669,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 -// AttributeValue element into a Go type. This is different from UnmarshalTypeError -// in that it wraps the underlying error that occured. +// An UnmarshalError wraps an error that occurred while unmarshaling a +// AttributeValue element into a Go type. This is different from +// UnmarshalTypeError 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..25b4020e5b7 100644 --- a/feature/dynamodb/attributevalue/doc.go +++ b/feature/dynamodb/attributevalue/doc.go @@ -1,19 +1,32 @@ // Package attributevalue provides marshaling and unmarshaling utilities to -// convert between Go types and types.AttributeValues. +// convert between Go types and Amazon DynamoDB AttributeValues. // -// These utilities allow you to marshal slices, maps, structs, and scalar values -// to and from types.AttributeValue. These are useful when marshaling -// Go value tyes to types.AttributeValue for DynamoDB requests, or -// unmarshaling the types.AttributeValue back into a Go value type. +// These utilities allow you to marshal slices, maps, structs, and scalar +// values to and from AttributeValue type. These utilities make it +// easier to convert between AttributeValue and Go types when working with +// DynamoDB resources. +// +// This package only converts between Go types and DynamoDB AttributeValue. See +// the feature/dynamodbstreams/attributevalue package for converting to +// DynamoDBStreams AttributeValue types. +// +// Converting AttributeValue between DynamoDB and DynamoDBStreams +// +// The FromDynamoStreamsDBMap, FromDynamoStreamsDBList, and FromDynamoDBStreams +// functions provide the conversion utilities to convert a DynamoDBStreams +// AttributeValue type to a DynamoDB AttributeValue type. Use these utilities +// when you need to convert the AttributeValue type between the two APIs. // // AttributeValue Marshaling // -// To marshal a Go type to a AttributeValue you can use the Marshal -// functions in the attributevalue package. There are specialized versions -// of these functions for collections of Attributevalue, such as maps and lists. +// To marshal a Go type to an AttributeValue you can use the Marshal, +// MarshalList, and MarshalMap functions. The List and Map functions are +// specialized versions of the Marshal for serializing slices and maps of +// Attributevalues. // -// The following example uses MarshalMap to convert the Record Go type to a -// types.AttributeValue type and use the value to make a PutItem API request. +// The following example uses MarshalMap to convert a Go struct, Record to a +// AttributeValue. The AttributeValue value is then used as input to the +// PutItem operation call. // // type Record struct { // ID string @@ -31,26 +44,27 @@ // } // av, err := attributevalue.MarshalMap(r) // if err != nil { -// panic(fmt.Sprintf("failed to DynamoDB marshal Record, %v", err)) +// return fmt.Errorf("failed to marshal Record, %w", err) // } // -// _, err = svc.PutItem(&dynamodb.PutItemInput{ +// _, err = client.PutItem(context.TODO(), &dynamodb.PutItemInput{ // TableName: aws.String(myTableName), // Item: av, // }) // if err != nil { -// panic(fmt.Sprintf("failed to put Record to DynamoDB, %v", err)) +// return fmt.Errorf("failed to put Record, %w", err) // } // // AttributeValue Unmarshaling // -// To unmarshal a types.AttributeValue to a Go type you can use the Unmarshal -// functions in the attributevalue package. There are specialized versions -// of these functions for collections of Attributevalue, such as maps and lists. +// To unmarshal an AttributeValue to a Go type you can use the Unmarshal, +// UnmarshalList, UnmarshalMap, and UnmarshalListOfMaps functions. The List and +// Map functions are specialized versions of the Unmarshal function for +// unmarshal slices and maps of Attributevalues. // -// The following example will unmarshal the DynamoDB's Scan API operation. The -// Items returned by the operation will be unmarshaled into the slice of Records -// Go type. +// The following example will unmarshal Items result from the DynamoDB's +// Scan API operation. The Items returned will be unmarshaled into the slice of +// the Records struct. // // type Record struct { // ID string @@ -59,37 +73,25 @@ // // //... // -// var records []Record -// -// // Use the ScanPages method to perform the scan with pagination. Use -// // just Scan method to make the API call without pagination. -// err := svc.ScanPages(&dynamodb.ScanInput{ +// result, err := client.Scan(context.Context(), &dynamodb.ScanInput{ // TableName: aws.String(myTableName), -// }, func(page *dynamodb.ScanOutput, last bool) bool { -// recs := []Record{} -// -// err := attributevalue.UnmarshalListOfMaps(page.Items, &recs) -// if err != nil { -// panic(fmt.Sprintf("failed to unmarshal Dynamodb Scan Items, %v", err)) -// } -// -// records = append(records, recs...) -// -// return true // keep paging // }) +// if err != nil { +// return fmt.Errorf("failed to scan table, %w", err) +// } +// +// var records []Record +// err := attributevalue.UnmarshalListOfMaps(results.Items, &records) +// if err != nil { +// return fmt.Errorf("failed to unmarshal Items, %w", err)) +// } // -// The ConvertTo, ConvertToList, ConvertToMap, ConvertFrom, ConvertFromMap -// and ConvertFromList methods have been deprecated. The Marshal and Unmarshal -// functions should be used instead. The ConvertTo|From marshallers do not -// support BinarySet, NumberSet, nor StringSets, and will incorrect marshal -// binary data fields in structs as base64 strings. +// Struct tags // -// The Marshal and Unmarshal functions correct this behavior, and removes -// the reliance on encoding.json. `json` struct tags are still supported. In -// addition support for a new struct tag `dynamodbav` was added. Support for -// the json.Marshaler and json.Unmarshaler interfaces have been removed and -// replaced with have been replaced with attributevalue.Marshaler and -// attributevalue.Unmarshaler interfaces. +// The AttributeValue Marshal and Unmarshal functions support the `dynamodbav` +// struct tag by default. Additional tags can be enabled with the +// EncoderOptions and DecoderOptions, TagKey option. // -// `time.Time` is marshaled as `time.RFC3339Nano` format. +// See the Marshal and Unmarshal function for information on how struct tags +// and fields are marshaled and unmarshaled. package attributevalue diff --git a/feature/dynamodb/attributevalue/empty_collections_test.go b/feature/dynamodb/attributevalue/empty_collections_test.go index d16db3bd0f0..e22aee0e8e5 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 { @@ -42,6 +42,23 @@ type testEmptyCollectionsOmittedNumericalScalars struct { Float64 float64 `dynamodbav:",omitempty"` } +type testEmptyCollectionsNulledNumericalScalars struct { + String string `dynamodbav:",nullempty"` + + Uint8 uint8 `dynamodbav:",nullempty"` + Uint16 uint16 `dynamodbav:",nullempty"` + Uint32 uint32 `dynamodbav:",nullempty"` + Uint64 uint64 `dynamodbav:",nullempty"` + + Int8 int8 `dynamodbav:",nullempty"` + Int16 int16 `dynamodbav:",nullempty"` + Int32 int32 `dynamodbav:",nullempty"` + Int64 int64 `dynamodbav:",nullempty"` + + Float32 float32 `dynamodbav:",nullempty"` + Float64 float64 `dynamodbav:",nullempty"` +} + type testEmptyCollectionsPtrScalars struct { PtrString *string @@ -60,6 +77,8 @@ type testEmptyCollectionsPtrScalars struct { } type testEmptyCollectionsOmittedPtrNumericalScalars struct { + PtrString *string `dynamodbav:",omitempty"` + PtrUint8 *uint8 `dynamodbav:",omitempty"` PtrUint16 *uint16 `dynamodbav:",omitempty"` PtrUint32 *uint32 `dynamodbav:",omitempty"` @@ -74,6 +93,23 @@ type testEmptyCollectionsOmittedPtrNumericalScalars struct { PtrFloat64 *float64 `dynamodbav:",omitempty"` } +type testEmptyCollectionsNulledPtrNumericalScalars struct { + PtrString *string `dynamodbav:",nullempty"` + + PtrUint8 *uint8 `dynamodbav:",nullempty"` + PtrUint16 *uint16 `dynamodbav:",nullempty"` + PtrUint32 *uint32 `dynamodbav:",nullempty"` + PtrUint64 *uint64 `dynamodbav:",nullempty"` + + PtrInt8 *int8 `dynamodbav:",nullempty"` + PtrInt16 *int16 `dynamodbav:",nullempty"` + PtrInt32 *int32 `dynamodbav:",nullempty"` + PtrInt64 *int64 `dynamodbav:",nullempty"` + + PtrFloat32 *float32 `dynamodbav:",nullempty"` + PtrFloat64 *float64 `dynamodbav:",nullempty"` +} + type testEmptyCollectionTypes struct { Map map[string]string Slice []string @@ -96,6 +132,17 @@ type testEmptyCollectionTypesOmitted struct { StringSet []string `dynamodbav:",stringset,omitempty"` } +type testEmptyCollectionTypesNulled struct { + Map map[string]string `dynamodbav:",nullempty"` + Slice []string `dynamodbav:",nullempty"` + ByteSlice []byte `dynamodbav:",nullempty"` + ByteArray [4]byte `dynamodbav:",nullempty"` + ZeroArray [0]byte `dynamodbav:",nullempty"` + BinarySet [][]byte `dynamodbav:",binaryset,nullempty"` + NumberSet []int `dynamodbav:",numberset,nullempty"` + StringSet []string `dynamodbav:",stringset,nullempty"` +} + type testEmptyCollectionStruct struct { Int int } @@ -105,43 +152,46 @@ type testEmptyCollectionStructOmitted struct { } var sharedEmptyCollectionsTestCases = map[string]struct { - in *types.AttributeValue + in types.AttributeValue + // alternative input to compare against for marshal flow + inMarshal 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 +210,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{}, @@ -195,39 +245,89 @@ var sharedEmptyCollectionsTestCases = map[string]struct { Float64: 10.1, }, }, + "null scalars with zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "String": &types.AttributeValueMemberNULL{Value: true}, + "Uint8": &types.AttributeValueMemberNULL{Value: true}, + "Uint16": &types.AttributeValueMemberNULL{Value: true}, + "Uint32": &types.AttributeValueMemberNULL{Value: true}, + "Uint64": &types.AttributeValueMemberNULL{Value: true}, + "Int8": &types.AttributeValueMemberNULL{Value: true}, + "Int16": &types.AttributeValueMemberNULL{Value: true}, + "Int32": &types.AttributeValueMemberNULL{Value: true}, + "Int64": &types.AttributeValueMemberNULL{Value: true}, + "Float32": &types.AttributeValueMemberNULL{Value: true}, + "Float64": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &testEmptyCollectionsNulledNumericalScalars{}, + expected: testEmptyCollectionsNulledNumericalScalars{}, + }, + "null scalars with non-zero value": { + 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: &testEmptyCollectionsNulledNumericalScalars{}, + expected: testEmptyCollectionsNulledNumericalScalars{ + String: "test string", + Uint8: 1, + Uint16: 2, + Uint32: 3, + Uint64: 4, + Int8: -5, + Int16: -6, + Int32: -7, + Int64: -8, + Float32: 9.9, + Float64: 10.1, + }, + }, "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 +345,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 +376,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 +412,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 +441,127 @@ 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{}, }, + "null nil pointer scalars": { + 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: &testEmptyCollectionsNulledPtrNumericalScalars{}, + expected: testEmptyCollectionsNulledPtrNumericalScalars{}, + }, + "null non-nil pointer to scalars with zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberS{Value: ""}, + "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: &testEmptyCollectionsNulledPtrNumericalScalars{}, + expected: testEmptyCollectionsNulledPtrNumericalScalars{ + PtrString: aws.String(""), + PtrUint8: aws.Uint8(0), + PtrUint16: aws.Uint16(0), + PtrUint32: aws.Uint32(0), + PtrUint64: aws.Uint64(0), + PtrInt8: aws.Int8(0), + PtrInt16: aws.Int16(0), + PtrInt32: aws.Int32(0), + PtrInt64: aws.Int64(0), + PtrFloat32: aws.Float32(0), + PtrFloat64: aws.Float64(0), + }, + }, + "null non-nil pointer to non-zero scalar": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberS{Value: "abc"}, + "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: &testEmptyCollectionsNulledPtrNumericalScalars{}, + expected: testEmptyCollectionsNulledPtrNumericalScalars{ + PtrString: aws.String("abc"), + PtrUint8: aws.Uint8(1), + PtrUint16: aws.Uint16(2), + PtrUint32: aws.Uint32(3), + PtrUint64: aws.Uint64(4), + PtrInt8: aws.Int8(-5), + PtrInt16: aws.Int16(-6), + PtrInt32: aws.Int32(-7), + PtrInt64: aws.Int64(-8), + PtrFloat32: aws.Float32(9.9), + PtrFloat64: aws.Float64(10.1), + }, + }, "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)}, + // sets are special and not serialized to empty if no elements + "BinarySet": &types.AttributeValueMemberBS{Value: [][]byte{}}, + "NumberSet": &types.AttributeValueMemberNS{Value: []string{}}, + "StringSet": &types.AttributeValueMemberSS{Value: []string{}}, + }, + }, + inMarshal: &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)}, + // sets are special and not serialized to empty if no elements + "BinarySet": &types.AttributeValueMemberNULL{Value: true}, + "NumberSet": &types.AttributeValueMemberNULL{Value: true}, + "StringSet": &types.AttributeValueMemberNULL{Value: true}, }, }, actual: &testEmptyCollectionTypes{}, @@ -382,20 +577,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 +609,35 @@ 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{}}, + }, + }, + inMarshal: &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.AttributeValueMemberNULL{Value: true}, + "NumberSet": &types.AttributeValueMemberNULL{Value: true}, + "StringSet": &types.AttributeValueMemberNULL{Value: true}, }, }, actual: &testEmptyCollectionTypesOmitted{}, @@ -443,19 +652,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{}, @@ -470,15 +682,98 @@ var sharedEmptyCollectionsTestCases = map[string]struct { StringSet: []string{"test", "slice"}, }, }, + "null maps slices nil values": { + 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)}, + "BinarySet": &types.AttributeValueMemberNULL{Value: true}, + "NumberSet": &types.AttributeValueMemberNULL{Value: true}, + "StringSet": &types.AttributeValueMemberNULL{Value: true}, + "ZeroArray": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &testEmptyCollectionTypesNulled{}, + expected: testEmptyCollectionTypesNulled{}, + }, + "null maps slices zero values": { + 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{}}, + }, + }, + inMarshal: &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.AttributeValueMemberNULL{Value: true}, + "NumberSet": &types.AttributeValueMemberNULL{Value: true}, + "StringSet": &types.AttributeValueMemberNULL{Value: true}, + "ZeroArray": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &testEmptyCollectionTypesNulled{}, + expected: testEmptyCollectionTypesNulled{ + Map: map[string]string{}, + Slice: []string{}, + ByteSlice: []byte{}, + ByteArray: [4]byte{}, + BinarySet: [][]byte{}, + NumberSet: []int{}, + StringSet: []string{}, + }, + }, + "null maps slices non-zero values": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Map": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "key": &types.AttributeValueMemberS{Value: "value"}, + }, + }, + "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"}}, + "ZeroArray": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &testEmptyCollectionTypesNulled{}, + expected: testEmptyCollectionTypesNulled{ + Map: map[string]string{"key": "value"}, + Slice: []string{"test", "slice"}, + ByteSlice: []byte{0, 1}, + ByteArray: [4]byte{0, 1, 2, 3}, + ZeroArray: [0]byte{}, + BinarySet: [][]byte{{0, 1}, {2, 3}}, + NumberSet: []int{0, 1}, + StringSet: []string{"test", "slice"}, + }, + }, "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 +786,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 +813,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 +829,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 +844,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"}, + }}, }, }, }, @@ -581,7 +880,11 @@ func TestMarshalEmptyCollections(t *testing.T) { for name, c := range sharedEmptyCollectionsTestCases { t.Run(name, func(t *testing.T) { av, err := Marshal(c.expected) - assertConvertTest(t, av, c.in, err, c.err) + in := c.in + if c.inMarshal != nil { + in = c.inMarshal + } + assertConvertTest(t, av, in, err, c.err) }) } } @@ -591,34 +894,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..50bf6aecbcd 100644 --- a/feature/dynamodb/attributevalue/encode.go +++ b/feature/dynamodb/attributevalue/encode.go @@ -6,12 +6,11 @@ import ( "strconv" "time" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" ) // An UnixTime provides aliasing of time.Time into a type that when marshaled -// and unmarshaled with DynamoDB AttributeValues it will be done so as number +// and unmarshaled with AttributeValues it will be done so as number // instead of string in seconds since January 1, 1970 UTC. // // This type is useful as an alternative to the struct tag `unixtime` when you @@ -30,24 +29,30 @@ import ( type UnixTime time.Time // MarshalDynamoDBAttributeValue implements the Marshaler interface so that -// the UnixTime can be marshaled from to a DynamoDB AttributeValue number +// the UnixTime can be marshaled from to a 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 -// the UnixTime can be unmarshaled from a DynamoDB AttributeValue number representing +// the UnixTime can be unmarshaled from a AttributeValue number representing // the number of seconds since January 1, 1970 UTC. // // If an error parsing the AttributeValue number occurs UnmarshalError will be // returned. -func (e *UnixTime) UnmarshalDynamoDBAttributeValue(av *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,21 +65,21 @@ 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 -// type. This value can be used in DynamoDB API operations to simplify marshaling +// Marshal will serialize the passed in Go value type into a AttributeValue +// type. This value can be used in API operations to simplify marshaling // your Go value types into AttributeValues. // // Marshal will recursively transverse the passed in value marshaling its @@ -87,6 +92,8 @@ type Marshaler interface { // Binary data (B), and [][]byte will be marshaled as binary data set // (BS). // +// The `time.Time` type is marshaled as `time.RFC3339Nano` format. +// // `dynamodbav` struct tag can be used to control how the value will be // marshaled into a AttributeValue. // @@ -97,17 +104,25 @@ type Marshaler interface { // Field int `dynamodbav:"myName"` // // // Field AttributeValue map key "myName", and -// // Field is omitted if it is empty +// // Field is omitted if the field is a zero value for the type. // Field int `dynamodbav:"myName,omitempty"` // // // Field AttributeValue map key "Field", and -// // Field is omitted if it is empty +// // Field is omitted if the field is a zero value for the type. // Field int `dynamodbav:",omitempty"` // -// // Field's elems will be omitted if empty +// // Field's elems will be omitted if the elem's value is empty. // // only valid for slices, and maps. // Field []string `dynamodbav:",omitemptyelem"` // +// // Field AttributeValue map key "Field", and +// // Field is sent as NULL if the field is a zero value for the type. +// Field int `dynamodbav:",nullempty"` +// +// // Field's elems will be sent as NULL if the elem's value a zero value +// // for the type. Only valid for slices, and maps. +// Field []string `dynamodbav:",nullemptyelem"` +// // // Field will be marshaled as a AttributeValue string // // only value for number types, (int,uint,float) // Field int `dynamodbav:",string"` @@ -129,121 +144,123 @@ type Marshaler interface { // Field time.Time `dynamodbav:",unixtime"` // // The omitempty tag is only used during Marshaling and is ignored for -// Unmarshal. Any zero value or a value when marshaled results in a -// AttributeValue NULL will be added to AttributeValue Maps during struct -// marshal. The omitemptyelem tag works the same as omitempty except it -// applies to maps and slices instead of struct fields, and will not be +// Unmarshal. omitempty will skip any member if the Go value of the member is +// zero. The omitemptyelem tag works the same as omitempty except it applies to +// the elements of maps and slices instead of struct fields, and will not be // included in the marshaled AttributeValue Map, List, or Set. // -// For convenience and backwards compatibility with ConvertTo functions -// json struct tags are supported by the Marshal and Unmarshal. If -// both json and dynamodbav struct tags are provided the json tag will -// be ignored in favor of dynamodbav. +// The nullempty tag is only used during Marshaling and is ignored for +// Unmarshal. nullempty will serialize a AttributeValueMemberNULL for the +// member if the Go value of the member is zero. nullemptyelem tag works the +// same as nullempty except it applies to the elements of maps and slices +// instead of struct fields, and will not be included in the marshaled +// AttributeValue Map, List, or Set. // // All struct fields and with anonymous fields, are marshaled unless the // any of the following conditions are meet. // // - the field is not exported // - json or dynamodbav field tag is "-" -// - json or dynamodbav field tag specifies "omitempty", and is empty. +// - json or dynamodbav field tag specifies "omitempty", and is a zero value. // -// Pointer and interfaces values encode as the value pointed to or contained -// in the interface. A nil value encodes as the AttributeValue NULL value. +// Pointer and interfaces values are encoded as the value pointed to or +// contained in the interface. A nil value encodes as the AttributeValue NULL +// value unless `omitempty` struct tag is provided. // // Channel, complex, and function values are not encoded and will be skipped // when walking the value to be marshaled. // -// When marshaling any error that occurs will halt the marshal and return +// Error that occurs when marshaling will stop the marshal, and return // the error. // // Marshal cannot represent cyclic data structures and will not handle them. // Passing cyclic structures to Marshal will result in an infinite recursion. -func Marshal(in interface{}) (*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. +// This is useful for 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 + + // Will encode any slice being encoded as a set (SS, NS, and BS) as a NULL + // AttributeValue if the slice is not nil, but is empty but contains no + // elements. + // + // If a type implements the Marshal interface, and returns empty set + // slices, this option will not modify the returned value. + // + // Defaults to enabled, because AttributeValue sets cannot currently be + // empty lists. + NullEmptySets bool } // An Encoder provides marshaling Go value types to AttributeValues. type Encoder struct { - MarshalOptions - - // Empty strings, "", will be marked as NULL AttributeValue types. - // Empty strings are not valid values for DynamoDB. Will not apply - // to lists, sets, or maps. Use the struct tag `omitemptyelem` - // to skip empty (zero) values in lists, sets and maps. - // - // Enabled by default. - NullEmptyString bool + 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, +func NewEncoder(optFns ...func(*EncoderOptions)) *Encoder { + options := EncoderOptions{ + NullEmptySets: true, } - for _, o := range opts { - o(e) + 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 +276,83 @@ 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 && fieldTag.NullEmpty { + return nil, &InvalidMarshalError{ + msg: "unable to encode AttributeValue for zero value field with incompatible struct tags, omitempty and nullempty"} + } + + if fieldTag.OmitEmpty { + return nil, nil + } else if isNullableZeroValue(v) || fieldTag.NullEmpty { + 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) + if fieldTag.OmitEmpty { + return nil, nil + } + // Handle case where member type needed to be dereferenced and resulted + // in a kind that is invalid. + 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 +363,231 @@ 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, + NullEmpty: fieldTag.NullEmptyElem, + }) 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 +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) + + return &types.AttributeValueMemberB{ + Value: append([]byte{}, slice.Bytes()...), + }, nil } - switch v.Type().Elem().Kind() { - case reflect.Uint8: - if v.Kind() == reflect.Slice && v.IsNil() { - encodeNull(av) + var setElemFn func(types.AttributeValue) error + var av types.AttributeValue + + if fieldTag.AsBinSet || v.Type() == byteSliceSliceType { // Binary Set + if v.Len() == 0 && e.options.NullEmptySets { + return encodeNull(), nil + } + + 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"} + } + bs.Value = append(bs.Value, b.Value) return nil } - slice := reflect.MakeSlice(byteSliceType, v.Len(), v.Len()) - reflect.Copy(slice, v) + } else if fieldTag.AsNumSet { // Number Set + if v.Len() == 0 && e.options.NullEmptySets { + return encodeNull(), nil + } - b := slice.Bytes() + 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"} + } + ns.Value = append(ns.Value, n.Value) + return nil + } - av.B = append([]byte{}, b...) - default: - var elemFn func(types.AttributeValue) error + } else if fieldTag.AsStrSet { // String Set + if v.Len() == 0 && e.options.NullEmptySets { + return encodeNull(), nil + } - 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 - } - } 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 - } - } else { // List - av.L = make([]types.AttributeValue, 0, v.Len()) - elemFn = func(elem types.AttributeValue) error { - av.L = append(av.L, elem) - return nil + 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, + NullEmpty: fieldTag.NullEmptyElem, + }) 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 + + 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, nil } - 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, nil } - - return nil } func encodeInt(i int64) string { @@ -542,9 +599,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,8 +614,10 @@ 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.Invalid: + return true case reflect.Array: return v.Len() == 0 case reflect.Map, reflect.Slice: @@ -580,84 +638,42 @@ 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.Invalid: + return true + case reflect.Map, reflect.Slice: + return v.IsNil() + 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 -} - -func keepOrOmitEmpty(omitEmpty bool, av types.AttributeValue, err error) (bool, error) { - if err != nil { - if _, ok := err.(*unsupportedMarshalTypeError); ok { - return true, nil - } - return false, err - } - - if av.NULL != nil && omitEmpty { - return true, nil - } - - return false, nil + return nil, nil } // An InvalidMarshalError is an error type representing an error // occurring when marshaling a Go value type to an AttributeValue. type InvalidMarshalError struct { - emptyOrigError msg string } // Error returns the string representation of the error. // satisfying the error interface func (e *InvalidMarshalError) Error() string { - return fmt.Sprintf("%s: %s", e.Code(), e.Message()) -} - -// Code returns the code of the error, satisfying the awserr.Error -// interface. -func (e *InvalidMarshalError) Code() string { - return "InvalidMarshalError" -} - -// Message returns the detailed message of the error, satisfying -// the awserr.Error interface. -func (e *InvalidMarshalError) Message() string { - return e.msg -} - -// An unsupportedMarshalTypeError represents a Go value type -// which cannot be marshaled into an AttributeValue and should -// be skipped by the marshaler. -type unsupportedMarshalTypeError struct { - emptyOrigError - Type reflect.Type -} - -// Error returns the string representation of the error. -// satisfying the error interface -func (e *unsupportedMarshalTypeError) Error() string { - return fmt.Sprintf("%s: %s", e.Code(), e.Message()) -} - -// Code returns the code of the error, satisfying the awserr.Error -// interface. -func (e *unsupportedMarshalTypeError) Code() string { - return "unsupportedMarshalTypeError" -} - -// Message returns the detailed message of the error, satisfying -// the awserr.Error interface. -func (e *unsupportedMarshalTypeError) Message() string { - return "Go value type " + e.Type.String() + " is not supported" + return fmt.Sprintf("marshal failed, %s", e.msg) } diff --git a/feature/dynamodb/attributevalue/encode_test.go b/feature/dynamodb/attributevalue/encode_test.go index c9369ecb531..bc2514a2c7b 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"}, }}, }, } @@ -106,17 +107,19 @@ func TestMarshalListOmitEmptyElem(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) } } 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"}, + "qrs": &types.AttributeValueMemberS{Value: "abc"}, }}, }, } @@ -126,14 +129,90 @@ func TestMarshalMapOmitEmptyElem(t *testing.T) { "efg": nil, "hij": "", "klm": "abc", + "nop": func() interface{} { + var v *string + return v + }(), + "qrs": func() interface{} { + v := "abc" + return &v + }(), }} actual, err := Marshal(m) 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) + } +} + +type testNullEmptyElemListStruct struct { + Values []string `dynamodbav:",nullemptyelem"` +} + +type testNullEmptyElemMapStruct struct { + Values map[string]interface{} `dynamodbav:",nullemptyelem"` +} + +func TestMarshalListNullEmptyElem(t *testing.T) { + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Values": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberNULL{Value: true}, + &types.AttributeValueMemberS{Value: "123"}, + }}, + }, + } + + m := testNullEmptyElemListStruct{Values: []string{"abc", "", "123"}} + + actual, err := Marshal(m) + if err != nil { + t.Errorf("expect nil, got %v", err) + } + if diff := cmp.Diff(expect, actual); len(diff) != 0 { + t.Errorf("expect match\n%s", diff) + } +} + +func TestMarshalMapNullEmptyElem(t *testing.T) { + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Values": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "abc": &types.AttributeValueMemberN{Value: "123"}, + "efg": &types.AttributeValueMemberNULL{Value: true}, + "hij": &types.AttributeValueMemberS{Value: ""}, + "klm": &types.AttributeValueMemberS{Value: "abc"}, + "nop": &types.AttributeValueMemberNULL{Value: true}, + "qrs": &types.AttributeValueMemberS{Value: "abc"}, + }}, + }, + } + + m := testNullEmptyElemMapStruct{Values: map[string]interface{}{ + "abc": 123., + "efg": nil, + "hij": "", + "klm": "abc", + "nop": func() interface{} { + var v *string + return v + }(), + "qrs": func() interface{} { + v := "abc" + return &v + }(), + }} + + actual, err := Marshal(m) + if err != nil { + t.Errorf("expect nil, got %v", err) + } + if diff := cmp.Diff(expect, actual); len(diff) != 0 { + t.Errorf("expect match\n%s", diff) } } @@ -144,9 +223,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 +267,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 +295,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 +324,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..a5396c70c4d 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,49 @@ 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.Fatalf("marshalList with input %#v returned %#v, expected error `%s`", + c.input, actual, c.err) + } else if err.Error() != c.err.Error() { + t.Fatalf("marshalList with input %#v returned error `%s`, expected error `%s`", + c.input, err, c.err) + } + return + } + if err != nil { + t.Fatalf("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 +508,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 +562,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) } } } diff --git a/feature/dynamodb/attributevalue/tag.go b/feature/dynamodb/attributevalue/tag.go index 29b79c3478c..6eb901706fb 100644 --- a/feature/dynamodb/attributevalue/tag.go +++ b/feature/dynamodb/attributevalue/tag.go @@ -10,6 +10,8 @@ type tag struct { Ignore bool OmitEmpty bool OmitEmptyElem bool + NullEmpty bool + NullEmptyElem bool AsString bool AsBinSet, AsNumSet, AsStrSet bool AsUnixTime bool @@ -53,6 +55,10 @@ func (t *tag) parseTagStr(tagStr string) { t.OmitEmpty = true case "omitemptyelem": t.OmitEmptyElem = true + case "nullempty": + t.NullEmpty = true + case "nullemptyelem": + t.NullEmptyElem = true case "string": t.AsString = true case "binaryset":