From 80ec801fe65d854878b9d93dc39702771ab35ad4 Mon Sep 17 00:00:00 2001 From: Radu Topala Date: Tue, 11 Jan 2022 20:22:26 +0200 Subject: [PATCH] i1494 passing opts to attributevalue marshalling (#1495) Fixes the SDK's code generation to pin smithy-cli to $smithyVersion to restrict the supported version. Co-authored-by: Jason Del Ponte <961963+jasdel@users.noreply.github.com> --- .../5ab32814eb5e456ea921d0f0a645ca5b.json | 8 + .../protocol-test-codegen/build.gradle.kts | 1 + feature/dynamodb/attributevalue/decode.go | 85 +++++++++++ feature/dynamodb/attributevalue/encode.go | 139 ++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 .changelog/5ab32814eb5e456ea921d0f0a645ca5b.json diff --git a/.changelog/5ab32814eb5e456ea921d0f0a645ca5b.json b/.changelog/5ab32814eb5e456ea921d0f0a645ca5b.json new file mode 100644 index 00000000000..ef3ffc0ddc2 --- /dev/null +++ b/.changelog/5ab32814eb5e456ea921d0f0a645ca5b.json @@ -0,0 +1,8 @@ +{ + "id": "5ab32814-eb5e-456e-a921-d0f0a645ca5b", + "type": "feature", + "description": "Adds new MarshalWithOptions and UnmarshalWithOptions helpers allowing Encoding and Decoding options to be specified when serializing AttributeValues. Addresses issue: https://github.com/aws/aws-sdk-go-v2/issues/1494", + "modules": [ + "feature/dynamodb/attributevalue" + ] +} diff --git a/codegen/protocol-test-codegen/build.gradle.kts b/codegen/protocol-test-codegen/build.gradle.kts index 4c63b2e14ba..f1e86e2e574 100644 --- a/codegen/protocol-test-codegen/build.gradle.kts +++ b/codegen/protocol-test-codegen/build.gradle.kts @@ -32,6 +32,7 @@ plugins { } dependencies { + implementation("software.amazon.smithy:smithy-cli:$smithyVersion") implementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion") implementation(project(":smithy-aws-go-codegen")) } diff --git a/feature/dynamodb/attributevalue/decode.go b/feature/dynamodb/attributevalue/decode.go index 8a92f8979a3..5a02853dc95 100644 --- a/feature/dynamodb/attributevalue/decode.go +++ b/feature/dynamodb/attributevalue/decode.go @@ -79,6 +79,53 @@ func Unmarshal(av types.AttributeValue, out interface{}) error { return NewDecoder().Decode(av, out) } +// UnmarshalWithOptions will unmarshal AttributeValues to Go value types. +// Both generic interface{} and concrete types are valid unmarshal +// destination types. +// +// Use the `optsFns` functional options to override the default configuration. +// +// UnmarshalWithOptions will allocate maps, slices, and pointers as needed to +// unmarshal the AttributeValue into the provided type value. +// +// When unmarshaling AttributeValues into structs Unmarshal matches +// the field names of the struct to the AttributeValue Map keys. +// Initially it will look for exact field name matching, but will +// fall back to case insensitive if not exact match is found. +// +// With the exception of omitempty, omitemptyelem, binaryset, numberset +// and stringset all struct tags used by Marshal are also used by +// UnmarshalWithOptions. +// +// When decoding AttributeValues to interfaces Unmarshal will use the +// following types. +// +// []byte, AV Binary (B) +// [][]byte, AV Binary Set (BS) +// bool, AV Boolean (BOOL) +// []interface{}, AV List (L) +// map[string]interface{}, AV Map (M) +// float64, AV Number (N) +// Number, AV Number (N) with UseNumber set +// []float64, AV Number Set (NS) +// []Number, AV Number Set (NS) with UseNumber set +// string, AV String (S) +// []string, AV String Set (SS) +// +// If the Decoder option, UseNumber is set numbers will be unmarshaled +// as Number values instead of float64. Use this to maintain the original +// string formating of the number as it was represented in the AttributeValue. +// In addition provides additional opportunities to parse the number +// string based on individual use cases. +// +// When unmarshaling any error that occurs will halt the unmarshal +// and return the error. +// +// The output value provided must be a non-nil pointer +func UnmarshalWithOptions(av types.AttributeValue, out interface{}, optFns ...func(options *DecoderOptions)) error { + return NewDecoder(optFns...).Decode(av, out) +} + // UnmarshalMap is an alias for Unmarshal which unmarshals from // a map of AttributeValues. // @@ -87,6 +134,16 @@ func UnmarshalMap(m map[string]types.AttributeValue, out interface{}) error { return NewDecoder().Decode(&types.AttributeValueMemberM{Value: m}, out) } +// UnmarshalMapWithOptions is an alias for UnmarshalWithOptions which unmarshals from +// a map of AttributeValues. +// +// Use the `optsFns` functional options to override the default configuration. +// +// The output value provided must be a non-nil pointer +func UnmarshalMapWithOptions(m map[string]types.AttributeValue, out interface{}, optFns ...func(options *DecoderOptions)) error { + return NewDecoder(optFns...).Decode(&types.AttributeValueMemberM{Value: m}, out) +} + // UnmarshalList is an alias for Unmarshal func which unmarshals // a slice of AttributeValues. // @@ -95,6 +152,16 @@ func UnmarshalList(l []types.AttributeValue, out interface{}) error { return NewDecoder().Decode(&types.AttributeValueMemberL{Value: l}, out) } +// UnmarshalListWithOptions is an alias for UnmarshalWithOptions func which unmarshals +// a slice of AttributeValues. +// +// Use the `optsFns` functional options to override the default configuration. +// +// The output value provided must be a non-nil pointer +func UnmarshalListWithOptions(l []types.AttributeValue, out interface{}, optFns ...func(options *DecoderOptions)) error { + return NewDecoder(optFns...).Decode(&types.AttributeValueMemberL{Value: l}, out) +} + // UnmarshalListOfMaps is an alias for Unmarshal func which unmarshals a // slice of maps of attribute values. // @@ -111,6 +178,24 @@ func UnmarshalListOfMaps(l []map[string]types.AttributeValue, out interface{}) e return UnmarshalList(items, out) } +// UnmarshalListOfMapsWithOptions is an alias for UnmarshalWithOptions func which unmarshals a +// slice of maps of attribute values. +// +// Use the `optsFns` functional options to override the default configuration. +// +// 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 UnmarshalListOfMapsWithOptions(l []map[string]types.AttributeValue, out interface{}, optFns ...func(options *DecoderOptions)) error { + items := make([]types.AttributeValue, len(l)) + for i, m := range l { + items[i] = &types.AttributeValueMemberM{Value: m} + } + + return UnmarshalListWithOptions(items, out, optFns...) +} + // DecoderOptions is a collection of options to configure how the decoder // unmarshalls the value. type DecoderOptions struct { diff --git a/feature/dynamodb/attributevalue/encode.go b/feature/dynamodb/attributevalue/encode.go index 56e1db53d2b..c8dcf94736a 100644 --- a/feature/dynamodb/attributevalue/encode.go +++ b/feature/dynamodb/attributevalue/encode.go @@ -179,10 +179,115 @@ func Marshal(in interface{}) (types.AttributeValue, error) { return NewEncoder().Encode(in) } +// MarshalWithOptions will serialize the passed in Go value type into a AttributeValue +// type, by using . This value can be used in API operations to simplify marshaling +// your Go value types into AttributeValues. +// +// Use the `optsFns` functional options to override the default configuration. +// +// MarshalWithOptions will recursively transverse the passed in value marshaling its +// contents into a AttributeValue. Marshal supports basic scalars +// (int,uint,float,bool,string), maps, slices, and structs. Anonymous +// nested types are flattened based on Go anonymous type visibility. +// +// Marshaling slices to AttributeValue will default to a List for all +// types except for []byte and [][]byte. []byte will be marshaled as +// Binary data (B), and [][]byte will be marshaled as binary data set +// (BS). +// +// 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. +// +// // Field is ignored +// Field int `dynamodbav:"-"` +// +// // Field AttributeValue map key "myName" +// Field int `dynamodbav:"myName"` +// +// // Field AttributeValue map key "myName", and +// // Field is omitted if the field is a zero value for the type. +// Field int `dynamodbav:"myName,omitempty"` +// +// // Field AttributeValue map key "Field", and +// // Field is omitted if the field is a zero value for the type. +// Field int `dynamodbav:",omitempty"` +// +// // 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"` +// +// // Field will be marshaled as a binary set +// Field [][]byte `dynamodbav:",binaryset"` +// +// // Field will be marshaled as a number set +// Field []int `dynamodbav:",numberset"` +// +// // Field will be marshaled as a string set +// Field []string `dynamodbav:",stringset"` +// +// // Field will be marshaled as Unix time number in seconds. +// // This tag is only valid with time.Time typed struct fields. +// // Important to note that zero value time as unixtime is not 0 seconds +// // from January 1, 1970 UTC, but -62135596800. Which is seconds between +// // January 1, 0001 UTC, and January 1, 0001 UTC. +// Field time.Time `dynamodbav:",unixtime"` +// +// The omitempty tag is only used during Marshaling and is ignored for +// Unmarshal. 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. +// +// 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 a zero 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. +// +// Error that occurs when marshaling will stop the marshal, and return +// the error. +// +// MarshalWithOptions cannot represent cyclic data structures and will not handle them. +// Passing cyclic structures to Marshal will result in an infinite recursion. +func MarshalWithOptions(in interface{}, optFns ...func(*EncoderOptions)) (types.AttributeValue, error) { + return NewEncoder(optFns...).Encode(in) +} + // 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. // +// Use the `optsFns` functional options to override the default configuration. +// // This is useful for APIs such as PutItem. func MarshalMap(in interface{}) (map[string]types.AttributeValue, error) { av, err := NewEncoder().Encode(in) @@ -195,6 +300,24 @@ func MarshalMap(in interface{}) (map[string]types.AttributeValue, error) { return asMap.Value, nil } +// MarshalMapWithOptions is an alias for MarshalWithOptions 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. +// +// Use the `optsFns` functional options to override the default configuration. +// +// This is useful for APIs such as PutItem. +func MarshalMapWithOptions(in interface{}, optFns ...func(*EncoderOptions)) (map[string]types.AttributeValue, error) { + av, err := NewEncoder(optFns...).Encode(in) + + asMap, ok := av.(*types.AttributeValueMemberM) + if err != nil || av == nil || !ok { + return map[string]types.AttributeValue{}, err + } + + return asMap.Value, nil +} + // MarshalList is an alias for Marshal func which marshals Go value // type to a slice of AttributeValues. If the in parameter does not serialize // to a slice, an empty AttributeValue slice will be returned. @@ -209,6 +332,22 @@ func MarshalList(in interface{}) ([]types.AttributeValue, error) { return asList.Value, nil } +// MarshalListWithOptions is an alias for MarshalWithOptions func which marshals Go value +// type to a slice of AttributeValues. If the in parameter does not serialize +// to a slice, an empty AttributeValue slice will be returned. +// +// Use the `optsFns` functional options to override the default configuration. +func MarshalListWithOptions(in interface{}, optFns ...func(*EncoderOptions)) ([]types.AttributeValue, error) { + av, err := NewEncoder(optFns...).Encode(in) + + asList, ok := av.(*types.AttributeValueMemberL) + if err != nil || av == nil || !ok { + return []types.AttributeValue{}, err + } + + return asList.Value, nil +} + // EncoderOptions is a collection of options shared between marshaling // and unmarshaling type EncoderOptions struct {