From bb7c6027420d2d7f0539dfd1233d89a5c30d5cca Mon Sep 17 00:00:00 2001 From: Jason Del Ponte Date: Tue, 15 Dec 2020 14:57:44 -0800 Subject: [PATCH] dynamodb/attributevalue: Add utility to (un)marshal Attribtue Values (#948) Adds utilities for mapping Amazon DynamoDB API AttributeValue to and from Go types. * Adds conversion util for DynamoDB -> DynamoDBStreams AttributeValue. * Fixes #895 - Adds back marshaler/unmarshalers for attribute value to/from go types. * Fixes #929 - Adds generic support for `json.Number` like types that are aliases of `string`s. * Fixes #115 - Simplifies the rules of `null` vs skipped vs zero value for all types. * All nil pointers, map, slice members are serialized as NULL AttributeValue. * `omitempty` struct tag skips zero value of members with the struct tag. `omitemptyelem` skips elements of list/map that have zero value. * `nullempty` struct tag serializes zero value of members with the struct tag as NULL AttributeValue. `nullemptyelem` does same for elements of list/map that have zero value. * Empty and Nil Sets (NS, BS, SS) are serialized to null by default unless `NullEmptySets` EncoderOptions is set to false. True by default. Nil sets are always serialized as NULL, unless the`omitempty` struct tag is used. * Adds `nullempty` and `nullemptyelem` struct tags to direct if the encoder should marshal the member as a AttributeValue NULL if the member's value is the type's zero value, (e.g. "" for string, 0 for number, nil for pointer/map/slice, false for bool) --- Makefile | 20 +- .../dynamodb-streams.2012-08-10.json | 2 +- .../aws-models/dynamodb.2012-08-10.json | 2 +- feature/dynamodb/attributevalue/LICENSE.txt | 202 ++++ feature/dynamodb/attributevalue/convert.go | 87 ++ feature/dynamodb/attributevalue/decode.go | 748 ++++++++++++++ .../dynamodb/attributevalue/decode_test.go | 725 ++++++++++++++ feature/dynamodb/attributevalue/doc.go | 97 ++ .../attributevalue/empty_collections_test.go | 938 ++++++++++++++++++ feature/dynamodb/attributevalue/encode.go | 671 +++++++++++++ .../dynamodb/attributevalue/encode_test.go | 366 +++++++ feature/dynamodb/attributevalue/field.go | 275 +++++ .../dynamodb/attributevalue/field_cache.go | 45 + feature/dynamodb/attributevalue/field_test.go | 128 +++ feature/dynamodb/attributevalue/go.mod | 18 + feature/dynamodb/attributevalue/go.sum | 38 + .../attributevalue/marshaler_examples_test.go | 94 ++ .../dynamodb/attributevalue/marshaler_test.go | 698 +++++++++++++ .../dynamodb/attributevalue/shared_test.go | 409 ++++++++ feature/dynamodb/attributevalue/tag.go | 74 ++ feature/dynamodb/attributevalue/tag_test.go | 47 + .../attributevalue/LICENSE.txt | 202 ++++ .../dynamodbstreams/attributevalue/convert.go | 87 ++ .../dynamodbstreams/attributevalue/decode.go | 748 ++++++++++++++ .../attributevalue/decode_test.go | 725 ++++++++++++++ feature/dynamodbstreams/attributevalue/doc.go | 67 ++ .../attributevalue/empty_collections_test.go | 938 ++++++++++++++++++ .../dynamodbstreams/attributevalue/encode.go | 671 +++++++++++++ .../attributevalue/encode_test.go | 366 +++++++ .../dynamodbstreams/attributevalue/field.go | 275 +++++ .../attributevalue/field_cache.go | 45 + .../attributevalue/field_test.go | 128 +++ feature/dynamodbstreams/attributevalue/go.mod | 18 + feature/dynamodbstreams/attributevalue/go.sum | 37 + .../attributevalue/marshaler_examples_test.go | 94 ++ .../attributevalue/marshaler_test.go | 698 +++++++++++++ .../attributevalue/shared_test.go | 409 ++++++++ feature/dynamodbstreams/attributevalue/tag.go | 74 ++ .../attributevalue/tag_test.go | 47 + service/dynamodb/deserializers.go | 97 +- service/dynamodb/serializers.go | 111 ++- service/dynamodb/types/types.go | 118 ++- service/dynamodbstreams/deserializers.go | 82 +- service/dynamodbstreams/types/types.go | 114 ++- 44 files changed, 11648 insertions(+), 187 deletions(-) create mode 100644 feature/dynamodb/attributevalue/LICENSE.txt create mode 100644 feature/dynamodb/attributevalue/convert.go create mode 100644 feature/dynamodb/attributevalue/decode.go create mode 100644 feature/dynamodb/attributevalue/decode_test.go create mode 100644 feature/dynamodb/attributevalue/doc.go create mode 100644 feature/dynamodb/attributevalue/empty_collections_test.go create mode 100644 feature/dynamodb/attributevalue/encode.go create mode 100644 feature/dynamodb/attributevalue/encode_test.go create mode 100644 feature/dynamodb/attributevalue/field.go create mode 100644 feature/dynamodb/attributevalue/field_cache.go create mode 100644 feature/dynamodb/attributevalue/field_test.go create mode 100644 feature/dynamodb/attributevalue/go.mod create mode 100644 feature/dynamodb/attributevalue/go.sum create mode 100644 feature/dynamodb/attributevalue/marshaler_examples_test.go create mode 100644 feature/dynamodb/attributevalue/marshaler_test.go create mode 100644 feature/dynamodb/attributevalue/shared_test.go create mode 100644 feature/dynamodb/attributevalue/tag.go create mode 100644 feature/dynamodb/attributevalue/tag_test.go create mode 100644 feature/dynamodbstreams/attributevalue/LICENSE.txt create mode 100644 feature/dynamodbstreams/attributevalue/convert.go create mode 100644 feature/dynamodbstreams/attributevalue/decode.go create mode 100644 feature/dynamodbstreams/attributevalue/decode_test.go create mode 100644 feature/dynamodbstreams/attributevalue/doc.go create mode 100644 feature/dynamodbstreams/attributevalue/empty_collections_test.go create mode 100644 feature/dynamodbstreams/attributevalue/encode.go create mode 100644 feature/dynamodbstreams/attributevalue/encode_test.go create mode 100644 feature/dynamodbstreams/attributevalue/field.go create mode 100644 feature/dynamodbstreams/attributevalue/field_cache.go create mode 100644 feature/dynamodbstreams/attributevalue/field_test.go create mode 100644 feature/dynamodbstreams/attributevalue/go.mod create mode 100644 feature/dynamodbstreams/attributevalue/go.sum create mode 100644 feature/dynamodbstreams/attributevalue/marshaler_examples_test.go create mode 100644 feature/dynamodbstreams/attributevalue/marshaler_test.go create mode 100644 feature/dynamodbstreams/attributevalue/shared_test.go create mode 100644 feature/dynamodbstreams/attributevalue/tag.go create mode 100644 feature/dynamodbstreams/attributevalue/tag_test.go diff --git a/Makefile b/Makefile index 49cc08d0247..3375201558c 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ all: generate unit ################### # Code Generation # ################### -generate: smithy-generate gen-config-asserts gen-repo-mod-replace gen-mod-dropreplace-smithy tidy-modules-. add-module-license-files gen-aws-ptrs +generate: smithy-generate gen-config-asserts copy-attributevalue-feature gen-repo-mod-replace gen-mod-dropreplace-smithy tidy-modules-. add-module-license-files gen-aws-ptrs smithy-generate: cd codegen && ./gradlew clean build -Plog-tests && ./gradlew clean @@ -110,6 +110,24 @@ gen-endpoint-prefix.json: -m '/tmp/aws-sdk-go-model-sync/models/apis/*/*/api-2.json' \ -o ${ENDPOINT_PREFIX_JSON} +copy-attributevalue-feature: + cd ./feature/dynamodbstreams/attributevalue && \ + find . -name "*.go" | grep -v "doc.go" | xargs -I % rm % && \ + find ../../dynamodb/attributevalue -name "*.go" | grep -v "doc.go" | xargs -I % cp % . && \ + ls *.go | grep -v "convert.go" | grep -v "doc.go" | \ + xargs -I % sed -i.bk -E 's:github.com/aws/aws-sdk-go-v2/(service|feature)/dynamodb:github.com/aws/aws-sdk-go-v2/\1/dynamodbstreams:g' % && \ + ls *.go | grep -v "convert.go" | grep -v "doc.go" | \ + xargs -I % sed -i.bk 's:DynamoDB:DynamoDBStreams:g' % && \ + ls *.go | grep -v "doc.go" | \ + xargs -I % sed -i.bk 's:dynamodb\.:dynamodbstreams.:g' % && \ + sed -i.bk 's:streams\.:ddbtypes.:g' "convert.go" && \ + sed -i.bk 's:ddb\.:streams.:g' "convert.go" && \ + sed -i.bk 's:ddbtypes\.:ddb.:g' "convert.go" &&\ + sed -i.bk 's:Streams::g' "convert.go" && \ + rm -rf ./*.bk && \ + gofmt -w -s . && \ + go test . + ################ # Unit Testing # diff --git a/codegen/sdk-codegen/aws-models/dynamodb-streams.2012-08-10.json b/codegen/sdk-codegen/aws-models/dynamodb-streams.2012-08-10.json index f91ed65ed2b..0c0bfac1d6c 100644 --- a/codegen/sdk-codegen/aws-models/dynamodb-streams.2012-08-10.json +++ b/codegen/sdk-codegen/aws-models/dynamodb-streams.2012-08-10.json @@ -48,7 +48,7 @@ } }, "com.amazonaws.dynamodbstreams#AttributeValue": { - "type": "structure", + "type": "union", "members": { "SS": { "target": "com.amazonaws.dynamodbstreams#StringSetAttributeValue", diff --git a/codegen/sdk-codegen/aws-models/dynamodb.2012-08-10.json b/codegen/sdk-codegen/aws-models/dynamodb.2012-08-10.json index 830b9ed8cf4..e60cf8edd65 100644 --- a/codegen/sdk-codegen/aws-models/dynamodb.2012-08-10.json +++ b/codegen/sdk-codegen/aws-models/dynamodb.2012-08-10.json @@ -141,7 +141,7 @@ } }, "com.amazonaws.dynamodb#AttributeValue": { - "type": "structure", + "type": "union", "members": { "S": { "target": "com.amazonaws.dynamodb#StringAttributeValue", diff --git a/feature/dynamodb/attributevalue/LICENSE.txt b/feature/dynamodb/attributevalue/LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/feature/dynamodb/attributevalue/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. 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 new file mode 100644 index 00000000000..6dd5a0adf55 --- /dev/null +++ b/feature/dynamodb/attributevalue/decode.go @@ -0,0 +1,748 @@ +package attributevalue + +import ( + "fmt" + "reflect" + "strconv" + "time" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +// An Unmarshaler is an interface to provide custom unmarshaling of +// AttributeValues. Use this to provide custom logic determining +// how AttributeValues should be unmarshaled. +// type ExampleUnmarshaler struct { +// Value int +// } +// +// func (u *ExampleUnmarshaler) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error { +// avN, ok := av.(*types.AttributeValueMemberN) +// if !ok { +// return nil +// } +// +// n, err := strconv.ParseInt(avN.Value, 10, 0) +// if err != nil { +// return err +// } +// +// u.Value = int(n) +// return nil +// } +type Unmarshaler interface { + UnmarshalDynamoDBAttributeValue(types.AttributeValue) error +} + +// Unmarshal will unmarshal AttributeValues to Go value types. +// Both generic interface{} and concrete types are valid unmarshal +// destination types. +// +// Unmarshal will allocate maps, slices, and pointers as needed to +// unmarshal the AttributeValue into the provided type value. +// +// When unmarshaling AttributeValues into structs Unmarshal matches +// the field names of the struct to the AttributeValue Map keys. +// Initially it will look for exact field name matching, but will +// fall back to case insensitive if not exact match is found. +// +// With the exception of omitempty, omitemptyelem, binaryset, numberset +// and stringset all struct tags used by Marshal are also used by +// Unmarshal. +// +// When decoding AttributeValues to interfaces Unmarshal will use the +// following types. +// +// []byte, AV Binary (B) +// [][]byte, AV Binary Set (BS) +// bool, AV Boolean (BOOL) +// []interface{}, AV List (L) +// map[string]interface{}, AV Map (M) +// float64, AV Number (N) +// Number, AV Number (N) with UseNumber set +// []float64, AV Number Set (NS) +// []Number, AV Number Set (NS) with UseNumber set +// string, AV String (S) +// []string, AV String Set (SS) +// +// If the Decoder option, UseNumber is set numbers will be unmarshaled +// as Number values instead of float64. Use this to maintain the original +// string formating of the number as it was represented in the AttributeValue. +// In addition provides additional opportunities to parse the number +// string based on individual use cases. +// +// When unmarshaling any error that occurs will halt the unmarshal +// and return the error. +// +// The output value provided must be a non-nil pointer +func Unmarshal(av types.AttributeValue, out interface{}) error { + return NewDecoder().Decode(av, out) +} + +// UnmarshalMap is an alias for Unmarshal which unmarshals from +// a map of AttributeValues. +// +// The output value provided must be a non-nil pointer +func UnmarshalMap(m map[string]types.AttributeValue, out interface{}) error { + return NewDecoder().Decode(&types.AttributeValueMemberM{Value: m}, out) +} + +// UnmarshalList is an alias for Unmarshal func which unmarshals +// a slice of AttributeValues. +// +// The output value provided must be a non-nil pointer +func UnmarshalList(l []types.AttributeValue, out interface{}) error { + 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 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.AttributeValueMemberM{Value: m} + } + + return UnmarshalList(items, out) +} + +// 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 + // is interface{}. Similar to encoding/json.Number + 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(optFns ...func(*DecoderOptions)) *Decoder { + var options DecoderOptions + for _, fn := range optFns { + fn(&options) + } + + return &Decoder{ + options: options, + } +} + +// Decode will unmarshal an AttributeValue into a Go value type. An error +// will be return if the decoder is unable to unmarshal the AttributeValue +// to the provide Go value type. +// +// The output value provided must be a non-nil pointer +func (d *Decoder) Decode(av 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)} + } + + return d.decode(av, v, tag{}) +} + +var stringInterfaceMapType = reflect.TypeOf(map[string]interface{}(nil)) +var byteSliceType = reflect.TypeOf([]byte(nil)) +var byteSliceSliceType = reflect.TypeOf([][]byte(nil)) +var timeType = reflect.TypeOf(time.Time{}) + +func (d *Decoder) decode(av types.AttributeValue, v reflect.Value, fieldTag tag) error { + var u Unmarshaler + _, isNull := av.(*types.AttributeValueMemberNULL) + if av == nil || isNull { + u, v = indirect(v, true) + if u != nil { + return u.UnmarshalDynamoDBAttributeValue(av) + } + return d.decodeNull(v) + } + + u, v = indirect(v, false) + if u != nil { + return u.UnmarshalDynamoDBAttributeValue(av) + } + + switch tv := av.(type) { + case *types.AttributeValueMemberB: + return d.decodeBinary(tv.Value, v) + + 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 { + if v.Kind() == reflect.Interface { + buf := make([]byte, len(b)) + copy(buf, b) + v.Set(reflect.ValueOf(buf)) + return nil + } + + if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return &UnmarshalTypeError{Value: "binary", Type: v.Type()} + } + + if v.Type() == byteSliceType { + // Optimization for []byte types + if v.IsNil() || v.Cap() < len(b) { + v.Set(reflect.MakeSlice(byteSliceType, len(b), len(b))) + } else if v.Len() != len(b) { + v.SetLen(len(b)) + } + copy(v.Interface().([]byte), b) + return nil + } + + switch v.Type().Elem().Kind() { + case reflect.Uint8: + // Fallback to reflection copy for type aliased of []byte type + if v.Kind() != reflect.Array && (v.IsNil() || v.Cap() < len(b)) { + v.Set(reflect.MakeSlice(v.Type(), len(b), len(b))) + } else if v.Len() != len(b) { + v.SetLen(len(b)) + } + for i := 0; i < len(b); i++ { + v.Index(i).SetUint(uint64(b[i])) + } + default: + if v.Kind() == reflect.Array && v.Type().Elem().Kind() == reflect.Uint8 { + reflect.Copy(v, reflect.ValueOf(b)) + break + } + return &UnmarshalTypeError{Value: "binary", Type: v.Type()} + } + + return nil +} + +func (d *Decoder) decodeBool(b bool, v reflect.Value) error { + switch v.Kind() { + case reflect.Bool, reflect.Interface: + v.Set(reflect.ValueOf(b).Convert(v.Type())) + + default: + return &UnmarshalTypeError{Value: "bool", Type: v.Type()} + } + + return nil +} + +func (d *Decoder) decodeBinarySet(bs [][]byte, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(bs) { + // What about if ignoring nil/empty values? + v.Set(reflect.MakeSlice(v.Type(), 0, len(bs))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + set := make([][]byte, len(bs)) + for i, b := range bs { + if err := d.decodeBinary(b, reflect.ValueOf(&set[i]).Elem()); err != nil { + return err + } + } + v.Set(reflect.ValueOf(set)) + return nil + default: + return &UnmarshalTypeError{Value: "binary set", Type: v.Type()} + } + + for i := 0; i < v.Cap() && i < len(bs); i++ { + if !isArray { + v.SetLen(i + 1) + } + u, elem := indirect(v.Index(i), false) + if u != nil { + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberBS{Value: bs}) + } + if err := d.decodeBinary(bs[i], elem); err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeNumber(n string, v reflect.Value, fieldTag tag) error { + switch v.Kind() { + case reflect.Interface: + i, err := d.decodeNumberToInterface(n) + if err != nil { + return err + } + v.Set(reflect.ValueOf(i)) + return nil + case reflect.String: + if isNumberValueType(v) { + v.SetString(n) + return nil + } + v.Set(reflect.ValueOf(n)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(n, 10, 64) + if err != nil { + return err + } + if v.OverflowInt(i) { + return &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, %s", n), + Type: v.Type(), + } + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + i, err := strconv.ParseUint(n, 10, 64) + if err != nil { + return err + } + if v.OverflowUint(i) { + return &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, %s", n), + Type: v.Type(), + } + } + v.SetUint(i) + case reflect.Float32, reflect.Float64: + i, err := strconv.ParseFloat(n, 64) + if err != nil { + return err + } + if v.OverflowFloat(i) { + return &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, %s", n), + Type: v.Type(), + } + } + v.SetFloat(i) + default: + if v.Type().ConvertibleTo(timeType) && fieldTag.AsUnixTime { + t, err := decodeUnixTime(n) + if err != nil { + return err + } + v.Set(reflect.ValueOf(t).Convert(v.Type())) + return nil + } + return &UnmarshalTypeError{Value: "number", Type: v.Type()} + } + + return 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) +} + +func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(ns) { + // What about if ignoring nil/empty values? + v.Set(reflect.MakeSlice(v.Type(), 0, len(ns))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + 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 { + return err + } + } + v.Set(reflect.ValueOf(set)) + } else { + set := make([]float64, len(ns)) + for i, n := range ns { + if err := d.decodeNumber(n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { + return err + } + } + v.Set(reflect.ValueOf(set)) + } + return nil + default: + return &UnmarshalTypeError{Value: "number set", Type: v.Type()} + } + + for i := 0; i < v.Cap() && i < len(ns); i++ { + if !isArray { + v.SetLen(i + 1) + } + u, elem := indirect(v.Index(i), false) + if u != nil { + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberNS{Value: ns}) + } + if err := d.decodeNumber(ns[i], elem, tag{}); err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeList(avList []types.AttributeValue, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(avList) { + // What about if ignoring nil/empty values? + v.Set(reflect.MakeSlice(v.Type(), 0, len(avList))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + s := make([]interface{}, len(avList)) + for i, av := range avList { + if err := d.decode(av, reflect.ValueOf(&s[i]).Elem(), tag{}); err != nil { + return err + } + } + v.Set(reflect.ValueOf(s)) + return nil + default: + return &UnmarshalTypeError{Value: "list", Type: v.Type()} + } + + // If v is not a slice, array + for i := 0; i < v.Cap() && i < len(avList); i++ { + if !isArray { + v.SetLen(i + 1) + } + if err := d.decode(avList[i], v.Index(i), tag{}); err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeMap(avMap map[string]types.AttributeValue, v reflect.Value) error { + switch v.Kind() { + case reflect.Map: + t := v.Type() + if t.Key().Kind() != reflect.String { + return &UnmarshalTypeError{Value: "map string key", Type: t.Key()} + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + case reflect.Interface: + v.Set(reflect.MakeMap(stringInterfaceMapType)) + v = v.Elem() + default: + return &UnmarshalTypeError{Value: "map", Type: v.Type()} + } + + if v.Kind() == reflect.Map { + for k, av := range avMap { + key := reflect.New(v.Type().Key()).Elem() + key.SetString(k) + elem := reflect.New(v.Type().Elem()).Elem() + if err := d.decode(av, elem, tag{}); err != nil { + return err + } + v.SetMapIndex(key, elem) + } + } else if v.Kind() == reflect.Struct { + fields := unionStructFields(v.Type(), structFieldOptions{ + TagKey: d.options.TagKey, + }) + for k, av := range avMap { + if f, ok := fields.FieldByName(k); ok { + fv := decoderFieldByIndex(v, f.Index) + if err := d.decode(av, fv, f.tag); err != nil { + return err + } + } + } + } + + return nil +} + +func (d *Decoder) decodeNull(v reflect.Value) error { + if v.IsValid() && v.CanSet() { + v.Set(reflect.Zero(v.Type())) + } + + return nil +} + +func (d *Decoder) decodeString(s string, v reflect.Value, fieldTag tag) error { + if fieldTag.AsString { + return d.decodeNumber(s, v, fieldTag) + } + + // To maintain backwards compatibility with ConvertFrom family of methods which + // converted strings to time.Time structs + if v.Type().ConvertibleTo(timeType) { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(t).Convert(v.Type())) + return nil + } + + switch v.Kind() { + case reflect.String: + v.SetString(s) + case reflect.Interface: + // Ensure type aliasing is handled properly + v.Set(reflect.ValueOf(s).Convert(v.Type())) + default: + return &UnmarshalTypeError{Value: "string", Type: v.Type()} + } + + return nil +} + +func (d *Decoder) decodeStringSet(ss []string, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(ss) { + v.Set(reflect.MakeSlice(v.Type(), 0, len(ss))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + set := make([]string, len(ss)) + for i, s := range ss { + if err := d.decodeString(s, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { + return err + } + } + v.Set(reflect.ValueOf(set)) + return nil + default: + return &UnmarshalTypeError{Value: "string set", Type: v.Type()} + } + + for i := 0; i < v.Cap() && i < len(ss); i++ { + if !isArray { + v.SetLen(i + 1) + } + u, elem := indirect(v.Index(i), false) + if u != nil { + return u.UnmarshalDynamoDBAttributeValue(&types.AttributeValueMemberSS{Value: ss}) + } + if err := d.decodeString(ss[i], elem, tag{}); err != nil { + return err + } + } + + return nil +} + +func decodeUnixTime(n string) (time.Time, error) { + v, err := strconv.ParseInt(n, 10, 64) + if err != nil { + return time.Time{}, &UnmarshalError{ + Err: err, Value: n, Type: timeType, + } + } + + return time.Unix(v, 0), nil +} + +// decoderFieldByIndex finds the field with the provided nested index, allocating +// embedded parent structs if needed +func decoderFieldByIndex(v reflect.Value, index []int) reflect.Value { + for i, x := range index { + if i > 0 && v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + v = v.Field(x) + } + return v +} + +// indirect will walk a value's interface or pointer value types. Returning +// the final value or the value a unmarshaler is defined on. +// +// Based on the enoding/json type reflect value type indirection in Go Stdlib +// https://golang.org/src/encoding/json/decode.go indirect func. +func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, reflect.Value) { + if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + for { + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { + v = e + continue + } + } + if v.Kind() != reflect.Ptr { + break + } + if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, reflect.Value{} + } + } + v = v.Elem() + } + + return nil, v +} + +// A Number represents a Attributevalue number literal. +type Number string + +// Float64 attempts to cast the number ot a float64, returning +// the result of the case or error if the case failed. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 attempts to cast the number ot a int64, returning +// the result of the case or error if the case failed. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// Uint64 attempts to cast the number ot a uint64, returning +// the result of the case or error if the case failed. +func (n Number) Uint64() (uint64, error) { + return strconv.ParseUint(string(n), 10, 64) +} + +// String returns the raw number represented as a string +func (n Number) String() string { + return string(n) +} + +// 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 { + Value string + Type reflect.Type +} + +// Error returns the string representation of the error. +// satisfying the error interface +func (e *UnmarshalTypeError) Error() string { + return fmt.Sprintf("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 { + Type reflect.Type +} + +// Error returns the string representation of the error. +// satisfying the error interface +func (e *InvalidUnmarshalError) Error() string { + var msg string + if e.Type == nil { + 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()) + } + + return fmt.Sprintf("unmarshal failed, %s", msg) +} + +// 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 +} + +func (e *UnmarshalError) Unwrap() error { + return e.Err +} + +// Error returns the string representation of the error satisfying the error +// interface. +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 new file mode 100644 index 00000000000..38260b075ff --- /dev/null +++ b/feature/dynamodb/attributevalue/decode_test.go @@ -0,0 +1,725 @@ +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 TestUnmarshalShared(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) + }) + } +} + +func TestUnmarshal(t *testing.T) { + cases := []struct { + in types.AttributeValue + actual, expected interface{} + err error + }{ + //------------ + // Sets + //------------ + { + in: &types.AttributeValueMemberBS{Value: [][]byte{ + {48, 49}, {50, 51}, + }}, + actual: &[][]byte{}, + expected: [][]byte{{48, 49}, {50, 51}}, + }, + { + in: &types.AttributeValueMemberNS{Value: []string{ + "123", "321", + }}, + actual: &[]int{}, + expected: []int{123, 321}, + }, + { + in: &types.AttributeValueMemberNS{Value: []string{ + "123", "321", + }}, + actual: &[]interface{}{}, + expected: []interface{}{123., 321.}, + }, + { + in: &types.AttributeValueMemberSS{Value: []string{ + "abc", "123", + }}, + actual: &[]string{}, + expected: &[]string{"abc", "123"}, + }, + { + in: &types.AttributeValueMemberSS{Value: []string{ + "abc", "123", + }}, + actual: &[]*string{}, + expected: &[]*string{aws.String("abc"), aws.String("123")}, + }, + //------------ + // Interfaces + //------------ + { + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: []byte{48, 49}, + }, + { + in: &types.AttributeValueMemberBS{Value: [][]byte{ + {48, 49}, {50, 51}, + }}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: [][]byte{{48, 49}, {50, 51}}, + }, + { + in: &types.AttributeValueMemberBOOL{Value: true}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: bool(true), + }, + { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberS{Value: "123"}, + }}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: []interface{}{"abc", "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{} + return &v + }(), + expected: map[string]interface{}{"123": "abc", "abc": "123"}, + }, + { + in: &types.AttributeValueMemberN{Value: "123"}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: float64(123), + }, + { + in: &types.AttributeValueMemberNS{Value: []string{ + "123", "321", + }}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: []float64{123., 321.}, + }, + { + in: &types.AttributeValueMemberS{Value: "123"}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: "123", + }, + { + 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{} { + var v interface{} + return &v + }(), + expected: []string{"123", "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.AttributeValueMemberN{Value: "512"}, + actual: new(uint8), + err: &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, 512"), + Type: reflect.TypeOf(uint8(0)), + }, + }, + // ------- + // Empty Values + // ------- + { + in: &types.AttributeValueMemberB{Value: []byte{}}, + actual: &[]byte{}, + expected: []byte{}, + }, + { + in: &types.AttributeValueMemberBS{Value: [][]byte{}}, + actual: &[][]byte{}, + expected: [][]byte{}, + }, + { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{}}, + actual: &[]interface{}{}, + expected: []interface{}{}, + }, + { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + actual: &map[string]interface{}{}, + expected: map[string]interface{}{}, + }, + { + in: &types.AttributeValueMemberN{Value: ""}, + actual: new(int), + err: fmt.Errorf("invalid syntax"), + }, + { + in: &types.AttributeValueMemberNS{Value: []string{}}, + actual: &[]string{}, + expected: []string{}, + }, + { + in: &types.AttributeValueMemberS{Value: ""}, + actual: new(string), + expected: "", + }, + { + in: &types.AttributeValueMemberSS{Value: []string{}}, + actual: &[]string{}, + expected: []string{}, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("case %d/%d", i, len(cases)), func(t *testing.T) { + err := Unmarshal(c.in, c.actual) + assertConvertTest(t, c.actual, c.expected, err, c.err) + }) + } +} + +func TestInterfaceInput(t *testing.T) { + var v interface{} + expected := []interface{}{"abc", "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 := map[string]struct { + in types.AttributeValue + actual, expected interface{} + err error + }{ + "invalid unmarshal": { + in: nil, + actual: int(0), + expected: nil, + err: &InvalidUnmarshalError{Type: reflect.TypeOf(int(0))}, + }, + } + + 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) + }) + } +} + +func TestUnmarshalListShared(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) + }) + } +} + +func TestUnmarshalListError(t *testing.T) { + cases := map[string]struct { + in []types.AttributeValue + actual, expected interface{} + err error + }{ + "invalid unmarshal": { + in: []types.AttributeValue{}, + actual: []interface{}{}, + expected: nil, + err: &InvalidUnmarshalError{Type: reflect.TypeOf([]interface{}{})}, + }, + } + + 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) + }) + } +} + +func TestUnmarshalMapShared(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) + }) + } +} + +func TestUnmarshalMapError(t *testing.T) { + cases := []struct { + in map[string]types.AttributeValue + actual, expected interface{} + err error + }{ + { + in: map[string]types.AttributeValue{}, + actual: map[string]interface{}{}, + expected: nil, + err: &InvalidUnmarshalError{Type: reflect.TypeOf(map[string]interface{}{})}, + }, + { + in: map[string]types.AttributeValue{ + "BOOL": &types.AttributeValueMemberBOOL{Value: true}, + }, + actual: &map[int]interface{}{}, + expected: nil, + err: &UnmarshalTypeError{Value: "map string key", Type: reflect.TypeOf(int(0))}, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + err := UnmarshalMap(c.in, c.actual) + assertConvertTest(t, c.actual, c.expected, err, c.err) + }) + } +} + +func TestUnmarshalListOfMaps(t *testing.T) { + type testItem struct { + Value string + Value2 int + } + + cases := map[string]struct { + in []map[string]types.AttributeValue + actual, expected interface{} + err error + }{ + "simple map conversion": { + in: []map[string]types.AttributeValue{ + { + "Value": &types.AttributeValueMemberBOOL{Value: true}, + }, + }, + actual: &[]map[string]interface{}{}, + expected: []map[string]interface{}{ + { + "Value": true, + }, + }, + }, + "attribute to struct": { + in: []map[string]types.AttributeValue{ + { + "Value": &types.AttributeValueMemberS{Value: "abc"}, + "Value2": &types.AttributeValueMemberN{Value: "123"}, + }, + }, + actual: &[]testItem{}, + expected: []testItem{ + { + Value: "abc", + Value2: 123, + }, + }, + }, + } + + 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) + }) + } +} + +type unmarshalUnmarshaler struct { + Value string + Value2 int + Value3 bool + Value4 time.Time +} + +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 := m.Value["abc"]; !ok { + return fmt.Errorf("expected `abc` map key") + } else if vv, kk := v.(*types.AttributeValueMemberS); !kk || vv == nil { + return fmt.Errorf("expected `abc` map value string") + } else { + u.Value = vv.Value + } + + if v, ok := m.Value["def"]; !ok { + return fmt.Errorf("expected `def` map key") + } else if vv, kk := v.(*types.AttributeValueMemberN); !kk || vv == nil { + return fmt.Errorf("expected `def` map value number") + } else { + n, err := strconv.ParseInt(vv.Value, 10, 64) + if err != nil { + return err + } + u.Value2 = int(n) + } + + if v, ok := m.Value["ghi"]; !ok { + return fmt.Errorf("expected `ghi` map key") + } else if vv, kk := v.(*types.AttributeValueMemberBOOL); !kk || vv == nil { + return fmt.Errorf("expected `ghi` map value number") + } else { + u.Value3 = vv.Value + } + + if v, ok := m.Value["jkl"]; !ok { + return fmt.Errorf("expected `jkl` map key") + } 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, vv.Value) + if err != nil { + return err + } + u.Value4 = t + } + + return nil +} + +func TestUnmarshalUnmashaler(t *testing.T) { + u := &unmarshalUnmarshaler{} + 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"}, + }, + } + + err := Unmarshal(av, u) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + if e, a := "value", u.Value; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := 123, u.Value2; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := true, u.Value3; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := testDate, u.Value4; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeUseNumber(t *testing.T) { + u := map[string]interface{}{} + 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(o *DecoderOptions) { + o.UseNumber = true + }) + err := decoder.Decode(av, &u) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + if e, a := "value", u["abc"]; e != a { + t.Errorf("expect %v, got %v", e, a) + } + n := u["def"].(Number) + if e, a := "123", n.String(); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := true, u["ghi"]; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeUseNumberNumberSet(t *testing.T) { + u := map[string]interface{}{} + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "ns": &types.AttributeValueMemberNS{ + Value: []string{ + "123", "321", + }, + }, + }, + } + + decoder := NewDecoder(func(o *DecoderOptions) { + o.UseNumber = true + }) + err := decoder.Decode(av, &u) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + ns := u["ns"].([]Number) + + if e, a := "123", ns[0].String(); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "321", ns[1].String(); e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeEmbeddedPointerStruct(t *testing.T) { + type B struct { + Bint int + } + type C struct { + Cint int + } + type A struct { + Aint int + *B + *C + } + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Aint": &types.AttributeValueMemberN{Value: "321"}, + "Bint": &types.AttributeValueMemberN{Value: "123"}, + }, + } + decoder := NewDecoder() + a := A{} + err := decoder.Decode(av, &a) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if e, a := 321, a.Aint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + // Embedded pointer struct can be created automatically. + if e, a := 123, a.Bint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + // But not for absent fields. + if a.C != nil { + t.Errorf("expect nil, got %v", a.C) + } +} + +func TestDecodeBooleanOverlay(t *testing.T) { + type BooleanOverlay bool + + av := &types.AttributeValueMemberBOOL{Value: true} + + decoder := NewDecoder() + + var v BooleanOverlay + + err := decoder.Decode(av, &v) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if e, a := BooleanOverlay(true), v; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeUnixTime(t *testing.T) { + type A struct { + Normal time.Time + Tagged time.Time `dynamodbav:",unixtime"` + Typed UnixTime + } + + expect := A{ + Normal: time.Unix(123, 0).UTC(), + Tagged: time.Unix(456, 0), + Typed: UnixTime(time.Unix(789, 0)), + } + + input := &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{} + + err := Unmarshal(input, &actual) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if e, a := expect, actual; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeAliasedUnixTime(t *testing.T) { + type A struct { + Normal AliasedTime + Tagged AliasedTime `dynamodbav:",unixtime"` + } + + expect := A{ + Normal: AliasedTime(time.Unix(123, 0).UTC()), + Tagged: AliasedTime(time.Unix(456, 0)), + } + + input := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Normal": &types.AttributeValueMemberS{Value: "1970-01-01T00:02:03Z"}, + "Tagged": &types.AttributeValueMemberN{Value: "456"}, + }, + } + actual := A{} + + err := Unmarshal(input, &actual) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if expect != actual { + t.Errorf("expect %v, got %v", expect, actual) + } +} + +// see github issue #1594 +func TestDecodeArrayType(t *testing.T) { + cases := []struct { + to, from interface{} + }{ + { + &[2]int{1, 2}, + &[2]int{}, + }, + { + &[2]int64{1, 2}, + &[2]int64{}, + }, + { + &[2]byte{1, 2}, + &[2]byte{}, + }, + { + &[2]bool{true, false}, + &[2]bool{}, + }, + { + &[2]string{"1", "2"}, + &[2]string{}, + }, + { + &[2][]string{{"1", "2"}}, + &[2][]string{}, + }, + } + + for _, c := range cases { + marshaled, err := Marshal(c.to) + if err != nil { + t.Errorf("expected no error, but received %v", err) + } + + if err = Unmarshal(marshaled, c.from); err != nil { + t.Errorf("expected no error, but received %v", err) + } + + if diff := cmp.Diff(c.to, c.from); len(diff) != 0 { + t.Errorf("expected match\n:%s", diff) + } + } +} + +func TestDecoderFieldByIndex(t *testing.T) { + type ( + Middle struct{ Inner int } + Outer struct{ *Middle } + ) + var outer Outer + + outerType := reflect.TypeOf(outer) + outerValue := reflect.ValueOf(&outer) + outerFields := unionStructFields(outerType, structFieldOptions{}) + innerField, _ := outerFields.FieldByName("Inner") + + f := decoderFieldByIndex(outerValue.Elem(), innerField.Index) + if outer.Middle == nil { + t.Errorf("expected outer.Middle to be non-nil") + } + if f.Kind() != reflect.Int || f.Int() != int64(outer.Inner) { + t.Error("expected f to be an int with value equal to outer.Inner") + } +} diff --git a/feature/dynamodb/attributevalue/doc.go b/feature/dynamodb/attributevalue/doc.go new file mode 100644 index 00000000000..25b4020e5b7 --- /dev/null +++ b/feature/dynamodb/attributevalue/doc.go @@ -0,0 +1,97 @@ +// Package attributevalue provides marshaling and unmarshaling utilities to +// convert between Go types and Amazon DynamoDB AttributeValues. +// +// 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 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 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 +// URLs []string +// } +// +// //... +// +// r := Record{ +// ID: "ABC123", +// URLs: []string{ +// "https://example.com/first/link", +// "https://example.com/second/url", +// }, +// } +// av, err := attributevalue.MarshalMap(r) +// if err != nil { +// return fmt.Errorf("failed to marshal Record, %w", err) +// } +// +// _, err = client.PutItem(context.TODO(), &dynamodb.PutItemInput{ +// TableName: aws.String(myTableName), +// Item: av, +// }) +// if err != nil { +// return fmt.Errorf("failed to put Record, %w", err) +// } +// +// AttributeValue Unmarshaling +// +// 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 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 +// URLs []string +// } +// +// //... +// +// result, err := client.Scan(context.Context(), &dynamodb.ScanInput{ +// TableName: aws.String(myTableName), +// }) +// 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)) +// } +// +// Struct tags +// +// 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. +// +// 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 new file mode 100644 index 00000000000..e22aee0e8e5 --- /dev/null +++ b/feature/dynamodb/attributevalue/empty_collections_test.go @@ -0,0 +1,938 @@ +package attributevalue + +import ( + "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 { + String string + + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + + Float32 float32 + Float64 float64 +} + +type testEmptyCollectionsOmittedNumericalScalars struct { + String string `dynamodbav:",omitempty"` + + Uint8 uint8 `dynamodbav:",omitempty"` + Uint16 uint16 `dynamodbav:",omitempty"` + Uint32 uint32 `dynamodbav:",omitempty"` + Uint64 uint64 `dynamodbav:",omitempty"` + + Int8 int8 `dynamodbav:",omitempty"` + Int16 int16 `dynamodbav:",omitempty"` + Int32 int32 `dynamodbav:",omitempty"` + Int64 int64 `dynamodbav:",omitempty"` + + Float32 float32 `dynamodbav:",omitempty"` + 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 + + PtrUint8 *uint8 + PtrUint16 *uint16 + PtrUint32 *uint32 + PtrUint64 *uint64 + + PtrInt8 *int8 + PtrInt16 *int16 + PtrInt32 *int32 + PtrInt64 *int64 + + PtrFloat32 *float32 + PtrFloat64 *float64 +} + +type testEmptyCollectionsOmittedPtrNumericalScalars struct { + PtrString *string `dynamodbav:",omitempty"` + + PtrUint8 *uint8 `dynamodbav:",omitempty"` + PtrUint16 *uint16 `dynamodbav:",omitempty"` + PtrUint32 *uint32 `dynamodbav:",omitempty"` + PtrUint64 *uint64 `dynamodbav:",omitempty"` + + PtrInt8 *int8 `dynamodbav:",omitempty"` + PtrInt16 *int16 `dynamodbav:",omitempty"` + PtrInt32 *int32 `dynamodbav:",omitempty"` + PtrInt64 *int64 `dynamodbav:",omitempty"` + + PtrFloat32 *float32 `dynamodbav:",omitempty"` + 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 + ByteSlice []byte + ByteArray [4]byte + ZeroArray [0]byte + BinarySet [][]byte `dynamodbav:",binaryset"` + NumberSet []int `dynamodbav:",numberset"` + StringSet []string `dynamodbav:",stringset"` +} + +type testEmptyCollectionTypesOmitted struct { + Map map[string]string `dynamodbav:",omitempty"` + Slice []string `dynamodbav:",omitempty"` + ByteSlice []byte `dynamodbav:",omitempty"` + ByteArray [4]byte `dynamodbav:",omitempty"` + ZeroArray [0]byte `dynamodbav:",omitempty"` + BinarySet [][]byte `dynamodbav:",binaryset,omitempty"` + NumberSet []int `dynamodbav:",numberset,omitempty"` + 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 +} + +type testEmptyCollectionStructOmitted struct { + Slice []string `dynamodbav:",omitempty"` +} + +var sharedEmptyCollectionsTestCases = map[string]struct { + 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.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.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{}, + expected: testEmptyCollectionsNumericalScalars{ + 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, + }, + }, + "omittable scalars with zero value": { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + actual: &testEmptyCollectionsOmittedNumericalScalars{}, + expected: testEmptyCollectionsOmittedNumericalScalars{}, + }, + "omittable 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: &testEmptyCollectionsOmittedNumericalScalars{}, + expected: testEmptyCollectionsOmittedNumericalScalars{ + 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, + }, + }, + "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.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.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{}, + expected: testEmptyCollectionsPtrScalars{ + 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), + }, + }, + "pointer scalars non-nil non-zero": { + 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{}, + expected: testEmptyCollectionsPtrScalars{ + PtrString: aws.String("test string"), + 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), + }, + }, + "omittable nil pointer scalars": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{}, + }, + actual: &testEmptyCollectionsOmittedPtrNumericalScalars{}, + expected: testEmptyCollectionsOmittedPtrNumericalScalars{}, + }, + "omittable non-nil pointer to scalars with zero value": { + 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{}, + expected: testEmptyCollectionsOmittedPtrNumericalScalars{ + 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), + }, + }, + "omittable non-nil pointer to non-zero scalar": { + 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{}, + expected: testEmptyCollectionsOmittedPtrNumericalScalars{ + 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 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)}, + "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.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{}, + expected: testEmptyCollectionTypes{ + Map: map[string]string{}, + Slice: []string{}, + ByteSlice: []byte{}, + ByteArray: [4]byte{}, + ZeroArray: [0]byte{}, + BinarySet: [][]byte{}, + NumberSet: []int{}, + StringSet: []string{}, + }, + }, + "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}}, + "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{}, + expected: testEmptyCollectionTypes{ + 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"}, + }, + }, + "omittable maps slices nil values": { + 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.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{}, + expected: testEmptyCollectionTypesOmitted{ + Map: map[string]string{}, + Slice: []string{}, + ByteSlice: []byte{}, + ByteArray: [4]byte{}, + BinarySet: [][]byte{}, + NumberSet: []int{}, + StringSet: []string{}, + }, + }, + "omittable 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"}}, + }, + }, + actual: &testEmptyCollectionTypesOmitted{}, + expected: testEmptyCollectionTypesOmitted{ + 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"}, + }, + }, + "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.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "0"}, + }, + }, + "PtrStruct": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{}, + expected: struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{}, + }, + "structs with members non-zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "1"}, + }, + }, + "PtrStruct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "1"}, + }, + }, + }, + }, + actual: &struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{}, + expected: struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{ + Struct: testEmptyCollectionStruct{Int: 1}, + PtrStruct: &testEmptyCollectionStruct{Int: 1}, + }, + }, + "struct with omittable members zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + "PtrStruct": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &struct { + Struct testEmptyCollectionStructOmitted + PtrStruct *testEmptyCollectionStructOmitted + }{}, + expected: struct { + Struct testEmptyCollectionStructOmitted + PtrStruct *testEmptyCollectionStructOmitted + }{}, + }, + "omittable struct with omittable members zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + }, + }, + actual: &struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + PtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{}, + expected: struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + PtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{}, + }, + "omittable struct with omittable members non-zero value": { + 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": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "test"}, + }}, + }, + }, + }, + }, + actual: &struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + InitPtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{}, + expected: struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + InitPtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{ + Struct: testEmptyCollectionStructOmitted{Slice: []string{"test"}}, + InitPtrStruct: &testEmptyCollectionStructOmitted{Slice: []string{"test"}}, + }, + }, +} + +func TestMarshalEmptyCollections(t *testing.T) { + for name, c := range sharedEmptyCollectionsTestCases { + t.Run(name, func(t *testing.T) { + av, err := Marshal(c.expected) + in := c.in + if c.inMarshal != nil { + in = c.inMarshal + } + assertConvertTest(t, av, in, err, c.err) + }) + } +} + +func TestEmptyCollectionsSpecialCases(t *testing.T) { + // ptr string non nil with empty value + + type SpecialCases struct { + PtrString *string + OmittedString string `dynamodbav:",omitempty"` + OmittedPtrString *string `dynamodbav:",omitempty"` + } + + expectedEncode := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberS{Value: ""}, + }, + } + expectedDecode := SpecialCases{} + + actualEncode, err := Marshal(&SpecialCases{ + PtrString: aws.String(""), + OmittedString: "", + OmittedPtrString: nil, + }) + if err != nil { + t.Fatalf("expected no err got %v", err) + } + if diff := cmp.Diff(expectedEncode, actualEncode); len(diff) != 0 { + t.Errorf("expected encode match\n%s", diff) + } + + var actualDecode SpecialCases + var av types.AttributeValue + err = Unmarshal(av, &actualDecode) + if err != nil { + t.Fatalf("expected no err got %v", err) + } + if diff := cmp.Diff(expectedDecode, actualDecode); len(diff) != 0 { + t.Errorf("expected dencode match\n%s", diff) + } +} + +func TestUnmarshalEmptyCollections(t *testing.T) { + for name, c := range sharedEmptyCollectionsTestCases { + t.Run(name, func(t *testing.T) { + err := Unmarshal(c.in, c.actual) + assertConvertTest(t, c.actual, c.expected, err, c.err) + }) + } +} diff --git a/feature/dynamodb/attributevalue/encode.go b/feature/dynamodb/attributevalue/encode.go new file mode 100644 index 00000000000..56e1db53d2b --- /dev/null +++ b/feature/dynamodb/attributevalue/encode.go @@ -0,0 +1,671 @@ +package attributevalue + +import ( + "fmt" + "reflect" + "strconv" + "time" + + "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 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 +// want to have your time value marshaled as Unix time in seconds into a number +// attribute type instead of the default time.RFC3339Nano. +// +// 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. +// +// Also, important to note: the default UnixTime implementation of the Marshaler +// interface will marshal into an attribute of type of number; therefore, +// it may not be used as a sort key if the attribute value is of type string. Further, +// the time.RFC3339Nano format removes trailing zeros from the seconds field +// and thus may not sort correctly once formatted. +type UnixTime time.Time + +// MarshalDynamoDBAttributeValue implements the Marshaler interface so that +// 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() (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 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 { + 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 + } + + *e = UnixTime(t) + return nil +} + +// A Marshaler is an interface to provide custom marshaling of Go value types +// to AttributeValues. Use this to provide custom logic determining how a +// Go Value type should be marshaled. +// +// type CustomIntType struct { +// Value Int +// } +// func (m *CustomIntType) MarshalDynamoDBAttributeValue() (types.AttributeValue, error) { +// return &types.AttributeValueMemberN{ +// Value: strconv.Itoa(m.Value), +// }, nil +// } +// +type Marshaler interface { + MarshalDynamoDBAttributeValue() (types.AttributeValue, error) +} + +// 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 +// 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. +// +// 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) { + return NewEncoder().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. +// +// This is useful for APIs such as PutItem. +func MarshalMap(in interface{}) (map[string]types.AttributeValue, error) { + av, err := NewEncoder().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. +func MarshalList(in interface{}) ([]types.AttributeValue, error) { + av, err := NewEncoder().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 { + // 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 { + options EncoderOptions +} + +// NewEncoder creates a new Encoder with default configuration. Use +// the `opts` functional options to override the default configuration. +func NewEncoder(optFns ...func(*EncoderOptions)) *Encoder { + options := EncoderOptions{ + NullEmptySets: true, + } + for _, fn := range optFns { + fn(&options) + } + + 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) { + return e.encode(reflect.ValueOf(in), tag{}) +} + +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 av, err := tryMarshaler(v); err != nil { + return nil, err + } else if av != nil { + return av, nil + } + } + + switch v.Kind() { + case reflect.Invalid: + 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(v, fieldTag) + + case reflect.Map: + return e.encodeMap(v, fieldTag) + + case reflect.Slice, reflect.Array: + return e.encodeSlice(v, fieldTag) + + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + // skip unsupported types + return nil, nil + + default: + return e.encodeScalar(v, fieldTag) + } +} + +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() + } + return &types.AttributeValueMemberS{Value: t.Format(time.RFC3339Nano)}, nil + } + + m := &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}} + fields := unionStructFields(v.Type(), structFieldOptions{ + TagKey: e.options.TagKey, + }) + for _, f := range fields.All() { + if f.Name == "" { + return nil, &InvalidMarshalError{msg: "map key cannot be empty"} + } + + fv, found := encoderFieldByIndex(v, f.Index) + if !found { + continue + } + + elem, err := e.encode(fv, f.tag) + if err != nil { + return nil, err + } else if elem == nil { + continue + } + + m.Value[f.Name] = elem + } + + return m, nil +} + +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 nil, &InvalidMarshalError{msg: "map key cannot be empty"} + } + + elemVal := v.MapIndex(key) + elem, err := e.encode(elemVal, tag{ + OmitEmpty: fieldTag.OmitEmptyElem, + NullEmpty: fieldTag.NullEmptyElem, + }) + if err != nil { + return nil, err + } else if elem == nil { + continue + } + + m.Value[keyName] = elem + } + + return m, 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 + } + + 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 + } + + } else if fieldTag.AsNumSet { // Number Set + if v.Len() == 0 && e.options.NullEmptySets { + return encodeNull(), nil + } + + 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 + } + + } else if fieldTag.AsStrSet { // String Set + if v.Len() == 0 && e.options.NullEmptySets { + return encodeNull(), 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 + } + + } 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 + } + } + + if err := e.encodeListElems(v, fieldTag, setElemFn); err != nil { + return nil, err + } + + return av, nil +} + +func (e *Encoder) encodeListElems(v reflect.Value, fieldTag tag, setElem func(types.AttributeValue) error) error { + for i := 0; i < v.Len(); i++ { + elem, err := e.encode(v.Index(i), tag{ + OmitEmpty: fieldTag.OmitEmptyElem, + NullEmpty: fieldTag.NullEmptyElem, + }) + if err != nil { + return err + } else if elem == nil { + continue + } + + if err := setElem(elem); err != nil { + return err + } + } + + return nil +} + +// 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 { + return &types.AttributeValueMemberS{Value: v.String()}, nil + } + return &types.AttributeValueMemberN{Value: v.String()}, nil + } + + switch v.Kind() { + case reflect.Bool: + return &types.AttributeValueMemberBOOL{Value: v.Bool()}, nil + + case reflect.String: + return e.encodeString(v) + + default: + // Fallback to encoding numbers, will return invalid type if not supported + av, err := e.encodeNumber(v) + if err != nil { + return nil, err + } + + n, isNumber := av.(*types.AttributeValueMemberN) + if fieldTag.AsString && isNumber { + return &types.AttributeValueMemberS{Value: n.Value}, nil + } + return av, nil + } +} + +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 nil, nil + } + + return &types.AttributeValueMemberN{Value: out}, nil +} + +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() + return &types.AttributeValueMemberS{Value: s}, nil + + default: + return nil, nil + } +} + +func encodeInt(i int64) string { + return strconv.FormatInt(i, 10) +} +func encodeUint(u uint64) string { + return strconv.FormatUint(u, 10) +} +func encodeFloat(f float64, bitSize int) string { + return strconv.FormatFloat(f, 'f', -1, bitSize) +} +func encodeNull() types.AttributeValue { + return &types.AttributeValueMemberNULL{Value: true} +} + +// encoderFieldByIndex finds the field with the provided nested index +func encoderFieldByIndex(v reflect.Value, index []int) (reflect.Value, bool) { + for i, x := range index { + if i > 0 && v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { + if v.IsNil() { + return reflect.Value{}, false + } + v = v.Elem() + } + v = v.Field(x) + } + return v, true +} + +func valueElem(v reflect.Value) reflect.Value { + switch v.Kind() { + case reflect.Interface, reflect.Ptr: + for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr { + v = v.Elem() + } + } + + return v +} + +func 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: + return v.IsNil() + case reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func 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 nil, nil + } + + if m, ok := v.Interface().(Marshaler); ok { + return m.MarshalDynamoDBAttributeValue() + } + + 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 { + msg string +} + +// Error returns the string representation of the error. +// satisfying the error interface +func (e *InvalidMarshalError) Error() string { + return fmt.Sprintf("marshal failed, %s", e.msg) +} diff --git a/feature/dynamodb/attributevalue/encode_test.go b/feature/dynamodb/attributevalue/encode_test.go new file mode 100644 index 00000000000..f07e4e9ffdc --- /dev/null +++ b/feature/dynamodb/attributevalue/encode_test.go @@ -0,0 +1,366 @@ +package attributevalue + +import ( + "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 name, c := range sharedTestCases { + t.Run(name, func(t *testing.T) { + av, err := Marshal(c.expected) + assertConvertTest(t, av, c.in, err, c.err) + }) + } +} + +func TestMarshalListShared(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) + }) + } +} + +func TestMarshalMapShared(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) + }) + } +} + +type marshalMarshaler struct { + Value string + Value2 int + Value3 bool + Value4 time.Time +} + +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) { + m := &marshalMarshaler{ + Value: "value", + Value2: 123, + Value3: true, + Value4: testDate, + } + + 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"}, + }, + } + + 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) + } +} + +type testOmitEmptyElemListStruct struct { + Values []string `dynamodbav:",omitemptyelem"` +} + +type testOmitEmptyElemMapStruct struct { + Values map[string]interface{} `dynamodbav:",omitemptyelem"` +} + +func TestMarshalListOmitEmptyElem(t *testing.T) { + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Values": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberS{Value: "123"}, + }}, + }, + } + + m := testOmitEmptyElemListStruct{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 TestMarshalMapOmitEmptyElem(t *testing.T) { + 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"}, + }}, + }, + } + + m := testOmitEmptyElemMapStruct{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) + } +} + +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) + } +} + +type testOmitEmptyScalar struct { + IntZero int `dynamodbav:",omitempty"` + IntPtrNil *int `dynamodbav:",omitempty"` + IntPtrSetZero *int `dynamodbav:",omitempty"` +} + +func TestMarshalOmitEmpty(t *testing.T) { + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "IntPtrSetZero": &types.AttributeValueMemberN{Value: "0"}, + }, + } + + m := testOmitEmptyScalar{IntPtrSetZero: aws.Int(0)} + + 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) + } +} + +func TestEncodeEmbeddedPointerStruct(t *testing.T) { + type B struct { + Bint int + } + type C struct { + Cint int + } + type A struct { + Aint int + *B + *C + } + a := A{Aint: 321, B: &B{123}} + if e, a := 321, a.Aint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := 123, a.Bint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if a.C != nil { + t.Errorf("expect nil, got %v", a.C) + } + + actual, err := Marshal(a) + if err != nil { + t.Errorf("expect nil, got %v", err) + } + 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) { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestEncodeUnixTime(t *testing.T) { + type A struct { + Normal time.Time + Tagged time.Time `dynamodbav:",unixtime"` + Typed UnixTime + } + + a := A{ + Normal: time.Unix(123, 0).UTC(), + Tagged: time.Unix(456, 0), + Typed: UnixTime(time.Unix(789, 0)), + } + + actual, err := Marshal(a) + if err != nil { + t.Errorf("expect nil, got %v", err) + } + 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) { + t.Errorf("expect %v, got %v", e, a) + } +} + +type AliasedTime time.Time + +func TestEncodeAliasedUnixTime(t *testing.T) { + type A struct { + Normal AliasedTime + Tagged AliasedTime `dynamodbav:",unixtime"` + } + + a := A{ + Normal: AliasedTime(time.Unix(123, 0).UTC()), + Tagged: AliasedTime(time.Unix(456, 0)), + } + + actual, err := Marshal(a) + if err != nil { + t.Errorf("expect no err, got %v", err) + } + 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) { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestEncoderFieldByIndex(t *testing.T) { + type ( + Middle struct{ Inner int } + Outer struct{ *Middle } + ) + + // nil embedded struct + outer := Outer{} + outerFields := unionStructFields(reflect.TypeOf(outer), structFieldOptions{}) + innerField, _ := outerFields.FieldByName("Inner") + + _, found := encoderFieldByIndex(reflect.ValueOf(&outer).Elem(), innerField.Index) + if found != false { + t.Error("expected found to be false when embedded struct is nil") + } + + // non-nil embedded struct + outer = Outer{Middle: &Middle{Inner: 3}} + outerFields = unionStructFields(reflect.TypeOf(outer), structFieldOptions{}) + innerField, _ = outerFields.FieldByName("Inner") + + f, found := encoderFieldByIndex(reflect.ValueOf(&outer).Elem(), innerField.Index) + if !found { + t.Error("expected found to be true") + } + if f.Kind() != reflect.Int || f.Int() != int64(outer.Inner) { + t.Error("expected f to be of kind Int with value equal to outer.Inner") + } +} diff --git a/feature/dynamodb/attributevalue/field.go b/feature/dynamodb/attributevalue/field.go new file mode 100644 index 00000000000..7abd3479a96 --- /dev/null +++ b/feature/dynamodb/attributevalue/field.go @@ -0,0 +1,275 @@ +package attributevalue + +import ( + "reflect" + "sort" +) + +type field struct { + tag + + Name string + NameFromTag bool + + Index []int + Type reflect.Type +} + +func buildField(pIdx []int, i int, sf reflect.StructField, fieldTag tag) field { + f := field{ + Name: sf.Name, + Type: sf.Type, + tag: fieldTag, + } + if len(fieldTag.Name) != 0 { + f.NameFromTag = true + f.Name = fieldTag.Name + } + + f.Index = make([]int, len(pIdx)+1) + copy(f.Index, pIdx) + f.Index[len(pIdx)] = i + + return f +} + +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 +} + +// unionStructFields returns a list of fields for the given type. Type info is cached +// to avoid repeated calls into the reflect package +func unionStructFields(t reflect.Type, opts structFieldOptions) *cachedFields { + if cached, ok := fieldCache.Load(t); ok { + return cached + } + + f := enumFields(t, opts) + sort.Sort(fieldsByName(f)) + f = visibleFields(f) + + fs := &cachedFields{ + fields: f, + fieldsByName: make(map[string]int, len(f)), + } + for i, f := range fs.fields { + fs.fieldsByName[f.Name] = i + } + + cached, _ := fieldCache.LoadOrStore(t, fs) + return cached +} + +// enumFields will recursively iterate through a structure and its nested +// anonymous fields. +// +// Based on the enoding/json struct field enumeration of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go typeField func. +func enumFields(t reflect.Type, opts structFieldOptions) []field { + // Fields to explore + current := []field{} + next := []field{{Type: t}} + + // count of queued names + count := map[reflect.Type]int{} + nextCount := map[reflect.Type]int{} + + visited := map[reflect.Type]struct{}{} + fields := []field{} + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, map[reflect.Type]int{} + + for _, f := range current { + if _, ok := visited[f.Type]; ok { + continue + } + visited[f.Type] = struct{}{} + + for i := 0; i < f.Type.NumField(); i++ { + sf := f.Type.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { + // Ignore unexported and non-anonymous fields + // unexported but anonymous field may still be used if + // the type has exported nested fields + continue + } + + fieldTag := tag{} + fieldTag.parseAVTag(sf.Tag) + // Because MarshalOptions.TagKey must be explicitly set. + if opts.TagKey != "" && fieldTag == (tag{}) { + fieldTag.parseStructTag(opts.TagKey, sf.Tag) + } + + if fieldTag.Ignore { + continue + } + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + + structField := buildField(f.Index, i, sf, fieldTag) + structField.Type = ft + + if !sf.Anonymous || ft.Kind() != reflect.Struct { + fields = append(fields, structField) + if count[f.Type] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + fields = append(fields, structField) + } + continue + } + + // Record new anon struct to explore next round + nextCount[ft]++ + if nextCount[ft] == 1 { + next = append(next, structField) + } + } + } + } + + return fields +} + +// visibleFields will return a slice of fields which are visible based on +// Go's standard visiblity rules with the exception of ties being broken +// by depth and struct tag naming. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go typeField func. +func visibleFields(fields []field) []field { + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with JSON tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := fields[:0] + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.Name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.Name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, fi) + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + fields = out + sort.Sort(fieldsByIndex(fields)) + + return fields +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's embedding rules, modified by the presence of +// JSON tags. If there are multiple top-level fields, the boolean +// will be false: This condition is an error in Go and we skip all +// the fields. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go dominantField func. +func dominantField(fields []field) (field, bool) { + // The fields are sorted in increasing index-length order. The winner + // must therefore be one with the shortest index length. Drop all + // longer entries, which is easy: just truncate the slice. + length := len(fields[0].Index) + tagged := -1 // Index of first tagged field. + for i, f := range fields { + if len(f.Index) > length { + fields = fields[:i] + break + } + if f.NameFromTag { + if tagged >= 0 { + // Multiple tagged fields at the same level: conflict. + // Return no field. + return field{}, false + } + tagged = i + } + } + if tagged >= 0 { + return fields[tagged], true + } + // All remaining fields have the same length. If there's more than one, + // we have a conflict (two fields named "X" at the same level) and we + // return no field. + if len(fields) > 1 { + return field{}, false + } + return fields[0], true +} + +// fieldsByName sorts field by name, breaking ties with depth, +// then breaking ties with "name came from json tag", then +// breaking ties with index sequence. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go fieldsByName type. +type fieldsByName []field + +func (x fieldsByName) Len() int { return len(x) } + +func (x fieldsByName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x fieldsByName) Less(i, j int) bool { + if x[i].Name != x[j].Name { + return x[i].Name < x[j].Name + } + if len(x[i].Index) != len(x[j].Index) { + return len(x[i].Index) < len(x[j].Index) + } + if x[i].NameFromTag != x[j].NameFromTag { + return x[i].NameFromTag + } + return fieldsByIndex(x).Less(i, j) +} + +// fieldsByIndex sorts field by index sequence. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go fieldsByIndex type. +type fieldsByIndex []field + +func (x fieldsByIndex) Len() int { return len(x) } + +func (x fieldsByIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x fieldsByIndex) Less(i, j int) bool { + for k, xik := range x[i].Index { + if k >= len(x[j].Index) { + return false + } + if xik != x[j].Index[k] { + return xik < x[j].Index[k] + } + } + return len(x[i].Index) < len(x[j].Index) +} diff --git a/feature/dynamodb/attributevalue/field_cache.go b/feature/dynamodb/attributevalue/field_cache.go new file mode 100644 index 00000000000..60a9d9c7499 --- /dev/null +++ b/feature/dynamodb/attributevalue/field_cache.go @@ -0,0 +1,45 @@ +package attributevalue + +import ( + "strings" + "sync" +) + +var fieldCache fieldCacher + +type fieldCacher struct { + cache sync.Map +} + +func (c *fieldCacher) Load(t interface{}) (*cachedFields, bool) { + if v, ok := c.cache.Load(t); ok { + return v.(*cachedFields), true + } + return nil, false +} + +func (c *fieldCacher) LoadOrStore(t interface{}, fs *cachedFields) (*cachedFields, bool) { + v, ok := c.cache.LoadOrStore(t, fs) + return v.(*cachedFields), ok +} + +type cachedFields struct { + fields []field + fieldsByName map[string]int +} + +func (f *cachedFields) All() []field { + return f.fields +} + +func (f *cachedFields) FieldByName(name string) (field, bool) { + if i, ok := f.fieldsByName[name]; ok { + return f.fields[i], ok + } + for _, f := range f.fields { + if strings.EqualFold(f.Name, name) { + return f, true + } + } + return field{}, false +} diff --git a/feature/dynamodb/attributevalue/field_test.go b/feature/dynamodb/attributevalue/field_test.go new file mode 100644 index 00000000000..82a09d6cf99 --- /dev/null +++ b/feature/dynamodb/attributevalue/field_test.go @@ -0,0 +1,128 @@ +package attributevalue + +import ( + "reflect" + "testing" +) + +type testUnionValues struct { + Name string + Value interface{} +} + +type unionSimple struct { + A int + B string + C []string +} + +type unionComplex struct { + unionSimple + A int +} + +type unionTagged struct { + A int `json:"A"` +} + +type unionTaggedComplex struct { + unionSimple + unionTagged + B string +} + +func TestUnionStructFields(t *testing.T) { + var cases = []struct { + in interface{} + expect []testUnionValues + }{ + { + in: unionSimple{1, "2", []string{"abc"}}, + expect: []testUnionValues{ + {"A", 1}, + {"B", "2"}, + {"C", []string{"abc"}}, + }, + }, + { + in: unionComplex{ + unionSimple: unionSimple{1, "2", []string{"abc"}}, + A: 2, + }, + expect: []testUnionValues{ + {"B", "2"}, + {"C", []string{"abc"}}, + {"A", 2}, + }, + }, + { + in: unionTaggedComplex{ + unionSimple: unionSimple{1, "2", []string{"abc"}}, + unionTagged: unionTagged{3}, + B: "3", + }, + expect: []testUnionValues{ + {"C", []string{"abc"}}, + {"A", 3}, + {"B", "3"}, + }, + }, + } + + for i, c := range cases { + v := reflect.ValueOf(c.in) + + fields := unionStructFields(v.Type(), structFieldOptions{TagKey: "json"}) + for j, f := range fields.All() { + expected := c.expect[j] + if e, a := expected.Name, f.Name; e != a { + t.Errorf("%d:%d expect %v, got %v", i, j, e, f) + } + actual := v.FieldByIndex(f.Index).Interface() + if e, a := expected.Value, actual; !reflect.DeepEqual(e, a) { + t.Errorf("%d:%d expect %v, got %v", i, j, e, f) + } + } + } +} + +func TestCachedFields(t *testing.T) { + type myStruct struct { + Dog int + CAT string + bird bool + } + + fields := unionStructFields(reflect.TypeOf(myStruct{}), structFieldOptions{}) + + const expectedNumFields = 2 + if numFields := len(fields.All()); numFields != expectedNumFields { + t.Errorf("expected number of fields to be %d but got %d", expectedNumFields, numFields) + } + + cases := []struct { + Name string + FieldName string + Found bool + }{ + {"Dog", "Dog", true}, + {"dog", "Dog", true}, + {"DOG", "Dog", true}, + {"Yorkie", "", false}, + {"Cat", "CAT", true}, + {"cat", "CAT", true}, + {"CAT", "CAT", true}, + {"tiger", "", false}, + {"bird", "", false}, + } + + for _, c := range cases { + f, found := fields.FieldByName(c.Name) + if found != c.Found { + t.Errorf("expected found to be %v but got %v", c.Found, found) + } + if found && f.Name != c.FieldName { + t.Errorf("expected field name to be %s but got %s", c.FieldName, f.Name) + } + } +} diff --git a/feature/dynamodb/attributevalue/go.mod b/feature/dynamodb/attributevalue/go.mod new file mode 100644 index 00000000000..7e268c73440 --- /dev/null +++ b/feature/dynamodb/attributevalue/go.mod @@ -0,0 +1,18 @@ +module github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue + +go 1.15 + +require ( + 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 => ../../../ + +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 new file mode 100644 index 00000000000..5dae3f89edf --- /dev/null +++ b/feature/dynamodb/attributevalue/go.sum @@ -0,0 +1,38 @@ +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/awslabs/smithy-go v0.4.0 h1:El0KyKn4zdM3pLuWJlgoeitQuu/mjwUPssr7L3xu3vs= +github.com/awslabs/smithy-go v0.4.0/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.4.1-0.20201208232924-b8cdbaa577ff h1:mtSekcc5R2mJG5+cdIlL15WD//Lobtzil5hkcr8WhiA= +github.com/awslabs/smithy-go v0.4.1-0.20201208232924-b8cdbaa577ff/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.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= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/feature/dynamodb/attributevalue/marshaler_examples_test.go b/feature/dynamodb/attributevalue/marshaler_examples_test.go new file mode 100644 index 00000000000..c1160fd2a63 --- /dev/null +++ b/feature/dynamodb/attributevalue/marshaler_examples_test.go @@ -0,0 +1,94 @@ +package attributevalue_test + +import ( + "fmt" + "reflect" + + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/internal/awsutil" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +func ExampleMarshal() { + type Record struct { + Bytes []byte + MyField string + Letters []string + Numbers []int + } + + r := Record{ + Bytes: []byte{48, 49}, + MyField: "MyFieldValue", + Letters: []string{"a", "b", "c", "d"}, + Numbers: []int{1, 2, 3}, + } + av, err := attributevalue.Marshal(r) + m := av.(*types.AttributeValueMemberM) + fmt.Println("err", err) + 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 { + // Value: len 2 + // } + // MyField { + // Value: "MyFieldValue" + // } + // Letters { + // Value: [ + // &{a}, + // &{b}, + // &{c}, + // &{d} + // ] + // } + // Numbers { + // Value: [&{1},&{2},&{3}] + // } +} + +func ExampleUnmarshal() { + type Record struct { + Bytes []byte + MyField string + Letters []string + A2Num map[string]int + } + + expect := Record{ + Bytes: []byte{48, 49}, + MyField: "MyFieldValue", + Letters: []string{"a", "b", "c", "d"}, + A2Num: map[string]int{"a": 1, "b": 2, "c": 3}, + } + + av := &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": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "a": &types.AttributeValueMemberN{Value: "1"}, + "b": &types.AttributeValueMemberN{Value: "2"}, + "c": &types.AttributeValueMemberN{Value: "3"}, + }}, + }, + } + + actual := Record{} + err := attributevalue.Unmarshal(av, &actual) + fmt.Println(err, reflect.DeepEqual(expect, actual)) + + // Output: + // true +} diff --git a/feature/dynamodb/attributevalue/marshaler_test.go b/feature/dynamodb/attributevalue/marshaler_test.go new file mode 100644 index 00000000000..6d84c8adac5 --- /dev/null +++ b/feature/dynamodb/attributevalue/marshaler_test.go @@ -0,0 +1,698 @@ +package attributevalue + +import ( + "math" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/google/go-cmp/cmp" +) + +type simpleMarshalStruct struct { + Byte []byte + String string + PtrString *string + Int int + Uint uint + Float32 float32 + Float64 float64 + Bool bool + Null *interface{} +} + +type complexMarshalStruct struct { + Simple []simpleMarshalStruct +} + +type myByteStruct struct { + Byte []byte +} + +type myByteSetStruct struct { + ByteSet [][]byte +} + +type marshallerTestInput struct { + input interface{} + expected interface{} + err error +} + +var trueValue = true +var falseValue = false + +var marshalerScalarInputs = map[string]marshallerTestInput{ + "nil": { + input: nil, + expected: &types.AttributeValueMemberNULL{Value: true}, + }, + "string": { + input: "some string", + expected: &types.AttributeValueMemberS{Value: "some string"}, + }, + "bool": { + input: true, + expected: &types.AttributeValueMemberBOOL{Value: true}, + }, + "bool false": { + input: false, + expected: &types.AttributeValueMemberBOOL{Value: false}, + }, + "float": { + input: 3.14, + expected: &types.AttributeValueMemberN{Value: "3.14"}, + }, + "max float32": { + input: math.MaxFloat32, + expected: &types.AttributeValueMemberN{Value: "340282346638528860000000000000000000000"}, + }, + "max float64": { + input: math.MaxFloat64, + expected: &types.AttributeValueMemberN{Value: "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, + }, + "integer": { + input: 12, + expected: &types.AttributeValueMemberN{Value: "12"}, + }, + "number integer": { + input: Number("12"), + expected: &types.AttributeValueMemberN{Value: "12"}, + }, + "zero values": { + input: simpleMarshalStruct{}, + 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 = 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": &types.AttributeValueMemberS{Value: "some string"}}, + }, + "bool": { + input: map[string]interface{}{"bool": true}, + expected: map[string]types.AttributeValue{"bool": &types.AttributeValueMemberBOOL{Value: true}}, + }, + "bool false": { + input: map[string]interface{}{"bool": false}, + expected: map[string]types.AttributeValue{"bool": &types.AttributeValueMemberBOOL{Value: false}}, + }, + "null": { + input: map[string]interface{}{"null": nil}, + expected: map[string]types.AttributeValue{"null": &types.AttributeValueMemberNULL{Value: true}}, + }, + "float": { + input: map[string]interface{}{"float": 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": &types.AttributeValueMemberN{Value: "340282346638528860000000000000000000000"}}, + }, + "float64": { + input: map[string]interface{}{"float": math.MaxFloat64}, + expected: map[string]types.AttributeValue{"float": &types.AttributeValueMemberN{Value: "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}, + }, + "decimal number": { + input: map[string]interface{}{"num": 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": &types.AttributeValueMemberB{Value: []byte{48, 49}}}, + }, + "nested blob": { + input: struct{ Byte []byte }{Byte: []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": &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": &types.AttributeValueMemberBS{Value: [][]byte{{48, 49}, {50, 51}}}}, + }, + "list": { + input: map[string]interface{}{"list": []interface{}{"a string", 12., 3.14, true, nil, false}}, + expected: map[string]types.AttributeValue{ + "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": { + input: map[string]interface{}{"map": map[string]interface{}{"nestednum": 12.}}, + expected: map[string]types.AttributeValue{ + "map": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "nestednum": &types.AttributeValueMemberN{Value: "12"}, + }, + }, + }, + }, + "struct": { + input: simpleMarshalStruct{}, + expected: 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"}, + }, + }, + "nested struct": { + input: complexMarshalStruct{}, + expected: map[string]types.AttributeValue{ + "Simple": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + "nested nil slice": { + input: struct { + Simple []string `dynamodbav:"simple"` + }{}, + expected: map[string]types.AttributeValue{ + "simple": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + "nested nil slice omit empty": { + input: struct { + Simple []string `dynamodbav:"simple,omitempty"` + }{}, + expected: map[string]types.AttributeValue{}, + }, + "nested ignored field": { + input: struct { + 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": &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"}, + }, + }, + &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"}, + }, + }, + }, + }, + }, + }, +} + +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{ + &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{ + &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"}, + }, + }, + }, + }, +} + +func Test_New_Marshal(t *testing.T) { + 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) { +} + +func Test_New_Unmarshal(t *testing.T) { + // Using the same inputs from Marshal, test the reverse mapping. + 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()) + }) + } +} + +func Test_New_UnmarshalError(t *testing.T) { + // Test that we get an error using Unmarshal to convert to a nil value. + expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)} + if err := Unmarshal(nil, nil); err == nil { + t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", nil, expected) + } else if err.Error() != expected.Error() { + t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", nil, err, expected) + } + + // Test that we get an error using Unmarshal to convert to a non-pointer value. + var actual map[string]interface{} + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)} + if err := Unmarshal(nil, actual); err == nil { + t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", actual, expected) + } else if err.Error() != expected.Error() { + t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", actual, err, expected) + } + + // Test that we get an error using Unmarshal to convert to nil struct. + var actual2 *struct{ A int } + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)} + if err := Unmarshal(nil, actual2); err == nil { + t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", actual2, expected) + } else if err.Error() != expected.Error() { + t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", actual2, err, expected) + } +} + +func Test_New_MarshalMap(t *testing.T) { + for 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 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()) + }) + } +} + +func Test_New_UnmarshalMapError(t *testing.T) { + // Test that we get an error using UnmarshalMap to convert to a nil value. + expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)} + if err := UnmarshalMap(nil, nil); err == nil { + t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", nil, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", nil, err, expected) + } + + // Test that we get an error using UnmarshalMap to convert to a non-pointer value. + var actual map[string]interface{} + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)} + if err := UnmarshalMap(nil, actual); err == nil { + t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", actual, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", actual, err, expected) + } + + // Test that we get an error using UnmarshalMap to convert to nil struct. + var actual2 *struct{ A int } + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)} + if err := UnmarshalMap(nil, actual2); err == nil { + t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", actual2, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", actual2, err, expected) + } +} + +func Test_New_MarshalList(t *testing.T) { + for 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 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()) + }) + } +} + +func Test_New_UnmarshalListError(t *testing.T) { + // Test that we get an error using UnmarshalList to convert to a nil value. + expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)} + if err := UnmarshalList(nil, nil); err == nil { + t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", nil, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", nil, err, expected) + } + + // Test that we get an error using UnmarshalList to convert to a non-pointer value. + var actual map[string]interface{} + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)} + if err := UnmarshalList(nil, actual); err == nil { + t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", actual, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", actual, err, expected) + } + + // Test that we get an error using UnmarshalList to convert to nil struct. + var actual2 *struct{ A int } + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)} + if err := UnmarshalList(nil, actual2); err == nil { + t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", actual2, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", actual2, err, expected) + } +} + +func compareObjects(t *testing.T, expected interface{}, actual interface{}) { + t.Helper() + if !reflect.DeepEqual(expected, actual) { + ev := reflect.ValueOf(expected) + av := reflect.ValueOf(actual) + if diff := cmp.Diff(expected, actual); len(diff) != 0 { + t.Errorf("expect kind(%s, %T) match actual kind(%s, %T)\n%s", + ev.Kind(), ev.Interface(), av.Kind(), av.Interface(), diff) + } + } +} + +func BenchmarkMarshalOneMember(b *testing.B) { + fieldCache = fieldCacher{} + + simple := simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + } + type MyCompositeStruct struct { + A simpleMarshalStruct `dynamodbav:"a"` + } + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := Marshal(MyCompositeStruct{ + A: simple, + }); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func BenchmarkMarshalTwoMembers(b *testing.B) { + fieldCache = fieldCacher{} + + simple := simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + } + + type MyCompositeStruct struct { + A simpleMarshalStruct `dynamodbav:"a"` + B simpleMarshalStruct `dynamodbav:"b"` + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := Marshal(MyCompositeStruct{ + A: simple, + B: simple, + }); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func BenchmarkUnmarshalOneMember(b *testing.B) { + fieldCache = fieldCacher{} + + myStructAVMap, _ := Marshal(simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + }) + + type MyCompositeStructOne struct { + A simpleMarshalStruct `dynamodbav:"a"` + } + var out MyCompositeStructOne + avMap := map[string]types.AttributeValue{ + "a": myStructAVMap, + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := Unmarshal(&types.AttributeValueMemberM{Value: avMap}, &out); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func BenchmarkUnmarshalTwoMembers(b *testing.B) { + fieldCache = fieldCacher{} + + myStructAVMap, _ := Marshal(simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + }) + + type MyCompositeStructTwo struct { + A simpleMarshalStruct `dynamodbav:"a"` + B simpleMarshalStruct `dynamodbav:"b"` + } + var out MyCompositeStructTwo + avMap := map[string]types.AttributeValue{ + "a": myStructAVMap, + "b": myStructAVMap, + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := Unmarshal(&types.AttributeValueMemberM{Value: avMap}, &out); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func Test_Encode_YAML_TagKey(t *testing.T) { + input := struct { + String string `yaml:"string"` + EmptyString string `yaml:"empty"` + OmitString string `yaml:"omitted,omitempty"` + Ignored string `yaml:"-"` + Byte []byte `yaml:"byte"` + Float32 float32 `yaml:"float32"` + Float64 float64 `yaml:"float64"` + Int int `yaml:"int"` + Uint uint `yaml:"uint"` + Slice []string `yaml:"slice"` + Map map[string]int `yaml:"map"` + NoTag string + }{ + String: "String", + Ignored: "Ignored", + Slice: []string{"one", "two"}, + Map: map[string]int{ + "one": 1, + "two": 2, + }, + NoTag: "NoTag", + } + + 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": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "one": &types.AttributeValueMemberN{Value: "1"}, + "two": &types.AttributeValueMemberN{Value: "2"}, + }, + }, + "NoTag": &types.AttributeValueMemberS{Value: "NoTag"}, + }, + } + + enc := NewEncoder(func(o *EncoderOptions) { + o.TagKey = "yaml" + }) + + actual, err := enc.Encode(input) + if err != nil { + 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 new file mode 100644 index 00000000000..abf9eb96daf --- /dev/null +++ b/feature/dynamodb/attributevalue/shared_test.go @@ -0,0 +1,409 @@ +package attributevalue + +import ( + "reflect" + "strings" + "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" +) + +type testBinarySetStruct struct { + Binarys [][]byte `dynamodbav:",binaryset"` +} +type testNumberSetStruct struct { + Numbers []int `dynamodbav:",numberset"` +} +type testStringSetStruct struct { + Strings []string `dynamodbav:",stringset"` +} + +type testIntAsStringStruct struct { + Value int `dynamodbav:",string"` +} + +type testOmitEmptyStruct struct { + Value string `dynamodbav:",omitempty"` + Value2 *string `dynamodbav:",omitempty"` + Value3 int +} + +type testAliasedString string +type testAliasedStringSlice []string +type testAliasedInt int +type testAliasedIntSlice []int +type testAliasedMap map[string]int +type testAliasedSlice []string +type testAliasedByteSlice []byte +type testAliasedBool bool +type testAliasedBoolSlice []bool + +type testAliasedStruct struct { + Value testAliasedString + Value2 testAliasedInt + Value3 testAliasedMap + Value4 testAliasedSlice + + Value5 testAliasedByteSlice + Value6 []testAliasedInt + Value7 []testAliasedString + + Value8 []testAliasedByteSlice `dynamodbav:",binaryset"` + Value9 []testAliasedInt `dynamodbav:",numberset"` + Value10 []testAliasedString `dynamodbav:",stringset"` + + Value11 testAliasedIntSlice + Value12 testAliasedStringSlice + + Value13 testAliasedBool + Value14 testAliasedBoolSlice + + Value15 map[testAliasedString]string +} + +type testNamedPointer *int + +var testDate, _ = time.Parse(time.RFC3339, "2016-05-03T17:06:26.209072Z") + +var sharedTestCases = map[string]struct { + in types.AttributeValue + actual, expected interface{} + err error +}{ + "binary slice": { + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, + actual: &[]byte{}, + expected: []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.AttributeValueMemberB{Value: []byte{48, 49}}, + actual: func() **[]byte { + v := make([]byte, 0, 10) + v2 := &v + return &v2 + }(), + expected: []byte{48, 49}, + }, + "bool": { + in: &types.AttributeValueMemberBOOL{Value: true}, + actual: new(bool), + expected: true, + }, + "list": { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "123"}, + }}, + actual: &[]int{}, + expected: []int{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.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "Abc": &types.AttributeValueMemberN{Value: "123"}, + }}, + actual: &struct{ Abc int }{}, + expected: struct{ Abc int }{Abc: 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"` + }{}, + expected: struct { + Abc int `json:"abc" dynamodbav:"abc"` + }{Abc: 123}, + }, + "number, int": { + in: &types.AttributeValueMemberN{Value: "123"}, + actual: new(int), + expected: 123, + }, + "number, Float": { + in: &types.AttributeValueMemberN{Value: "123.1"}, + actual: new(float64), + expected: float64(123.1), + }, + "null ptr": { + in: &types.AttributeValueMemberNULL{Value: true}, + actual: new(*string), + expected: nil, + }, + "string": { + in: &types.AttributeValueMemberS{Value: "abc"}, + actual: new(string), + expected: "abc", + }, + "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.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.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.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Value": &types.AttributeValueMemberS{Value: "123"}, + }, + }, + actual: &testIntAsStringStruct{}, + expected: testIntAsStringStruct{Value: 123}, + }, + "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.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": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "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": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "3"}, + }}, + "Value8": &types.AttributeValueMemberBS{Value: [][]byte{ + {0, 1, 2}, {3, 4, 5}, + }}, + "Value9": &types.AttributeValueMemberNS{Value: []string{ + "1", + "2", + "3", + }}, + "Value10": &types.AttributeValueMemberSS{Value: []string{ + "1", + "2", + "3", + }}, + "Value11": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, + }}, + "Value12": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "3"}, + }}, + "Value13": &types.AttributeValueMemberBOOL{Value: true}, + "Value14": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberBOOL{Value: false}, + &types.AttributeValueMemberBOOL{Value: true}, + }}, + "Value15": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "TestKey": &types.AttributeValueMemberS{Value: "TestElement"}, + }}, + }, + }, + actual: &testAliasedStruct{}, + expected: testAliasedStruct{ + Value: "123", Value2: 123, + Value3: testAliasedMap{ + "Key": 321, + }, + Value4: testAliasedSlice{"1", "2", "3"}, + Value5: testAliasedByteSlice{0, 1, 2}, + Value6: []testAliasedInt{1, 2, 3}, + Value7: []testAliasedString{"1", "2", "3"}, + Value8: []testAliasedByteSlice{ + {0, 1, 2}, + {3, 4, 5}, + }, + Value9: []testAliasedInt{1, 2, 3}, + Value10: []testAliasedString{"1", "2", "3"}, + Value11: testAliasedIntSlice{1, 2, 3}, + Value12: testAliasedStringSlice{"1", "2", "3"}, + Value13: true, + Value14: testAliasedBoolSlice{true, false, true}, + Value15: map[testAliasedString]string{"TestKey": "TestElement"}, + }, + }, + "number named pointer": { + in: &types.AttributeValueMemberN{Value: "123"}, + actual: new(testNamedPointer), + expected: testNamedPointer(aws.Int(123)), + }, + "time.Time": { + in: &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, + actual: new(time.Time), + expected: testDate, + }, + "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.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"` + }{}, + expected: struct { + Abc time.Time `json:"abc" dynamodbav:"abc"` + }{Abc: testDate}, + }, + "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"` + }{}, + expected: struct { + Abc *time.Time `json:"abc" dynamodbav:"abc"` + }{Abc: &testDate}, + }, +} + +var sharedListTestCases = map[string]struct { + in []types.AttributeValue + actual, expected interface{} + err error +}{ + "union members": { + in: []types.AttributeValue{ + &types.AttributeValueMemberB{Value: []byte{48, 49}}, + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberN{Value: "123"}, + &types.AttributeValueMemberS{Value: "123"}, + }, + actual: func() *[]interface{} { + v := []interface{}{} + return &v + }(), + expected: []interface{}{[]byte{48, 49}, true, 123., "123"}, + }, + "numbers": { + in: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, + }, + actual: &[]interface{}{}, + expected: []interface{}{1., 2., 3.}, + }, +} + +var sharedMapTestCases = map[string]struct { + in map[string]types.AttributeValue + actual, expected interface{} + err error +}{ + "union members": { + in: map[string]types.AttributeValue{ + "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{}{ + "B": []byte{48, 49}, "BOOL": true, + "N": 123., "S": "123", + }, + }, +} + +func assertConvertTest(t *testing.T, actual, expected interface{}, err, expectedErr error) { + t.Helper() + + if expectedErr != nil { + if err != nil { + if e, a := expectedErr, err; !strings.Contains(a.Error(), e.Error()) { + t.Errorf("expect %v, got %v", e, a) + } + } else { + t.Fatalf("expected error, %v", expectedErr) + } + } else if err != nil { + t.Fatalf("expect no error, got %v", err) + } else { + if diff := cmp.Diff(ptrToValue(expected), ptrToValue(actual)); len(diff) != 0 { + t.Errorf("expect match\n%s", diff) + } + } +} + +func ptrToValue(in interface{}) interface{} { + v := reflect.ValueOf(in) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if !v.IsValid() { + return nil + } + if v.Kind() == reflect.Ptr { + return ptrToValue(v.Interface()) + } + return v.Interface() +} diff --git a/feature/dynamodb/attributevalue/tag.go b/feature/dynamodb/attributevalue/tag.go new file mode 100644 index 00000000000..6eb901706fb --- /dev/null +++ b/feature/dynamodb/attributevalue/tag.go @@ -0,0 +1,74 @@ +package attributevalue + +import ( + "reflect" + "strings" +) + +type tag struct { + Name string + Ignore bool + OmitEmpty bool + OmitEmptyElem bool + NullEmpty bool + NullEmptyElem bool + AsString bool + AsBinSet, AsNumSet, AsStrSet bool + AsUnixTime bool +} + +func (t *tag) parseAVTag(structTag reflect.StructTag) { + tagStr := structTag.Get("dynamodbav") + if len(tagStr) == 0 { + return + } + + t.parseTagStr(tagStr) +} + +func (t *tag) parseStructTag(tag string, structTag reflect.StructTag) { + tagStr := structTag.Get(tag) + if len(tagStr) == 0 { + return + } + + t.parseTagStr(tagStr) +} + +func (t *tag) parseTagStr(tagStr string) { + parts := strings.Split(tagStr, ",") + if len(parts) == 0 { + return + } + + if name := parts[0]; name == "-" { + t.Name = "" + t.Ignore = true + } else { + t.Name = name + t.Ignore = false + } + + for _, opt := range parts[1:] { + switch opt { + case "omitempty": + t.OmitEmpty = true + case "omitemptyelem": + t.OmitEmptyElem = true + case "nullempty": + t.NullEmpty = true + case "nullemptyelem": + t.NullEmptyElem = true + case "string": + t.AsString = true + case "binaryset": + t.AsBinSet = true + case "numberset": + t.AsNumSet = true + case "stringset": + t.AsStrSet = true + case "unixtime": + t.AsUnixTime = true + } + } +} diff --git a/feature/dynamodb/attributevalue/tag_test.go b/feature/dynamodb/attributevalue/tag_test.go new file mode 100644 index 00000000000..e772a8486fa --- /dev/null +++ b/feature/dynamodb/attributevalue/tag_test.go @@ -0,0 +1,47 @@ +package attributevalue + +import ( + "reflect" + "testing" +) + +func TestTagParse(t *testing.T) { + cases := []struct { + in reflect.StructTag + json, av bool + expect tag + }{ + {`json:""`, true, false, tag{}}, + {`json:"name"`, true, false, tag{Name: "name"}}, + {`json:"name,omitempty"`, true, false, tag{Name: "name", OmitEmpty: true}}, + {`json:"-"`, true, false, tag{Ignore: true}}, + {`json:",omitempty"`, true, false, tag{OmitEmpty: true}}, + {`json:",string"`, true, false, tag{AsString: true}}, + {`dynamodbav:""`, false, true, tag{}}, + {`dynamodbav:","`, false, true, tag{}}, + {`dynamodbav:"name"`, false, true, tag{Name: "name"}}, + {`dynamodbav:"name"`, false, true, tag{Name: "name"}}, + {`dynamodbav:"-"`, false, true, tag{Ignore: true}}, + {`dynamodbav:",omitempty"`, false, true, tag{OmitEmpty: true}}, + {`dynamodbav:",omitemptyelem"`, false, true, tag{OmitEmptyElem: true}}, + {`dynamodbav:",string"`, false, true, tag{AsString: true}}, + {`dynamodbav:",binaryset"`, false, true, tag{AsBinSet: true}}, + {`dynamodbav:",numberset"`, false, true, tag{AsNumSet: true}}, + {`dynamodbav:",stringset"`, false, true, tag{AsStrSet: true}}, + {`dynamodbav:",stringset,omitemptyelem"`, false, true, tag{AsStrSet: true, OmitEmptyElem: true}}, + {`dynamodbav:"name,stringset,omitemptyelem"`, false, true, tag{Name: "name", AsStrSet: true, OmitEmptyElem: true}}, + } + + for i, c := range cases { + actual := tag{} + if c.json { + actual.parseStructTag("json", c.in) + } + if c.av { + actual.parseAVTag(c.in) + } + if e, a := c.expect, actual; !reflect.DeepEqual(e, a) { + t.Errorf("case %d, expect %v, got %v", i, e, a) + } + } +} diff --git a/feature/dynamodbstreams/attributevalue/LICENSE.txt b/feature/dynamodbstreams/attributevalue/LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/feature/dynamodbstreams/attributevalue/convert.go b/feature/dynamodbstreams/attributevalue/convert.go new file mode 100644 index 00000000000..9d620f448e4 --- /dev/null +++ b/feature/dynamodbstreams/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" +) + +// FromDynamoDBMap converts a map of Amazon DynamoDB +// AttributeValues, and all nested members. +func FromDynamoDBMap(from map[string]ddb.AttributeValue) (to map[string]streams.AttributeValue, err error) { + to = make(map[string]streams.AttributeValue, len(from)) + for field, value := range from { + to[field], err = FromDynamoDB(value) + if err != nil { + return nil, err + } + } + + return to, nil +} + +// FromDynamoDBList converts a slice of Amazon DynamoDB +// AttributeValues, and all nested members. +func FromDynamoDBList(from []ddb.AttributeValue) (to []streams.AttributeValue, err error) { + to = make([]streams.AttributeValue, len(from)) + for i := 0; i < len(from); i++ { + to[i], err = FromDynamoDB(from[i]) + if err != nil { + return nil, err + } + } + + return to, nil +} + +// FromDynamoDB converts an Amazon DynamoDB AttributeValue, and +// all nested members. +func FromDynamoDB(from ddb.AttributeValue) (streams.AttributeValue, error) { + switch tv := from.(type) { + case *ddb.AttributeValueMemberNULL: + return &streams.AttributeValueMemberNULL{Value: tv.Value}, nil + + case *ddb.AttributeValueMemberBOOL: + return &streams.AttributeValueMemberBOOL{Value: tv.Value}, nil + + case *ddb.AttributeValueMemberB: + return &streams.AttributeValueMemberB{Value: tv.Value}, nil + + case *ddb.AttributeValueMemberBS: + bs := make([][]byte, len(tv.Value)) + for i := 0; i < len(tv.Value); i++ { + bs[i] = append([]byte{}, tv.Value[i]...) + } + return &streams.AttributeValueMemberBS{Value: bs}, nil + + case *ddb.AttributeValueMemberN: + return &streams.AttributeValueMemberN{Value: tv.Value}, nil + + case *ddb.AttributeValueMemberNS: + return &streams.AttributeValueMemberNS{Value: append([]string{}, tv.Value...)}, nil + + case *ddb.AttributeValueMemberS: + return &streams.AttributeValueMemberS{Value: tv.Value}, nil + + case *ddb.AttributeValueMemberSS: + return &streams.AttributeValueMemberSS{Value: append([]string{}, tv.Value...)}, nil + + case *ddb.AttributeValueMemberL: + values, err := FromDynamoDBList(tv.Value) + if err != nil { + return nil, err + } + return &streams.AttributeValueMemberL{Value: values}, nil + + case *ddb.AttributeValueMemberM: + values, err := FromDynamoDBMap(tv.Value) + if err != nil { + return nil, err + } + return &streams.AttributeValueMemberM{Value: values}, nil + + default: + return nil, fmt.Errorf("unknown AttributeValue union member, %T", from) + } +} diff --git a/feature/dynamodbstreams/attributevalue/decode.go b/feature/dynamodbstreams/attributevalue/decode.go new file mode 100644 index 00000000000..e62b8099e44 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/decode.go @@ -0,0 +1,748 @@ +package attributevalue + +import ( + "fmt" + "reflect" + "strconv" + "time" + + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" +) + +// An Unmarshaler is an interface to provide custom unmarshaling of +// AttributeValues. Use this to provide custom logic determining +// how AttributeValues should be unmarshaled. +// type ExampleUnmarshaler struct { +// Value int +// } +// +// func (u *ExampleUnmarshaler) UnmarshalDynamoDBStreamsAttributeValue(av types.AttributeValue) error { +// avN, ok := av.(*types.AttributeValueMemberN) +// if !ok { +// return nil +// } +// +// n, err := strconv.ParseInt(avN.Value, 10, 0) +// if err != nil { +// return err +// } +// +// u.Value = int(n) +// return nil +// } +type Unmarshaler interface { + UnmarshalDynamoDBStreamsAttributeValue(types.AttributeValue) error +} + +// Unmarshal will unmarshal AttributeValues to Go value types. +// Both generic interface{} and concrete types are valid unmarshal +// destination types. +// +// Unmarshal will allocate maps, slices, and pointers as needed to +// unmarshal the AttributeValue into the provided type value. +// +// When unmarshaling AttributeValues into structs Unmarshal matches +// the field names of the struct to the AttributeValue Map keys. +// Initially it will look for exact field name matching, but will +// fall back to case insensitive if not exact match is found. +// +// With the exception of omitempty, omitemptyelem, binaryset, numberset +// and stringset all struct tags used by Marshal are also used by +// Unmarshal. +// +// When decoding AttributeValues to interfaces Unmarshal will use the +// following types. +// +// []byte, AV Binary (B) +// [][]byte, AV Binary Set (BS) +// bool, AV Boolean (BOOL) +// []interface{}, AV List (L) +// map[string]interface{}, AV Map (M) +// float64, AV Number (N) +// Number, AV Number (N) with UseNumber set +// []float64, AV Number Set (NS) +// []Number, AV Number Set (NS) with UseNumber set +// string, AV String (S) +// []string, AV String Set (SS) +// +// If the Decoder option, UseNumber is set numbers will be unmarshaled +// as Number values instead of float64. Use this to maintain the original +// string formating of the number as it was represented in the AttributeValue. +// In addition provides additional opportunities to parse the number +// string based on individual use cases. +// +// When unmarshaling any error that occurs will halt the unmarshal +// and return the error. +// +// The output value provided must be a non-nil pointer +func Unmarshal(av types.AttributeValue, out interface{}) error { + return NewDecoder().Decode(av, out) +} + +// UnmarshalMap is an alias for Unmarshal which unmarshals from +// a map of AttributeValues. +// +// The output value provided must be a non-nil pointer +func UnmarshalMap(m map[string]types.AttributeValue, out interface{}) error { + return NewDecoder().Decode(&types.AttributeValueMemberM{Value: m}, out) +} + +// UnmarshalList is an alias for Unmarshal func which unmarshals +// a slice of AttributeValues. +// +// The output value provided must be a non-nil pointer +func UnmarshalList(l []types.AttributeValue, out interface{}) error { + 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 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.AttributeValueMemberM{Value: m} + } + + return UnmarshalList(items, out) +} + +// 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 + // is interface{}. Similar to encoding/json.Number + 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(optFns ...func(*DecoderOptions)) *Decoder { + var options DecoderOptions + for _, fn := range optFns { + fn(&options) + } + + return &Decoder{ + options: options, + } +} + +// Decode will unmarshal an AttributeValue into a Go value type. An error +// will be return if the decoder is unable to unmarshal the AttributeValue +// to the provide Go value type. +// +// The output value provided must be a non-nil pointer +func (d *Decoder) Decode(av 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)} + } + + return d.decode(av, v, tag{}) +} + +var stringInterfaceMapType = reflect.TypeOf(map[string]interface{}(nil)) +var byteSliceType = reflect.TypeOf([]byte(nil)) +var byteSliceSliceType = reflect.TypeOf([][]byte(nil)) +var timeType = reflect.TypeOf(time.Time{}) + +func (d *Decoder) decode(av types.AttributeValue, v reflect.Value, fieldTag tag) error { + var u Unmarshaler + _, isNull := av.(*types.AttributeValueMemberNULL) + if av == nil || isNull { + u, v = indirect(v, true) + if u != nil { + return u.UnmarshalDynamoDBStreamsAttributeValue(av) + } + return d.decodeNull(v) + } + + u, v = indirect(v, false) + if u != nil { + return u.UnmarshalDynamoDBStreamsAttributeValue(av) + } + + switch tv := av.(type) { + case *types.AttributeValueMemberB: + return d.decodeBinary(tv.Value, v) + + 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 { + if v.Kind() == reflect.Interface { + buf := make([]byte, len(b)) + copy(buf, b) + v.Set(reflect.ValueOf(buf)) + return nil + } + + if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { + return &UnmarshalTypeError{Value: "binary", Type: v.Type()} + } + + if v.Type() == byteSliceType { + // Optimization for []byte types + if v.IsNil() || v.Cap() < len(b) { + v.Set(reflect.MakeSlice(byteSliceType, len(b), len(b))) + } else if v.Len() != len(b) { + v.SetLen(len(b)) + } + copy(v.Interface().([]byte), b) + return nil + } + + switch v.Type().Elem().Kind() { + case reflect.Uint8: + // Fallback to reflection copy for type aliased of []byte type + if v.Kind() != reflect.Array && (v.IsNil() || v.Cap() < len(b)) { + v.Set(reflect.MakeSlice(v.Type(), len(b), len(b))) + } else if v.Len() != len(b) { + v.SetLen(len(b)) + } + for i := 0; i < len(b); i++ { + v.Index(i).SetUint(uint64(b[i])) + } + default: + if v.Kind() == reflect.Array && v.Type().Elem().Kind() == reflect.Uint8 { + reflect.Copy(v, reflect.ValueOf(b)) + break + } + return &UnmarshalTypeError{Value: "binary", Type: v.Type()} + } + + return nil +} + +func (d *Decoder) decodeBool(b bool, v reflect.Value) error { + switch v.Kind() { + case reflect.Bool, reflect.Interface: + v.Set(reflect.ValueOf(b).Convert(v.Type())) + + default: + return &UnmarshalTypeError{Value: "bool", Type: v.Type()} + } + + return nil +} + +func (d *Decoder) decodeBinarySet(bs [][]byte, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(bs) { + // What about if ignoring nil/empty values? + v.Set(reflect.MakeSlice(v.Type(), 0, len(bs))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + set := make([][]byte, len(bs)) + for i, b := range bs { + if err := d.decodeBinary(b, reflect.ValueOf(&set[i]).Elem()); err != nil { + return err + } + } + v.Set(reflect.ValueOf(set)) + return nil + default: + return &UnmarshalTypeError{Value: "binary set", Type: v.Type()} + } + + for i := 0; i < v.Cap() && i < len(bs); i++ { + if !isArray { + v.SetLen(i + 1) + } + u, elem := indirect(v.Index(i), false) + if u != nil { + return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberBS{Value: bs}) + } + if err := d.decodeBinary(bs[i], elem); err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeNumber(n string, v reflect.Value, fieldTag tag) error { + switch v.Kind() { + case reflect.Interface: + i, err := d.decodeNumberToInterface(n) + if err != nil { + return err + } + v.Set(reflect.ValueOf(i)) + return nil + case reflect.String: + if isNumberValueType(v) { + v.SetString(n) + return nil + } + v.Set(reflect.ValueOf(n)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(n, 10, 64) + if err != nil { + return err + } + if v.OverflowInt(i) { + return &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, %s", n), + Type: v.Type(), + } + } + v.SetInt(i) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + i, err := strconv.ParseUint(n, 10, 64) + if err != nil { + return err + } + if v.OverflowUint(i) { + return &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, %s", n), + Type: v.Type(), + } + } + v.SetUint(i) + case reflect.Float32, reflect.Float64: + i, err := strconv.ParseFloat(n, 64) + if err != nil { + return err + } + if v.OverflowFloat(i) { + return &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, %s", n), + Type: v.Type(), + } + } + v.SetFloat(i) + default: + if v.Type().ConvertibleTo(timeType) && fieldTag.AsUnixTime { + t, err := decodeUnixTime(n) + if err != nil { + return err + } + v.Set(reflect.ValueOf(t).Convert(v.Type())) + return nil + } + return &UnmarshalTypeError{Value: "number", Type: v.Type()} + } + + return 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) +} + +func (d *Decoder) decodeNumberSet(ns []string, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(ns) { + // What about if ignoring nil/empty values? + v.Set(reflect.MakeSlice(v.Type(), 0, len(ns))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + 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 { + return err + } + } + v.Set(reflect.ValueOf(set)) + } else { + set := make([]float64, len(ns)) + for i, n := range ns { + if err := d.decodeNumber(n, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { + return err + } + } + v.Set(reflect.ValueOf(set)) + } + return nil + default: + return &UnmarshalTypeError{Value: "number set", Type: v.Type()} + } + + for i := 0; i < v.Cap() && i < len(ns); i++ { + if !isArray { + v.SetLen(i + 1) + } + u, elem := indirect(v.Index(i), false) + if u != nil { + return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberNS{Value: ns}) + } + if err := d.decodeNumber(ns[i], elem, tag{}); err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeList(avList []types.AttributeValue, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(avList) { + // What about if ignoring nil/empty values? + v.Set(reflect.MakeSlice(v.Type(), 0, len(avList))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + s := make([]interface{}, len(avList)) + for i, av := range avList { + if err := d.decode(av, reflect.ValueOf(&s[i]).Elem(), tag{}); err != nil { + return err + } + } + v.Set(reflect.ValueOf(s)) + return nil + default: + return &UnmarshalTypeError{Value: "list", Type: v.Type()} + } + + // If v is not a slice, array + for i := 0; i < v.Cap() && i < len(avList); i++ { + if !isArray { + v.SetLen(i + 1) + } + if err := d.decode(avList[i], v.Index(i), tag{}); err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeMap(avMap map[string]types.AttributeValue, v reflect.Value) error { + switch v.Kind() { + case reflect.Map: + t := v.Type() + if t.Key().Kind() != reflect.String { + return &UnmarshalTypeError{Value: "map string key", Type: t.Key()} + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + case reflect.Interface: + v.Set(reflect.MakeMap(stringInterfaceMapType)) + v = v.Elem() + default: + return &UnmarshalTypeError{Value: "map", Type: v.Type()} + } + + if v.Kind() == reflect.Map { + for k, av := range avMap { + key := reflect.New(v.Type().Key()).Elem() + key.SetString(k) + elem := reflect.New(v.Type().Elem()).Elem() + if err := d.decode(av, elem, tag{}); err != nil { + return err + } + v.SetMapIndex(key, elem) + } + } else if v.Kind() == reflect.Struct { + fields := unionStructFields(v.Type(), structFieldOptions{ + TagKey: d.options.TagKey, + }) + for k, av := range avMap { + if f, ok := fields.FieldByName(k); ok { + fv := decoderFieldByIndex(v, f.Index) + if err := d.decode(av, fv, f.tag); err != nil { + return err + } + } + } + } + + return nil +} + +func (d *Decoder) decodeNull(v reflect.Value) error { + if v.IsValid() && v.CanSet() { + v.Set(reflect.Zero(v.Type())) + } + + return nil +} + +func (d *Decoder) decodeString(s string, v reflect.Value, fieldTag tag) error { + if fieldTag.AsString { + return d.decodeNumber(s, v, fieldTag) + } + + // To maintain backwards compatibility with ConvertFrom family of methods which + // converted strings to time.Time structs + if v.Type().ConvertibleTo(timeType) { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(t).Convert(v.Type())) + return nil + } + + switch v.Kind() { + case reflect.String: + v.SetString(s) + case reflect.Interface: + // Ensure type aliasing is handled properly + v.Set(reflect.ValueOf(s).Convert(v.Type())) + default: + return &UnmarshalTypeError{Value: "string", Type: v.Type()} + } + + return nil +} + +func (d *Decoder) decodeStringSet(ss []string, v reflect.Value) error { + var isArray bool + + switch v.Kind() { + case reflect.Slice: + // Make room for the slice elements if needed + if v.IsNil() || v.Cap() < len(ss) { + v.Set(reflect.MakeSlice(v.Type(), 0, len(ss))) + } + case reflect.Array: + // Limited to capacity of existing array. + isArray = true + case reflect.Interface: + set := make([]string, len(ss)) + for i, s := range ss { + if err := d.decodeString(s, reflect.ValueOf(&set[i]).Elem(), tag{}); err != nil { + return err + } + } + v.Set(reflect.ValueOf(set)) + return nil + default: + return &UnmarshalTypeError{Value: "string set", Type: v.Type()} + } + + for i := 0; i < v.Cap() && i < len(ss); i++ { + if !isArray { + v.SetLen(i + 1) + } + u, elem := indirect(v.Index(i), false) + if u != nil { + return u.UnmarshalDynamoDBStreamsAttributeValue(&types.AttributeValueMemberSS{Value: ss}) + } + if err := d.decodeString(ss[i], elem, tag{}); err != nil { + return err + } + } + + return nil +} + +func decodeUnixTime(n string) (time.Time, error) { + v, err := strconv.ParseInt(n, 10, 64) + if err != nil { + return time.Time{}, &UnmarshalError{ + Err: err, Value: n, Type: timeType, + } + } + + return time.Unix(v, 0), nil +} + +// decoderFieldByIndex finds the field with the provided nested index, allocating +// embedded parent structs if needed +func decoderFieldByIndex(v reflect.Value, index []int) reflect.Value { + for i, x := range index { + if i > 0 && v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + v = v.Field(x) + } + return v +} + +// indirect will walk a value's interface or pointer value types. Returning +// the final value or the value a unmarshaler is defined on. +// +// Based on the enoding/json type reflect value type indirection in Go Stdlib +// https://golang.org/src/encoding/json/decode.go indirect func. +func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, reflect.Value) { + if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + for { + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { + v = e + continue + } + } + if v.Kind() != reflect.Ptr { + break + } + if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, reflect.Value{} + } + } + v = v.Elem() + } + + return nil, v +} + +// A Number represents a Attributevalue number literal. +type Number string + +// Float64 attempts to cast the number ot a float64, returning +// the result of the case or error if the case failed. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 attempts to cast the number ot a int64, returning +// the result of the case or error if the case failed. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// Uint64 attempts to cast the number ot a uint64, returning +// the result of the case or error if the case failed. +func (n Number) Uint64() (uint64, error) { + return strconv.ParseUint(string(n), 10, 64) +} + +// String returns the raw number represented as a string +func (n Number) String() string { + return string(n) +} + +// 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 { + Value string + Type reflect.Type +} + +// Error returns the string representation of the error. +// satisfying the error interface +func (e *UnmarshalTypeError) Error() string { + return fmt.Sprintf("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 { + Type reflect.Type +} + +// Error returns the string representation of the error. +// satisfying the error interface +func (e *InvalidUnmarshalError) Error() string { + var msg string + if e.Type == nil { + 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()) + } + + return fmt.Sprintf("unmarshal failed, %s", msg) +} + +// 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 +} + +func (e *UnmarshalError) Unwrap() error { + return e.Err +} + +// Error returns the string representation of the error satisfying the error +// interface. +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/dynamodbstreams/attributevalue/decode_test.go b/feature/dynamodbstreams/attributevalue/decode_test.go new file mode 100644 index 00000000000..706b634f58f --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/decode_test.go @@ -0,0 +1,725 @@ +package attributevalue + +import ( + "fmt" + "reflect" + "strconv" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" + "github.com/google/go-cmp/cmp" +) + +func TestUnmarshalShared(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) + }) + } +} + +func TestUnmarshal(t *testing.T) { + cases := []struct { + in types.AttributeValue + actual, expected interface{} + err error + }{ + //------------ + // Sets + //------------ + { + in: &types.AttributeValueMemberBS{Value: [][]byte{ + {48, 49}, {50, 51}, + }}, + actual: &[][]byte{}, + expected: [][]byte{{48, 49}, {50, 51}}, + }, + { + in: &types.AttributeValueMemberNS{Value: []string{ + "123", "321", + }}, + actual: &[]int{}, + expected: []int{123, 321}, + }, + { + in: &types.AttributeValueMemberNS{Value: []string{ + "123", "321", + }}, + actual: &[]interface{}{}, + expected: []interface{}{123., 321.}, + }, + { + in: &types.AttributeValueMemberSS{Value: []string{ + "abc", "123", + }}, + actual: &[]string{}, + expected: &[]string{"abc", "123"}, + }, + { + in: &types.AttributeValueMemberSS{Value: []string{ + "abc", "123", + }}, + actual: &[]*string{}, + expected: &[]*string{aws.String("abc"), aws.String("123")}, + }, + //------------ + // Interfaces + //------------ + { + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: []byte{48, 49}, + }, + { + in: &types.AttributeValueMemberBS{Value: [][]byte{ + {48, 49}, {50, 51}, + }}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: [][]byte{{48, 49}, {50, 51}}, + }, + { + in: &types.AttributeValueMemberBOOL{Value: true}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: bool(true), + }, + { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberS{Value: "123"}, + }}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: []interface{}{"abc", "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{} + return &v + }(), + expected: map[string]interface{}{"123": "abc", "abc": "123"}, + }, + { + in: &types.AttributeValueMemberN{Value: "123"}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: float64(123), + }, + { + in: &types.AttributeValueMemberNS{Value: []string{ + "123", "321", + }}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: []float64{123., 321.}, + }, + { + in: &types.AttributeValueMemberS{Value: "123"}, + actual: func() interface{} { + var v interface{} + return &v + }(), + expected: "123", + }, + { + 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{} { + var v interface{} + return &v + }(), + expected: []string{"123", "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.AttributeValueMemberN{Value: "512"}, + actual: new(uint8), + err: &UnmarshalTypeError{ + Value: fmt.Sprintf("number overflow, 512"), + Type: reflect.TypeOf(uint8(0)), + }, + }, + // ------- + // Empty Values + // ------- + { + in: &types.AttributeValueMemberB{Value: []byte{}}, + actual: &[]byte{}, + expected: []byte{}, + }, + { + in: &types.AttributeValueMemberBS{Value: [][]byte{}}, + actual: &[][]byte{}, + expected: [][]byte{}, + }, + { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{}}, + actual: &[]interface{}{}, + expected: []interface{}{}, + }, + { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + actual: &map[string]interface{}{}, + expected: map[string]interface{}{}, + }, + { + in: &types.AttributeValueMemberN{Value: ""}, + actual: new(int), + err: fmt.Errorf("invalid syntax"), + }, + { + in: &types.AttributeValueMemberNS{Value: []string{}}, + actual: &[]string{}, + expected: []string{}, + }, + { + in: &types.AttributeValueMemberS{Value: ""}, + actual: new(string), + expected: "", + }, + { + in: &types.AttributeValueMemberSS{Value: []string{}}, + actual: &[]string{}, + expected: []string{}, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("case %d/%d", i, len(cases)), func(t *testing.T) { + err := Unmarshal(c.in, c.actual) + assertConvertTest(t, c.actual, c.expected, err, c.err) + }) + } +} + +func TestInterfaceInput(t *testing.T) { + var v interface{} + expected := []interface{}{"abc", "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 := map[string]struct { + in types.AttributeValue + actual, expected interface{} + err error + }{ + "invalid unmarshal": { + in: nil, + actual: int(0), + expected: nil, + err: &InvalidUnmarshalError{Type: reflect.TypeOf(int(0))}, + }, + } + + 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) + }) + } +} + +func TestUnmarshalListShared(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) + }) + } +} + +func TestUnmarshalListError(t *testing.T) { + cases := map[string]struct { + in []types.AttributeValue + actual, expected interface{} + err error + }{ + "invalid unmarshal": { + in: []types.AttributeValue{}, + actual: []interface{}{}, + expected: nil, + err: &InvalidUnmarshalError{Type: reflect.TypeOf([]interface{}{})}, + }, + } + + 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) + }) + } +} + +func TestUnmarshalMapShared(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) + }) + } +} + +func TestUnmarshalMapError(t *testing.T) { + cases := []struct { + in map[string]types.AttributeValue + actual, expected interface{} + err error + }{ + { + in: map[string]types.AttributeValue{}, + actual: map[string]interface{}{}, + expected: nil, + err: &InvalidUnmarshalError{Type: reflect.TypeOf(map[string]interface{}{})}, + }, + { + in: map[string]types.AttributeValue{ + "BOOL": &types.AttributeValueMemberBOOL{Value: true}, + }, + actual: &map[int]interface{}{}, + expected: nil, + err: &UnmarshalTypeError{Value: "map string key", Type: reflect.TypeOf(int(0))}, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + err := UnmarshalMap(c.in, c.actual) + assertConvertTest(t, c.actual, c.expected, err, c.err) + }) + } +} + +func TestUnmarshalListOfMaps(t *testing.T) { + type testItem struct { + Value string + Value2 int + } + + cases := map[string]struct { + in []map[string]types.AttributeValue + actual, expected interface{} + err error + }{ + "simple map conversion": { + in: []map[string]types.AttributeValue{ + { + "Value": &types.AttributeValueMemberBOOL{Value: true}, + }, + }, + actual: &[]map[string]interface{}{}, + expected: []map[string]interface{}{ + { + "Value": true, + }, + }, + }, + "attribute to struct": { + in: []map[string]types.AttributeValue{ + { + "Value": &types.AttributeValueMemberS{Value: "abc"}, + "Value2": &types.AttributeValueMemberN{Value: "123"}, + }, + }, + actual: &[]testItem{}, + expected: []testItem{ + { + Value: "abc", + Value2: 123, + }, + }, + }, + } + + 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) + }) + } +} + +type unmarshalUnmarshaler struct { + Value string + Value2 int + Value3 bool + Value4 time.Time +} + +func (u *unmarshalUnmarshaler) UnmarshalDynamoDBStreamsAttributeValue(av types.AttributeValue) error { + m, ok := av.(*types.AttributeValueMemberM) + if !ok || m == nil { + return fmt.Errorf("expected AttributeValue to be map") + } + + if v, ok := m.Value["abc"]; !ok { + return fmt.Errorf("expected `abc` map key") + } else if vv, kk := v.(*types.AttributeValueMemberS); !kk || vv == nil { + return fmt.Errorf("expected `abc` map value string") + } else { + u.Value = vv.Value + } + + if v, ok := m.Value["def"]; !ok { + return fmt.Errorf("expected `def` map key") + } else if vv, kk := v.(*types.AttributeValueMemberN); !kk || vv == nil { + return fmt.Errorf("expected `def` map value number") + } else { + n, err := strconv.ParseInt(vv.Value, 10, 64) + if err != nil { + return err + } + u.Value2 = int(n) + } + + if v, ok := m.Value["ghi"]; !ok { + return fmt.Errorf("expected `ghi` map key") + } else if vv, kk := v.(*types.AttributeValueMemberBOOL); !kk || vv == nil { + return fmt.Errorf("expected `ghi` map value number") + } else { + u.Value3 = vv.Value + } + + if v, ok := m.Value["jkl"]; !ok { + return fmt.Errorf("expected `jkl` map key") + } 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, vv.Value) + if err != nil { + return err + } + u.Value4 = t + } + + return nil +} + +func TestUnmarshalUnmashaler(t *testing.T) { + u := &unmarshalUnmarshaler{} + 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"}, + }, + } + + err := Unmarshal(av, u) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + if e, a := "value", u.Value; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := 123, u.Value2; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := true, u.Value3; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := testDate, u.Value4; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeUseNumber(t *testing.T) { + u := map[string]interface{}{} + 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(o *DecoderOptions) { + o.UseNumber = true + }) + err := decoder.Decode(av, &u) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + if e, a := "value", u["abc"]; e != a { + t.Errorf("expect %v, got %v", e, a) + } + n := u["def"].(Number) + if e, a := "123", n.String(); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := true, u["ghi"]; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeUseNumberNumberSet(t *testing.T) { + u := map[string]interface{}{} + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "ns": &types.AttributeValueMemberNS{ + Value: []string{ + "123", "321", + }, + }, + }, + } + + decoder := NewDecoder(func(o *DecoderOptions) { + o.UseNumber = true + }) + err := decoder.Decode(av, &u) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + ns := u["ns"].([]Number) + + if e, a := "123", ns[0].String(); e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "321", ns[1].String(); e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeEmbeddedPointerStruct(t *testing.T) { + type B struct { + Bint int + } + type C struct { + Cint int + } + type A struct { + Aint int + *B + *C + } + av := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Aint": &types.AttributeValueMemberN{Value: "321"}, + "Bint": &types.AttributeValueMemberN{Value: "123"}, + }, + } + decoder := NewDecoder() + a := A{} + err := decoder.Decode(av, &a) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if e, a := 321, a.Aint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + // Embedded pointer struct can be created automatically. + if e, a := 123, a.Bint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + // But not for absent fields. + if a.C != nil { + t.Errorf("expect nil, got %v", a.C) + } +} + +func TestDecodeBooleanOverlay(t *testing.T) { + type BooleanOverlay bool + + av := &types.AttributeValueMemberBOOL{Value: true} + + decoder := NewDecoder() + + var v BooleanOverlay + + err := decoder.Decode(av, &v) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if e, a := BooleanOverlay(true), v; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeUnixTime(t *testing.T) { + type A struct { + Normal time.Time + Tagged time.Time `dynamodbav:",unixtime"` + Typed UnixTime + } + + expect := A{ + Normal: time.Unix(123, 0).UTC(), + Tagged: time.Unix(456, 0), + Typed: UnixTime(time.Unix(789, 0)), + } + + input := &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{} + + err := Unmarshal(input, &actual) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if e, a := expect, actual; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestDecodeAliasedUnixTime(t *testing.T) { + type A struct { + Normal AliasedTime + Tagged AliasedTime `dynamodbav:",unixtime"` + } + + expect := A{ + Normal: AliasedTime(time.Unix(123, 0).UTC()), + Tagged: AliasedTime(time.Unix(456, 0)), + } + + input := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Normal": &types.AttributeValueMemberS{Value: "1970-01-01T00:02:03Z"}, + "Tagged": &types.AttributeValueMemberN{Value: "456"}, + }, + } + actual := A{} + + err := Unmarshal(input, &actual) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if expect != actual { + t.Errorf("expect %v, got %v", expect, actual) + } +} + +// see github issue #1594 +func TestDecodeArrayType(t *testing.T) { + cases := []struct { + to, from interface{} + }{ + { + &[2]int{1, 2}, + &[2]int{}, + }, + { + &[2]int64{1, 2}, + &[2]int64{}, + }, + { + &[2]byte{1, 2}, + &[2]byte{}, + }, + { + &[2]bool{true, false}, + &[2]bool{}, + }, + { + &[2]string{"1", "2"}, + &[2]string{}, + }, + { + &[2][]string{{"1", "2"}}, + &[2][]string{}, + }, + } + + for _, c := range cases { + marshaled, err := Marshal(c.to) + if err != nil { + t.Errorf("expected no error, but received %v", err) + } + + if err = Unmarshal(marshaled, c.from); err != nil { + t.Errorf("expected no error, but received %v", err) + } + + if diff := cmp.Diff(c.to, c.from); len(diff) != 0 { + t.Errorf("expected match\n:%s", diff) + } + } +} + +func TestDecoderFieldByIndex(t *testing.T) { + type ( + Middle struct{ Inner int } + Outer struct{ *Middle } + ) + var outer Outer + + outerType := reflect.TypeOf(outer) + outerValue := reflect.ValueOf(&outer) + outerFields := unionStructFields(outerType, structFieldOptions{}) + innerField, _ := outerFields.FieldByName("Inner") + + f := decoderFieldByIndex(outerValue.Elem(), innerField.Index) + if outer.Middle == nil { + t.Errorf("expected outer.Middle to be non-nil") + } + if f.Kind() != reflect.Int || f.Int() != int64(outer.Inner) { + t.Error("expected f to be an int with value equal to outer.Inner") + } +} diff --git a/feature/dynamodbstreams/attributevalue/doc.go b/feature/dynamodbstreams/attributevalue/doc.go new file mode 100644 index 00000000000..1e282e0c198 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/doc.go @@ -0,0 +1,67 @@ +// Package attributevalue provides marshaling and unmarshaling utilities to +// convert between Go types and Amazon DynamoDB Streams AttributeValues. +// +// These utilities allow you to marshal slices, maps, structs, and scalar +// values to and from the 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 DynamoDBStreams +// AttributeValue. See the feature/dynamodb/attributevalue package for +// converting to DynamoDB AttributeValue types. +// +// Converting AttributeValue between DynamoDB and DynamoDBStreams +// +// The FromDynamoDBMap, FromDynamoDBList, and FromDynamoDB functions provide +// the conversion utilities to convert a DynamoDB AttributeValue type to a +// DynamoDBStreams AttributeValue type. Use these utilities when you need to +// convert the AttributeValue type between the two APIs. +// +// AttributeValue Unmarshaling +// +// 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 Items result from the DynamoDBStreams +// GetRecords operation. The items returned will be unmarshaled into the slice +// of the Records struct. +// +// type Record struct { +// ID string +// URLs []string +// } +// +// //... +// +// result, err := client.GetRecords(context.Context(), &dynamodbstreams.GetRecordsInput{ +// ShardIterator: &shardIterator, +// }) +// if err != nil { +// return fmt.Errorf("failed to get records from stream, %w", err) +// } +// +// var records []Record +// for _, ddbRecord := range result.Records { +// if record.DynamoDB == nil { +// continue +// } +// +// var record +// err := attributevalue.UnmarshalMap(ddbRecord.NewImage, &record) +// if err != nil { +// return fmt.Errorf("failed to unmarshal record, %w", err)) +// } +// records = append(records, record) +// } +// +// Struct tags +// +// 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. +// +// See the Marshal and Unmarshal function for information on how struct tags +// and fields are marshaled and unmarshaled. +package attributevalue diff --git a/feature/dynamodbstreams/attributevalue/empty_collections_test.go b/feature/dynamodbstreams/attributevalue/empty_collections_test.go new file mode 100644 index 00000000000..75967fb041f --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/empty_collections_test.go @@ -0,0 +1,938 @@ +package attributevalue + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" + "github.com/google/go-cmp/cmp" +) + +type testEmptyCollectionsNumericalScalars struct { + String string + + Uint8 uint8 + Uint16 uint16 + Uint32 uint32 + Uint64 uint64 + + Int8 int8 + Int16 int16 + Int32 int32 + Int64 int64 + + Float32 float32 + Float64 float64 +} + +type testEmptyCollectionsOmittedNumericalScalars struct { + String string `dynamodbav:",omitempty"` + + Uint8 uint8 `dynamodbav:",omitempty"` + Uint16 uint16 `dynamodbav:",omitempty"` + Uint32 uint32 `dynamodbav:",omitempty"` + Uint64 uint64 `dynamodbav:",omitempty"` + + Int8 int8 `dynamodbav:",omitempty"` + Int16 int16 `dynamodbav:",omitempty"` + Int32 int32 `dynamodbav:",omitempty"` + Int64 int64 `dynamodbav:",omitempty"` + + Float32 float32 `dynamodbav:",omitempty"` + 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 + + PtrUint8 *uint8 + PtrUint16 *uint16 + PtrUint32 *uint32 + PtrUint64 *uint64 + + PtrInt8 *int8 + PtrInt16 *int16 + PtrInt32 *int32 + PtrInt64 *int64 + + PtrFloat32 *float32 + PtrFloat64 *float64 +} + +type testEmptyCollectionsOmittedPtrNumericalScalars struct { + PtrString *string `dynamodbav:",omitempty"` + + PtrUint8 *uint8 `dynamodbav:",omitempty"` + PtrUint16 *uint16 `dynamodbav:",omitempty"` + PtrUint32 *uint32 `dynamodbav:",omitempty"` + PtrUint64 *uint64 `dynamodbav:",omitempty"` + + PtrInt8 *int8 `dynamodbav:",omitempty"` + PtrInt16 *int16 `dynamodbav:",omitempty"` + PtrInt32 *int32 `dynamodbav:",omitempty"` + PtrInt64 *int64 `dynamodbav:",omitempty"` + + PtrFloat32 *float32 `dynamodbav:",omitempty"` + 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 + ByteSlice []byte + ByteArray [4]byte + ZeroArray [0]byte + BinarySet [][]byte `dynamodbav:",binaryset"` + NumberSet []int `dynamodbav:",numberset"` + StringSet []string `dynamodbav:",stringset"` +} + +type testEmptyCollectionTypesOmitted struct { + Map map[string]string `dynamodbav:",omitempty"` + Slice []string `dynamodbav:",omitempty"` + ByteSlice []byte `dynamodbav:",omitempty"` + ByteArray [4]byte `dynamodbav:",omitempty"` + ZeroArray [0]byte `dynamodbav:",omitempty"` + BinarySet [][]byte `dynamodbav:",binaryset,omitempty"` + NumberSet []int `dynamodbav:",numberset,omitempty"` + 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 +} + +type testEmptyCollectionStructOmitted struct { + Slice []string `dynamodbav:",omitempty"` +} + +var sharedEmptyCollectionsTestCases = map[string]struct { + 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.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.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{}, + expected: testEmptyCollectionsNumericalScalars{ + 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, + }, + }, + "omittable scalars with zero value": { + in: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + actual: &testEmptyCollectionsOmittedNumericalScalars{}, + expected: testEmptyCollectionsOmittedNumericalScalars{}, + }, + "omittable 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: &testEmptyCollectionsOmittedNumericalScalars{}, + expected: testEmptyCollectionsOmittedNumericalScalars{ + 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, + }, + }, + "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.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.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{}, + expected: testEmptyCollectionsPtrScalars{ + 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), + }, + }, + "pointer scalars non-nil non-zero": { + 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{}, + expected: testEmptyCollectionsPtrScalars{ + PtrString: aws.String("test string"), + 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), + }, + }, + "omittable nil pointer scalars": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{}, + }, + actual: &testEmptyCollectionsOmittedPtrNumericalScalars{}, + expected: testEmptyCollectionsOmittedPtrNumericalScalars{}, + }, + "omittable non-nil pointer to scalars with zero value": { + 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{}, + expected: testEmptyCollectionsOmittedPtrNumericalScalars{ + 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), + }, + }, + "omittable non-nil pointer to non-zero scalar": { + 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{}, + expected: testEmptyCollectionsOmittedPtrNumericalScalars{ + 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 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)}, + "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.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{}, + expected: testEmptyCollectionTypes{ + Map: map[string]string{}, + Slice: []string{}, + ByteSlice: []byte{}, + ByteArray: [4]byte{}, + ZeroArray: [0]byte{}, + BinarySet: [][]byte{}, + NumberSet: []int{}, + StringSet: []string{}, + }, + }, + "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}}, + "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{}, + expected: testEmptyCollectionTypes{ + 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"}, + }, + }, + "omittable maps slices nil values": { + 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.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{}, + expected: testEmptyCollectionTypesOmitted{ + Map: map[string]string{}, + Slice: []string{}, + ByteSlice: []byte{}, + ByteArray: [4]byte{}, + BinarySet: [][]byte{}, + NumberSet: []int{}, + StringSet: []string{}, + }, + }, + "omittable 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"}}, + }, + }, + actual: &testEmptyCollectionTypesOmitted{}, + expected: testEmptyCollectionTypesOmitted{ + 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"}, + }, + }, + "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.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "0"}, + }, + }, + "PtrStruct": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{}, + expected: struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{}, + }, + "structs with members non-zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "1"}, + }, + }, + "PtrStruct": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Int": &types.AttributeValueMemberN{Value: "1"}, + }, + }, + }, + }, + actual: &struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{}, + expected: struct { + Struct testEmptyCollectionStruct + PtrStruct *testEmptyCollectionStruct + }{ + Struct: testEmptyCollectionStruct{Int: 1}, + PtrStruct: &testEmptyCollectionStruct{Int: 1}, + }, + }, + "struct with omittable members zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + "PtrStruct": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + actual: &struct { + Struct testEmptyCollectionStructOmitted + PtrStruct *testEmptyCollectionStructOmitted + }{}, + expected: struct { + Struct testEmptyCollectionStructOmitted + PtrStruct *testEmptyCollectionStructOmitted + }{}, + }, + "omittable struct with omittable members zero value": { + in: &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Struct": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}}, + }, + }, + actual: &struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + PtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{}, + expected: struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + PtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{}, + }, + "omittable struct with omittable members non-zero value": { + 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": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Slice": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "test"}, + }}, + }, + }, + }, + }, + actual: &struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + InitPtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{}, + expected: struct { + Struct testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + InitPtrStruct *testEmptyCollectionStructOmitted `dynamodbav:",omitempty"` + }{ + Struct: testEmptyCollectionStructOmitted{Slice: []string{"test"}}, + InitPtrStruct: &testEmptyCollectionStructOmitted{Slice: []string{"test"}}, + }, + }, +} + +func TestMarshalEmptyCollections(t *testing.T) { + for name, c := range sharedEmptyCollectionsTestCases { + t.Run(name, func(t *testing.T) { + av, err := Marshal(c.expected) + in := c.in + if c.inMarshal != nil { + in = c.inMarshal + } + assertConvertTest(t, av, in, err, c.err) + }) + } +} + +func TestEmptyCollectionsSpecialCases(t *testing.T) { + // ptr string non nil with empty value + + type SpecialCases struct { + PtrString *string + OmittedString string `dynamodbav:",omitempty"` + OmittedPtrString *string `dynamodbav:",omitempty"` + } + + expectedEncode := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "PtrString": &types.AttributeValueMemberS{Value: ""}, + }, + } + expectedDecode := SpecialCases{} + + actualEncode, err := Marshal(&SpecialCases{ + PtrString: aws.String(""), + OmittedString: "", + OmittedPtrString: nil, + }) + if err != nil { + t.Fatalf("expected no err got %v", err) + } + if diff := cmp.Diff(expectedEncode, actualEncode); len(diff) != 0 { + t.Errorf("expected encode match\n%s", diff) + } + + var actualDecode SpecialCases + var av types.AttributeValue + err = Unmarshal(av, &actualDecode) + if err != nil { + t.Fatalf("expected no err got %v", err) + } + if diff := cmp.Diff(expectedDecode, actualDecode); len(diff) != 0 { + t.Errorf("expected dencode match\n%s", diff) + } +} + +func TestUnmarshalEmptyCollections(t *testing.T) { + for name, c := range sharedEmptyCollectionsTestCases { + t.Run(name, func(t *testing.T) { + err := Unmarshal(c.in, c.actual) + assertConvertTest(t, c.actual, c.expected, err, c.err) + }) + } +} diff --git a/feature/dynamodbstreams/attributevalue/encode.go b/feature/dynamodbstreams/attributevalue/encode.go new file mode 100644 index 00000000000..462e4f705d9 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/encode.go @@ -0,0 +1,671 @@ +package attributevalue + +import ( + "fmt" + "reflect" + "strconv" + "time" + + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" +) + +// An UnixTime provides aliasing of time.Time into a type that when marshaled +// 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 +// want to have your time value marshaled as Unix time in seconds into a number +// attribute type instead of the default time.RFC3339Nano. +// +// 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. +// +// Also, important to note: the default UnixTime implementation of the Marshaler +// interface will marshal into an attribute of type of number; therefore, +// it may not be used as a sort key if the attribute value is of type string. Further, +// the time.RFC3339Nano format removes trailing zeros from the seconds field +// and thus may not sort correctly once formatted. +type UnixTime time.Time + +// MarshalDynamoDBStreamsAttributeValue implements the Marshaler interface so that +// 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) MarshalDynamoDBStreamsAttributeValue() (types.AttributeValue, error) { + return &types.AttributeValueMemberN{ + Value: strconv.FormatInt(time.Time(e).Unix(), 10), + }, nil +} + +// UnmarshalDynamoDBStreamsAttributeValue implements the Unmarshaler interface so that +// 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) UnmarshalDynamoDBStreamsAttributeValue(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 + } + + *e = UnixTime(t) + return nil +} + +// A Marshaler is an interface to provide custom marshaling of Go value types +// to AttributeValues. Use this to provide custom logic determining how a +// Go Value type should be marshaled. +// +// type CustomIntType struct { +// Value Int +// } +// func (m *CustomIntType) MarshalDynamoDBStreamsAttributeValue() (types.AttributeValue, error) { +// return &types.AttributeValueMemberN{ +// Value: strconv.Itoa(m.Value), +// }, nil +// } +// +type Marshaler interface { + MarshalDynamoDBStreamsAttributeValue() (types.AttributeValue, error) +} + +// 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 +// 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. +// +// 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) { + return NewEncoder().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. +// +// This is useful for APIs such as PutItem. +func MarshalMap(in interface{}) (map[string]types.AttributeValue, error) { + av, err := NewEncoder().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. +func MarshalList(in interface{}) ([]types.AttributeValue, error) { + av, err := NewEncoder().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 { + // 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 { + options EncoderOptions +} + +// NewEncoder creates a new Encoder with default configuration. Use +// the `opts` functional options to override the default configuration. +func NewEncoder(optFns ...func(*EncoderOptions)) *Encoder { + options := EncoderOptions{ + NullEmptySets: true, + } + for _, fn := range optFns { + fn(&options) + } + + 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) { + return e.encode(reflect.ValueOf(in), tag{}) +} + +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 av, err := tryMarshaler(v); err != nil { + return nil, err + } else if av != nil { + return av, nil + } + } + + switch v.Kind() { + case reflect.Invalid: + 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(v, fieldTag) + + case reflect.Map: + return e.encodeMap(v, fieldTag) + + case reflect.Slice, reflect.Array: + return e.encodeSlice(v, fieldTag) + + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + // skip unsupported types + return nil, nil + + default: + return e.encodeScalar(v, fieldTag) + } +} + +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).MarshalDynamoDBStreamsAttributeValue() + } + return &types.AttributeValueMemberS{Value: t.Format(time.RFC3339Nano)}, nil + } + + m := &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{}} + fields := unionStructFields(v.Type(), structFieldOptions{ + TagKey: e.options.TagKey, + }) + for _, f := range fields.All() { + if f.Name == "" { + return nil, &InvalidMarshalError{msg: "map key cannot be empty"} + } + + fv, found := encoderFieldByIndex(v, f.Index) + if !found { + continue + } + + elem, err := e.encode(fv, f.tag) + if err != nil { + return nil, err + } else if elem == nil { + continue + } + + m.Value[f.Name] = elem + } + + return m, nil +} + +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 nil, &InvalidMarshalError{msg: "map key cannot be empty"} + } + + elemVal := v.MapIndex(key) + elem, err := e.encode(elemVal, tag{ + OmitEmpty: fieldTag.OmitEmptyElem, + NullEmpty: fieldTag.NullEmptyElem, + }) + if err != nil { + return nil, err + } else if elem == nil { + continue + } + + m.Value[keyName] = elem + } + + return m, 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 + } + + 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 + } + + } else if fieldTag.AsNumSet { // Number Set + if v.Len() == 0 && e.options.NullEmptySets { + return encodeNull(), nil + } + + 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 + } + + } else if fieldTag.AsStrSet { // String Set + if v.Len() == 0 && e.options.NullEmptySets { + return encodeNull(), 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 + } + + } 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 + } + } + + if err := e.encodeListElems(v, fieldTag, setElemFn); err != nil { + return nil, err + } + + return av, nil +} + +func (e *Encoder) encodeListElems(v reflect.Value, fieldTag tag, setElem func(types.AttributeValue) error) error { + for i := 0; i < v.Len(); i++ { + elem, err := e.encode(v.Index(i), tag{ + OmitEmpty: fieldTag.OmitEmptyElem, + NullEmpty: fieldTag.NullEmptyElem, + }) + if err != nil { + return err + } else if elem == nil { + continue + } + + if err := setElem(elem); err != nil { + return err + } + } + + return nil +} + +// 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 { + return &types.AttributeValueMemberS{Value: v.String()}, nil + } + return &types.AttributeValueMemberN{Value: v.String()}, nil + } + + switch v.Kind() { + case reflect.Bool: + return &types.AttributeValueMemberBOOL{Value: v.Bool()}, nil + + case reflect.String: + return e.encodeString(v) + + default: + // Fallback to encoding numbers, will return invalid type if not supported + av, err := e.encodeNumber(v) + if err != nil { + return nil, err + } + + n, isNumber := av.(*types.AttributeValueMemberN) + if fieldTag.AsString && isNumber { + return &types.AttributeValueMemberS{Value: n.Value}, nil + } + return av, nil + } +} + +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 nil, nil + } + + return &types.AttributeValueMemberN{Value: out}, nil +} + +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() + return &types.AttributeValueMemberS{Value: s}, nil + + default: + return nil, nil + } +} + +func encodeInt(i int64) string { + return strconv.FormatInt(i, 10) +} +func encodeUint(u uint64) string { + return strconv.FormatUint(u, 10) +} +func encodeFloat(f float64, bitSize int) string { + return strconv.FormatFloat(f, 'f', -1, bitSize) +} +func encodeNull() types.AttributeValue { + return &types.AttributeValueMemberNULL{Value: true} +} + +// encoderFieldByIndex finds the field with the provided nested index +func encoderFieldByIndex(v reflect.Value, index []int) (reflect.Value, bool) { + for i, x := range index { + if i > 0 && v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { + if v.IsNil() { + return reflect.Value{}, false + } + v = v.Elem() + } + v = v.Field(x) + } + return v, true +} + +func valueElem(v reflect.Value) reflect.Value { + switch v.Kind() { + case reflect.Interface, reflect.Ptr: + for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr { + v = v.Elem() + } + } + + return v +} + +func 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: + return v.IsNil() + case reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func 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 nil, nil + } + + if m, ok := v.Interface().(Marshaler); ok { + return m.MarshalDynamoDBStreamsAttributeValue() + } + + 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 { + msg string +} + +// Error returns the string representation of the error. +// satisfying the error interface +func (e *InvalidMarshalError) Error() string { + return fmt.Sprintf("marshal failed, %s", e.msg) +} diff --git a/feature/dynamodbstreams/attributevalue/encode_test.go b/feature/dynamodbstreams/attributevalue/encode_test.go new file mode 100644 index 00000000000..989d08c7f10 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/encode_test.go @@ -0,0 +1,366 @@ +package attributevalue + +import ( + "reflect" + "strconv" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" + "github.com/google/go-cmp/cmp" +) + +func TestMarshalShared(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) + }) + } +} + +func TestMarshalListShared(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) + }) + } +} + +func TestMarshalMapShared(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) + }) + } +} + +type marshalMarshaler struct { + Value string + Value2 int + Value3 bool + Value4 time.Time +} + +func (m *marshalMarshaler) MarshalDynamoDBStreamsAttributeValue() (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) { + m := &marshalMarshaler{ + Value: "value", + Value2: 123, + Value3: true, + Value4: testDate, + } + + 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"}, + }, + } + + 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) + } +} + +type testOmitEmptyElemListStruct struct { + Values []string `dynamodbav:",omitemptyelem"` +} + +type testOmitEmptyElemMapStruct struct { + Values map[string]interface{} `dynamodbav:",omitemptyelem"` +} + +func TestMarshalListOmitEmptyElem(t *testing.T) { + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Values": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "abc"}, + &types.AttributeValueMemberS{Value: "123"}, + }}, + }, + } + + m := testOmitEmptyElemListStruct{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 TestMarshalMapOmitEmptyElem(t *testing.T) { + 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"}, + }}, + }, + } + + m := testOmitEmptyElemMapStruct{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) + } +} + +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) + } +} + +type testOmitEmptyScalar struct { + IntZero int `dynamodbav:",omitempty"` + IntPtrNil *int `dynamodbav:",omitempty"` + IntPtrSetZero *int `dynamodbav:",omitempty"` +} + +func TestMarshalOmitEmpty(t *testing.T) { + expect := &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "IntPtrSetZero": &types.AttributeValueMemberN{Value: "0"}, + }, + } + + m := testOmitEmptyScalar{IntPtrSetZero: aws.Int(0)} + + 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) + } +} + +func TestEncodeEmbeddedPointerStruct(t *testing.T) { + type B struct { + Bint int + } + type C struct { + Cint int + } + type A struct { + Aint int + *B + *C + } + a := A{Aint: 321, B: &B{123}} + if e, a := 321, a.Aint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := 123, a.Bint; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if a.C != nil { + t.Errorf("expect nil, got %v", a.C) + } + + actual, err := Marshal(a) + if err != nil { + t.Errorf("expect nil, got %v", err) + } + 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) { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestEncodeUnixTime(t *testing.T) { + type A struct { + Normal time.Time + Tagged time.Time `dynamodbav:",unixtime"` + Typed UnixTime + } + + a := A{ + Normal: time.Unix(123, 0).UTC(), + Tagged: time.Unix(456, 0), + Typed: UnixTime(time.Unix(789, 0)), + } + + actual, err := Marshal(a) + if err != nil { + t.Errorf("expect nil, got %v", err) + } + 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) { + t.Errorf("expect %v, got %v", e, a) + } +} + +type AliasedTime time.Time + +func TestEncodeAliasedUnixTime(t *testing.T) { + type A struct { + Normal AliasedTime + Tagged AliasedTime `dynamodbav:",unixtime"` + } + + a := A{ + Normal: AliasedTime(time.Unix(123, 0).UTC()), + Tagged: AliasedTime(time.Unix(456, 0)), + } + + actual, err := Marshal(a) + if err != nil { + t.Errorf("expect no err, got %v", err) + } + 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) { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestEncoderFieldByIndex(t *testing.T) { + type ( + Middle struct{ Inner int } + Outer struct{ *Middle } + ) + + // nil embedded struct + outer := Outer{} + outerFields := unionStructFields(reflect.TypeOf(outer), structFieldOptions{}) + innerField, _ := outerFields.FieldByName("Inner") + + _, found := encoderFieldByIndex(reflect.ValueOf(&outer).Elem(), innerField.Index) + if found != false { + t.Error("expected found to be false when embedded struct is nil") + } + + // non-nil embedded struct + outer = Outer{Middle: &Middle{Inner: 3}} + outerFields = unionStructFields(reflect.TypeOf(outer), structFieldOptions{}) + innerField, _ = outerFields.FieldByName("Inner") + + f, found := encoderFieldByIndex(reflect.ValueOf(&outer).Elem(), innerField.Index) + if !found { + t.Error("expected found to be true") + } + if f.Kind() != reflect.Int || f.Int() != int64(outer.Inner) { + t.Error("expected f to be of kind Int with value equal to outer.Inner") + } +} diff --git a/feature/dynamodbstreams/attributevalue/field.go b/feature/dynamodbstreams/attributevalue/field.go new file mode 100644 index 00000000000..7abd3479a96 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/field.go @@ -0,0 +1,275 @@ +package attributevalue + +import ( + "reflect" + "sort" +) + +type field struct { + tag + + Name string + NameFromTag bool + + Index []int + Type reflect.Type +} + +func buildField(pIdx []int, i int, sf reflect.StructField, fieldTag tag) field { + f := field{ + Name: sf.Name, + Type: sf.Type, + tag: fieldTag, + } + if len(fieldTag.Name) != 0 { + f.NameFromTag = true + f.Name = fieldTag.Name + } + + f.Index = make([]int, len(pIdx)+1) + copy(f.Index, pIdx) + f.Index[len(pIdx)] = i + + return f +} + +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 +} + +// unionStructFields returns a list of fields for the given type. Type info is cached +// to avoid repeated calls into the reflect package +func unionStructFields(t reflect.Type, opts structFieldOptions) *cachedFields { + if cached, ok := fieldCache.Load(t); ok { + return cached + } + + f := enumFields(t, opts) + sort.Sort(fieldsByName(f)) + f = visibleFields(f) + + fs := &cachedFields{ + fields: f, + fieldsByName: make(map[string]int, len(f)), + } + for i, f := range fs.fields { + fs.fieldsByName[f.Name] = i + } + + cached, _ := fieldCache.LoadOrStore(t, fs) + return cached +} + +// enumFields will recursively iterate through a structure and its nested +// anonymous fields. +// +// Based on the enoding/json struct field enumeration of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go typeField func. +func enumFields(t reflect.Type, opts structFieldOptions) []field { + // Fields to explore + current := []field{} + next := []field{{Type: t}} + + // count of queued names + count := map[reflect.Type]int{} + nextCount := map[reflect.Type]int{} + + visited := map[reflect.Type]struct{}{} + fields := []field{} + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, map[reflect.Type]int{} + + for _, f := range current { + if _, ok := visited[f.Type]; ok { + continue + } + visited[f.Type] = struct{}{} + + for i := 0; i < f.Type.NumField(); i++ { + sf := f.Type.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { + // Ignore unexported and non-anonymous fields + // unexported but anonymous field may still be used if + // the type has exported nested fields + continue + } + + fieldTag := tag{} + fieldTag.parseAVTag(sf.Tag) + // Because MarshalOptions.TagKey must be explicitly set. + if opts.TagKey != "" && fieldTag == (tag{}) { + fieldTag.parseStructTag(opts.TagKey, sf.Tag) + } + + if fieldTag.Ignore { + continue + } + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + + structField := buildField(f.Index, i, sf, fieldTag) + structField.Type = ft + + if !sf.Anonymous || ft.Kind() != reflect.Struct { + fields = append(fields, structField) + if count[f.Type] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + fields = append(fields, structField) + } + continue + } + + // Record new anon struct to explore next round + nextCount[ft]++ + if nextCount[ft] == 1 { + next = append(next, structField) + } + } + } + } + + return fields +} + +// visibleFields will return a slice of fields which are visible based on +// Go's standard visiblity rules with the exception of ties being broken +// by depth and struct tag naming. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go typeField func. +func visibleFields(fields []field) []field { + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with JSON tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := fields[:0] + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.Name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.Name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, fi) + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + fields = out + sort.Sort(fieldsByIndex(fields)) + + return fields +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's embedding rules, modified by the presence of +// JSON tags. If there are multiple top-level fields, the boolean +// will be false: This condition is an error in Go and we skip all +// the fields. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go dominantField func. +func dominantField(fields []field) (field, bool) { + // The fields are sorted in increasing index-length order. The winner + // must therefore be one with the shortest index length. Drop all + // longer entries, which is easy: just truncate the slice. + length := len(fields[0].Index) + tagged := -1 // Index of first tagged field. + for i, f := range fields { + if len(f.Index) > length { + fields = fields[:i] + break + } + if f.NameFromTag { + if tagged >= 0 { + // Multiple tagged fields at the same level: conflict. + // Return no field. + return field{}, false + } + tagged = i + } + } + if tagged >= 0 { + return fields[tagged], true + } + // All remaining fields have the same length. If there's more than one, + // we have a conflict (two fields named "X" at the same level) and we + // return no field. + if len(fields) > 1 { + return field{}, false + } + return fields[0], true +} + +// fieldsByName sorts field by name, breaking ties with depth, +// then breaking ties with "name came from json tag", then +// breaking ties with index sequence. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go fieldsByName type. +type fieldsByName []field + +func (x fieldsByName) Len() int { return len(x) } + +func (x fieldsByName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x fieldsByName) Less(i, j int) bool { + if x[i].Name != x[j].Name { + return x[i].Name < x[j].Name + } + if len(x[i].Index) != len(x[j].Index) { + return len(x[i].Index) < len(x[j].Index) + } + if x[i].NameFromTag != x[j].NameFromTag { + return x[i].NameFromTag + } + return fieldsByIndex(x).Less(i, j) +} + +// fieldsByIndex sorts field by index sequence. +// +// Based on the enoding/json field filtering of the Go Stdlib +// https://golang.org/src/encoding/json/encode.go fieldsByIndex type. +type fieldsByIndex []field + +func (x fieldsByIndex) Len() int { return len(x) } + +func (x fieldsByIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x fieldsByIndex) Less(i, j int) bool { + for k, xik := range x[i].Index { + if k >= len(x[j].Index) { + return false + } + if xik != x[j].Index[k] { + return xik < x[j].Index[k] + } + } + return len(x[i].Index) < len(x[j].Index) +} diff --git a/feature/dynamodbstreams/attributevalue/field_cache.go b/feature/dynamodbstreams/attributevalue/field_cache.go new file mode 100644 index 00000000000..60a9d9c7499 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/field_cache.go @@ -0,0 +1,45 @@ +package attributevalue + +import ( + "strings" + "sync" +) + +var fieldCache fieldCacher + +type fieldCacher struct { + cache sync.Map +} + +func (c *fieldCacher) Load(t interface{}) (*cachedFields, bool) { + if v, ok := c.cache.Load(t); ok { + return v.(*cachedFields), true + } + return nil, false +} + +func (c *fieldCacher) LoadOrStore(t interface{}, fs *cachedFields) (*cachedFields, bool) { + v, ok := c.cache.LoadOrStore(t, fs) + return v.(*cachedFields), ok +} + +type cachedFields struct { + fields []field + fieldsByName map[string]int +} + +func (f *cachedFields) All() []field { + return f.fields +} + +func (f *cachedFields) FieldByName(name string) (field, bool) { + if i, ok := f.fieldsByName[name]; ok { + return f.fields[i], ok + } + for _, f := range f.fields { + if strings.EqualFold(f.Name, name) { + return f, true + } + } + return field{}, false +} diff --git a/feature/dynamodbstreams/attributevalue/field_test.go b/feature/dynamodbstreams/attributevalue/field_test.go new file mode 100644 index 00000000000..82a09d6cf99 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/field_test.go @@ -0,0 +1,128 @@ +package attributevalue + +import ( + "reflect" + "testing" +) + +type testUnionValues struct { + Name string + Value interface{} +} + +type unionSimple struct { + A int + B string + C []string +} + +type unionComplex struct { + unionSimple + A int +} + +type unionTagged struct { + A int `json:"A"` +} + +type unionTaggedComplex struct { + unionSimple + unionTagged + B string +} + +func TestUnionStructFields(t *testing.T) { + var cases = []struct { + in interface{} + expect []testUnionValues + }{ + { + in: unionSimple{1, "2", []string{"abc"}}, + expect: []testUnionValues{ + {"A", 1}, + {"B", "2"}, + {"C", []string{"abc"}}, + }, + }, + { + in: unionComplex{ + unionSimple: unionSimple{1, "2", []string{"abc"}}, + A: 2, + }, + expect: []testUnionValues{ + {"B", "2"}, + {"C", []string{"abc"}}, + {"A", 2}, + }, + }, + { + in: unionTaggedComplex{ + unionSimple: unionSimple{1, "2", []string{"abc"}}, + unionTagged: unionTagged{3}, + B: "3", + }, + expect: []testUnionValues{ + {"C", []string{"abc"}}, + {"A", 3}, + {"B", "3"}, + }, + }, + } + + for i, c := range cases { + v := reflect.ValueOf(c.in) + + fields := unionStructFields(v.Type(), structFieldOptions{TagKey: "json"}) + for j, f := range fields.All() { + expected := c.expect[j] + if e, a := expected.Name, f.Name; e != a { + t.Errorf("%d:%d expect %v, got %v", i, j, e, f) + } + actual := v.FieldByIndex(f.Index).Interface() + if e, a := expected.Value, actual; !reflect.DeepEqual(e, a) { + t.Errorf("%d:%d expect %v, got %v", i, j, e, f) + } + } + } +} + +func TestCachedFields(t *testing.T) { + type myStruct struct { + Dog int + CAT string + bird bool + } + + fields := unionStructFields(reflect.TypeOf(myStruct{}), structFieldOptions{}) + + const expectedNumFields = 2 + if numFields := len(fields.All()); numFields != expectedNumFields { + t.Errorf("expected number of fields to be %d but got %d", expectedNumFields, numFields) + } + + cases := []struct { + Name string + FieldName string + Found bool + }{ + {"Dog", "Dog", true}, + {"dog", "Dog", true}, + {"DOG", "Dog", true}, + {"Yorkie", "", false}, + {"Cat", "CAT", true}, + {"cat", "CAT", true}, + {"CAT", "CAT", true}, + {"tiger", "", false}, + {"bird", "", false}, + } + + for _, c := range cases { + f, found := fields.FieldByName(c.Name) + if found != c.Found { + t.Errorf("expected found to be %v but got %v", c.Found, found) + } + if found && f.Name != c.FieldName { + t.Errorf("expected field name to be %s but got %s", c.FieldName, f.Name) + } + } +} diff --git a/feature/dynamodbstreams/attributevalue/go.mod b/feature/dynamodbstreams/attributevalue/go.mod new file mode 100644 index 00000000000..378511e26bf --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/go.mod @@ -0,0 +1,18 @@ +module github.com/aws/aws-sdk-go-v2/feature/dynamodbstreams/attributevalue + +go 1.15 + +require ( + 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 => ../../../ + +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/dynamodbstreams/attributevalue/go.sum b/feature/dynamodbstreams/attributevalue/go.sum new file mode 100644 index 00000000000..5409e55e671 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/go.sum @@ -0,0 +1,37 @@ +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/awslabs/smithy-go v0.4.0 h1:El0KyKn4zdM3pLuWJlgoeitQuu/mjwUPssr7L3xu3vs= +github.com/awslabs/smithy-go v0.4.0/go.mod h1:hPOQwnmBLHsUphH13tVSjQhTAFma0/0XoZGbBcOuABI= +github.com/awslabs/smithy-go v0.4.1-0.20201208232924-b8cdbaa577ff h1:mtSekcc5R2mJG5+cdIlL15WD//Lobtzil5hkcr8WhiA= +github.com/awslabs/smithy-go v0.4.1-0.20201208232924-b8cdbaa577ff/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/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= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/feature/dynamodbstreams/attributevalue/marshaler_examples_test.go b/feature/dynamodbstreams/attributevalue/marshaler_examples_test.go new file mode 100644 index 00000000000..f28c2d90f49 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/marshaler_examples_test.go @@ -0,0 +1,94 @@ +package attributevalue_test + +import ( + "fmt" + "reflect" + + "github.com/aws/aws-sdk-go-v2/feature/dynamodbstreams/attributevalue" + "github.com/aws/aws-sdk-go-v2/internal/awsutil" + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" +) + +func ExampleMarshal() { + type Record struct { + Bytes []byte + MyField string + Letters []string + Numbers []int + } + + r := Record{ + Bytes: []byte{48, 49}, + MyField: "MyFieldValue", + Letters: []string{"a", "b", "c", "d"}, + Numbers: []int{1, 2, 3}, + } + av, err := attributevalue.Marshal(r) + m := av.(*types.AttributeValueMemberM) + fmt.Println("err", err) + 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 { + // Value: len 2 + // } + // MyField { + // Value: "MyFieldValue" + // } + // Letters { + // Value: [ + // &{a}, + // &{b}, + // &{c}, + // &{d} + // ] + // } + // Numbers { + // Value: [&{1},&{2},&{3}] + // } +} + +func ExampleUnmarshal() { + type Record struct { + Bytes []byte + MyField string + Letters []string + A2Num map[string]int + } + + expect := Record{ + Bytes: []byte{48, 49}, + MyField: "MyFieldValue", + Letters: []string{"a", "b", "c", "d"}, + A2Num: map[string]int{"a": 1, "b": 2, "c": 3}, + } + + av := &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": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "a": &types.AttributeValueMemberN{Value: "1"}, + "b": &types.AttributeValueMemberN{Value: "2"}, + "c": &types.AttributeValueMemberN{Value: "3"}, + }}, + }, + } + + actual := Record{} + err := attributevalue.Unmarshal(av, &actual) + fmt.Println(err, reflect.DeepEqual(expect, actual)) + + // Output: + // true +} diff --git a/feature/dynamodbstreams/attributevalue/marshaler_test.go b/feature/dynamodbstreams/attributevalue/marshaler_test.go new file mode 100644 index 00000000000..26d4d91a5c7 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/marshaler_test.go @@ -0,0 +1,698 @@ +package attributevalue + +import ( + "math" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" + "github.com/google/go-cmp/cmp" +) + +type simpleMarshalStruct struct { + Byte []byte + String string + PtrString *string + Int int + Uint uint + Float32 float32 + Float64 float64 + Bool bool + Null *interface{} +} + +type complexMarshalStruct struct { + Simple []simpleMarshalStruct +} + +type myByteStruct struct { + Byte []byte +} + +type myByteSetStruct struct { + ByteSet [][]byte +} + +type marshallerTestInput struct { + input interface{} + expected interface{} + err error +} + +var trueValue = true +var falseValue = false + +var marshalerScalarInputs = map[string]marshallerTestInput{ + "nil": { + input: nil, + expected: &types.AttributeValueMemberNULL{Value: true}, + }, + "string": { + input: "some string", + expected: &types.AttributeValueMemberS{Value: "some string"}, + }, + "bool": { + input: true, + expected: &types.AttributeValueMemberBOOL{Value: true}, + }, + "bool false": { + input: false, + expected: &types.AttributeValueMemberBOOL{Value: false}, + }, + "float": { + input: 3.14, + expected: &types.AttributeValueMemberN{Value: "3.14"}, + }, + "max float32": { + input: math.MaxFloat32, + expected: &types.AttributeValueMemberN{Value: "340282346638528860000000000000000000000"}, + }, + "max float64": { + input: math.MaxFloat64, + expected: &types.AttributeValueMemberN{Value: "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, + }, + "integer": { + input: 12, + expected: &types.AttributeValueMemberN{Value: "12"}, + }, + "number integer": { + input: Number("12"), + expected: &types.AttributeValueMemberN{Value: "12"}, + }, + "zero values": { + input: simpleMarshalStruct{}, + 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 = 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": &types.AttributeValueMemberS{Value: "some string"}}, + }, + "bool": { + input: map[string]interface{}{"bool": true}, + expected: map[string]types.AttributeValue{"bool": &types.AttributeValueMemberBOOL{Value: true}}, + }, + "bool false": { + input: map[string]interface{}{"bool": false}, + expected: map[string]types.AttributeValue{"bool": &types.AttributeValueMemberBOOL{Value: false}}, + }, + "null": { + input: map[string]interface{}{"null": nil}, + expected: map[string]types.AttributeValue{"null": &types.AttributeValueMemberNULL{Value: true}}, + }, + "float": { + input: map[string]interface{}{"float": 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": &types.AttributeValueMemberN{Value: "340282346638528860000000000000000000000"}}, + }, + "float64": { + input: map[string]interface{}{"float": math.MaxFloat64}, + expected: map[string]types.AttributeValue{"float": &types.AttributeValueMemberN{Value: "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}}, + }, + "decimal number": { + input: map[string]interface{}{"num": 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": &types.AttributeValueMemberB{Value: []byte{48, 49}}}, + }, + "nested blob": { + input: struct{ Byte []byte }{Byte: []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": &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": &types.AttributeValueMemberBS{Value: [][]byte{{48, 49}, {50, 51}}}}, + }, + "list": { + input: map[string]interface{}{"list": []interface{}{"a string", 12., 3.14, true, nil, false}}, + expected: map[string]types.AttributeValue{ + "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": { + input: map[string]interface{}{"map": map[string]interface{}{"nestednum": 12.}}, + expected: map[string]types.AttributeValue{ + "map": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "nestednum": &types.AttributeValueMemberN{Value: "12"}, + }, + }, + }, + }, + "struct": { + input: simpleMarshalStruct{}, + expected: 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"}, + }, + }, + "nested struct": { + input: complexMarshalStruct{}, + expected: map[string]types.AttributeValue{ + "Simple": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + "nested nil slice": { + input: struct { + Simple []string `dynamodbav:"simple"` + }{}, + expected: map[string]types.AttributeValue{ + "simple": &types.AttributeValueMemberNULL{Value: true}, + }, + }, + "nested nil slice omit empty": { + input: struct { + Simple []string `dynamodbav:"simple,omitempty"` + }{}, + expected: map[string]types.AttributeValue{}, + }, + "nested ignored field": { + input: struct { + 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": &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"}, + }, + }, + &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"}, + }, + }, + }, + }, + }, + }, +} + +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{ + &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{ + &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"}, + }, + }, + }, + }, +} + +func Test_New_Marshal(t *testing.T) { + 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) { +} + +func Test_New_Unmarshal(t *testing.T) { + // Using the same inputs from Marshal, test the reverse mapping. + 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()) + }) + } +} + +func Test_New_UnmarshalError(t *testing.T) { + // Test that we get an error using Unmarshal to convert to a nil value. + expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)} + if err := Unmarshal(nil, nil); err == nil { + t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", nil, expected) + } else if err.Error() != expected.Error() { + t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", nil, err, expected) + } + + // Test that we get an error using Unmarshal to convert to a non-pointer value. + var actual map[string]interface{} + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)} + if err := Unmarshal(nil, actual); err == nil { + t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", actual, expected) + } else if err.Error() != expected.Error() { + t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", actual, err, expected) + } + + // Test that we get an error using Unmarshal to convert to nil struct. + var actual2 *struct{ A int } + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)} + if err := Unmarshal(nil, actual2); err == nil { + t.Errorf("Unmarshal with input %T returned no error, expected error `%v`", actual2, expected) + } else if err.Error() != expected.Error() { + t.Errorf("Unmarshal with input %T returned error `%v`, expected error `%v`", actual2, err, expected) + } +} + +func Test_New_MarshalMap(t *testing.T) { + for 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 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()) + }) + } +} + +func Test_New_UnmarshalMapError(t *testing.T) { + // Test that we get an error using UnmarshalMap to convert to a nil value. + expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)} + if err := UnmarshalMap(nil, nil); err == nil { + t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", nil, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", nil, err, expected) + } + + // Test that we get an error using UnmarshalMap to convert to a non-pointer value. + var actual map[string]interface{} + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)} + if err := UnmarshalMap(nil, actual); err == nil { + t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", actual, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", actual, err, expected) + } + + // Test that we get an error using UnmarshalMap to convert to nil struct. + var actual2 *struct{ A int } + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)} + if err := UnmarshalMap(nil, actual2); err == nil { + t.Errorf("UnmarshalMap with input %T returned no error, expected error `%v`", actual2, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalMap with input %T returned error `%v`, expected error `%v`", actual2, err, expected) + } +} + +func Test_New_MarshalList(t *testing.T) { + for 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 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()) + }) + } +} + +func Test_New_UnmarshalListError(t *testing.T) { + // Test that we get an error using UnmarshalList to convert to a nil value. + expected := &InvalidUnmarshalError{Type: reflect.TypeOf(nil)} + if err := UnmarshalList(nil, nil); err == nil { + t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", nil, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", nil, err, expected) + } + + // Test that we get an error using UnmarshalList to convert to a non-pointer value. + var actual map[string]interface{} + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual)} + if err := UnmarshalList(nil, actual); err == nil { + t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", actual, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", actual, err, expected) + } + + // Test that we get an error using UnmarshalList to convert to nil struct. + var actual2 *struct{ A int } + expected = &InvalidUnmarshalError{Type: reflect.TypeOf(actual2)} + if err := UnmarshalList(nil, actual2); err == nil { + t.Errorf("UnmarshalList with input %T returned no error, expected error `%v`", actual2, expected) + } else if err.Error() != expected.Error() { + t.Errorf("UnmarshalList with input %T returned error `%v`, expected error `%v`", actual2, err, expected) + } +} + +func compareObjects(t *testing.T, expected interface{}, actual interface{}) { + t.Helper() + if !reflect.DeepEqual(expected, actual) { + ev := reflect.ValueOf(expected) + av := reflect.ValueOf(actual) + if diff := cmp.Diff(expected, actual); len(diff) != 0 { + t.Errorf("expect kind(%s, %T) match actual kind(%s, %T)\n%s", + ev.Kind(), ev.Interface(), av.Kind(), av.Interface(), diff) + } + } +} + +func BenchmarkMarshalOneMember(b *testing.B) { + fieldCache = fieldCacher{} + + simple := simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + } + type MyCompositeStruct struct { + A simpleMarshalStruct `dynamodbav:"a"` + } + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := Marshal(MyCompositeStruct{ + A: simple, + }); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func BenchmarkMarshalTwoMembers(b *testing.B) { + fieldCache = fieldCacher{} + + simple := simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + } + + type MyCompositeStruct struct { + A simpleMarshalStruct `dynamodbav:"a"` + B simpleMarshalStruct `dynamodbav:"b"` + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := Marshal(MyCompositeStruct{ + A: simple, + B: simple, + }); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func BenchmarkUnmarshalOneMember(b *testing.B) { + fieldCache = fieldCacher{} + + myStructAVMap, _ := Marshal(simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + }) + + type MyCompositeStructOne struct { + A simpleMarshalStruct `dynamodbav:"a"` + } + var out MyCompositeStructOne + avMap := map[string]types.AttributeValue{ + "a": myStructAVMap, + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := Unmarshal(&types.AttributeValueMemberM{Value: avMap}, &out); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func BenchmarkUnmarshalTwoMembers(b *testing.B) { + fieldCache = fieldCacher{} + + myStructAVMap, _ := Marshal(simpleMarshalStruct{ + String: "abc", + Int: 123, + Uint: 123, + Float32: 123.321, + Float64: 123.321, + Bool: true, + Null: nil, + }) + + type MyCompositeStructTwo struct { + A simpleMarshalStruct `dynamodbav:"a"` + B simpleMarshalStruct `dynamodbav:"b"` + } + var out MyCompositeStructTwo + avMap := map[string]types.AttributeValue{ + "a": myStructAVMap, + "b": myStructAVMap, + } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := Unmarshal(&types.AttributeValueMemberM{Value: avMap}, &out); err != nil { + b.Error("unexpected error:", err) + } + } + }) +} + +func Test_Encode_YAML_TagKey(t *testing.T) { + input := struct { + String string `yaml:"string"` + EmptyString string `yaml:"empty"` + OmitString string `yaml:"omitted,omitempty"` + Ignored string `yaml:"-"` + Byte []byte `yaml:"byte"` + Float32 float32 `yaml:"float32"` + Float64 float64 `yaml:"float64"` + Int int `yaml:"int"` + Uint uint `yaml:"uint"` + Slice []string `yaml:"slice"` + Map map[string]int `yaml:"map"` + NoTag string + }{ + String: "String", + Ignored: "Ignored", + Slice: []string{"one", "two"}, + Map: map[string]int{ + "one": 1, + "two": 2, + }, + NoTag: "NoTag", + } + + 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": &types.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "one": &types.AttributeValueMemberN{Value: "1"}, + "two": &types.AttributeValueMemberN{Value: "2"}, + }, + }, + "NoTag": &types.AttributeValueMemberS{Value: "NoTag"}, + }, + } + + enc := NewEncoder(func(o *EncoderOptions) { + o.TagKey = "yaml" + }) + + actual, err := enc.Encode(input) + if err != nil { + t.Errorf("Encode with input %#v returned error `%s`, expected nil", input, err) + } + + compareObjects(t, expected, actual) +} diff --git a/feature/dynamodbstreams/attributevalue/shared_test.go b/feature/dynamodbstreams/attributevalue/shared_test.go new file mode 100644 index 00000000000..764ac593104 --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/shared_test.go @@ -0,0 +1,409 @@ +package attributevalue + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams/types" + "github.com/google/go-cmp/cmp" +) + +type testBinarySetStruct struct { + Binarys [][]byte `dynamodbav:",binaryset"` +} +type testNumberSetStruct struct { + Numbers []int `dynamodbav:",numberset"` +} +type testStringSetStruct struct { + Strings []string `dynamodbav:",stringset"` +} + +type testIntAsStringStruct struct { + Value int `dynamodbav:",string"` +} + +type testOmitEmptyStruct struct { + Value string `dynamodbav:",omitempty"` + Value2 *string `dynamodbav:",omitempty"` + Value3 int +} + +type testAliasedString string +type testAliasedStringSlice []string +type testAliasedInt int +type testAliasedIntSlice []int +type testAliasedMap map[string]int +type testAliasedSlice []string +type testAliasedByteSlice []byte +type testAliasedBool bool +type testAliasedBoolSlice []bool + +type testAliasedStruct struct { + Value testAliasedString + Value2 testAliasedInt + Value3 testAliasedMap + Value4 testAliasedSlice + + Value5 testAliasedByteSlice + Value6 []testAliasedInt + Value7 []testAliasedString + + Value8 []testAliasedByteSlice `dynamodbav:",binaryset"` + Value9 []testAliasedInt `dynamodbav:",numberset"` + Value10 []testAliasedString `dynamodbav:",stringset"` + + Value11 testAliasedIntSlice + Value12 testAliasedStringSlice + + Value13 testAliasedBool + Value14 testAliasedBoolSlice + + Value15 map[testAliasedString]string +} + +type testNamedPointer *int + +var testDate, _ = time.Parse(time.RFC3339, "2016-05-03T17:06:26.209072Z") + +var sharedTestCases = map[string]struct { + in types.AttributeValue + actual, expected interface{} + err error +}{ + "binary slice": { + in: &types.AttributeValueMemberB{Value: []byte{48, 49}}, + actual: &[]byte{}, + expected: []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.AttributeValueMemberB{Value: []byte{48, 49}}, + actual: func() **[]byte { + v := make([]byte, 0, 10) + v2 := &v + return &v2 + }(), + expected: []byte{48, 49}, + }, + "bool": { + in: &types.AttributeValueMemberBOOL{Value: true}, + actual: new(bool), + expected: true, + }, + "list": { + in: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "123"}, + }}, + actual: &[]int{}, + expected: []int{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.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "Abc": &types.AttributeValueMemberN{Value: "123"}, + }}, + actual: &struct{ Abc int }{}, + expected: struct{ Abc int }{Abc: 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"` + }{}, + expected: struct { + Abc int `json:"abc" dynamodbav:"abc"` + }{Abc: 123}, + }, + "number, int": { + in: &types.AttributeValueMemberN{Value: "123"}, + actual: new(int), + expected: 123, + }, + "number, Float": { + in: &types.AttributeValueMemberN{Value: "123.1"}, + actual: new(float64), + expected: float64(123.1), + }, + "null ptr": { + in: &types.AttributeValueMemberNULL{Value: true}, + actual: new(*string), + expected: nil, + }, + "string": { + in: &types.AttributeValueMemberS{Value: "abc"}, + actual: new(string), + expected: "abc", + }, + "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.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.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.AttributeValueMemberM{ + Value: map[string]types.AttributeValue{ + "Value": &types.AttributeValueMemberS{Value: "123"}, + }, + }, + actual: &testIntAsStringStruct{}, + expected: testIntAsStringStruct{Value: 123}, + }, + "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.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": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "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": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "3"}, + }}, + "Value8": &types.AttributeValueMemberBS{Value: [][]byte{ + {0, 1, 2}, {3, 4, 5}, + }}, + "Value9": &types.AttributeValueMemberNS{Value: []string{ + "1", + "2", + "3", + }}, + "Value10": &types.AttributeValueMemberSS{Value: []string{ + "1", + "2", + "3", + }}, + "Value11": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, + }}, + "Value12": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberS{Value: "1"}, + &types.AttributeValueMemberS{Value: "2"}, + &types.AttributeValueMemberS{Value: "3"}, + }}, + "Value13": &types.AttributeValueMemberBOOL{Value: true}, + "Value14": &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberBOOL{Value: false}, + &types.AttributeValueMemberBOOL{Value: true}, + }}, + "Value15": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ + "TestKey": &types.AttributeValueMemberS{Value: "TestElement"}, + }}, + }, + }, + actual: &testAliasedStruct{}, + expected: testAliasedStruct{ + Value: "123", Value2: 123, + Value3: testAliasedMap{ + "Key": 321, + }, + Value4: testAliasedSlice{"1", "2", "3"}, + Value5: testAliasedByteSlice{0, 1, 2}, + Value6: []testAliasedInt{1, 2, 3}, + Value7: []testAliasedString{"1", "2", "3"}, + Value8: []testAliasedByteSlice{ + {0, 1, 2}, + {3, 4, 5}, + }, + Value9: []testAliasedInt{1, 2, 3}, + Value10: []testAliasedString{"1", "2", "3"}, + Value11: testAliasedIntSlice{1, 2, 3}, + Value12: testAliasedStringSlice{"1", "2", "3"}, + Value13: true, + Value14: testAliasedBoolSlice{true, false, true}, + Value15: map[testAliasedString]string{"TestKey": "TestElement"}, + }, + }, + "number named pointer": { + in: &types.AttributeValueMemberN{Value: "123"}, + actual: new(testNamedPointer), + expected: testNamedPointer(aws.Int(123)), + }, + "time.Time": { + in: &types.AttributeValueMemberS{Value: "2016-05-03T17:06:26.209072Z"}, + actual: new(time.Time), + expected: testDate, + }, + "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.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"` + }{}, + expected: struct { + Abc time.Time `json:"abc" dynamodbav:"abc"` + }{Abc: testDate}, + }, + "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"` + }{}, + expected: struct { + Abc *time.Time `json:"abc" dynamodbav:"abc"` + }{Abc: &testDate}, + }, +} + +var sharedListTestCases = map[string]struct { + in []types.AttributeValue + actual, expected interface{} + err error +}{ + "union members": { + in: []types.AttributeValue{ + &types.AttributeValueMemberB{Value: []byte{48, 49}}, + &types.AttributeValueMemberBOOL{Value: true}, + &types.AttributeValueMemberN{Value: "123"}, + &types.AttributeValueMemberS{Value: "123"}, + }, + actual: func() *[]interface{} { + v := []interface{}{} + return &v + }(), + expected: []interface{}{[]byte{48, 49}, true, 123., "123"}, + }, + "numbers": { + in: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + &types.AttributeValueMemberN{Value: "3"}, + }, + actual: &[]interface{}{}, + expected: []interface{}{1., 2., 3.}, + }, +} + +var sharedMapTestCases = map[string]struct { + in map[string]types.AttributeValue + actual, expected interface{} + err error +}{ + "union members": { + in: map[string]types.AttributeValue{ + "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{}{ + "B": []byte{48, 49}, "BOOL": true, + "N": 123., "S": "123", + }, + }, +} + +func assertConvertTest(t *testing.T, actual, expected interface{}, err, expectedErr error) { + t.Helper() + + if expectedErr != nil { + if err != nil { + if e, a := expectedErr, err; !strings.Contains(a.Error(), e.Error()) { + t.Errorf("expect %v, got %v", e, a) + } + } else { + t.Fatalf("expected error, %v", expectedErr) + } + } else if err != nil { + t.Fatalf("expect no error, got %v", err) + } else { + if diff := cmp.Diff(ptrToValue(expected), ptrToValue(actual)); len(diff) != 0 { + t.Errorf("expect match\n%s", diff) + } + } +} + +func ptrToValue(in interface{}) interface{} { + v := reflect.ValueOf(in) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if !v.IsValid() { + return nil + } + if v.Kind() == reflect.Ptr { + return ptrToValue(v.Interface()) + } + return v.Interface() +} diff --git a/feature/dynamodbstreams/attributevalue/tag.go b/feature/dynamodbstreams/attributevalue/tag.go new file mode 100644 index 00000000000..6eb901706fb --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/tag.go @@ -0,0 +1,74 @@ +package attributevalue + +import ( + "reflect" + "strings" +) + +type tag struct { + Name string + Ignore bool + OmitEmpty bool + OmitEmptyElem bool + NullEmpty bool + NullEmptyElem bool + AsString bool + AsBinSet, AsNumSet, AsStrSet bool + AsUnixTime bool +} + +func (t *tag) parseAVTag(structTag reflect.StructTag) { + tagStr := structTag.Get("dynamodbav") + if len(tagStr) == 0 { + return + } + + t.parseTagStr(tagStr) +} + +func (t *tag) parseStructTag(tag string, structTag reflect.StructTag) { + tagStr := structTag.Get(tag) + if len(tagStr) == 0 { + return + } + + t.parseTagStr(tagStr) +} + +func (t *tag) parseTagStr(tagStr string) { + parts := strings.Split(tagStr, ",") + if len(parts) == 0 { + return + } + + if name := parts[0]; name == "-" { + t.Name = "" + t.Ignore = true + } else { + t.Name = name + t.Ignore = false + } + + for _, opt := range parts[1:] { + switch opt { + case "omitempty": + t.OmitEmpty = true + case "omitemptyelem": + t.OmitEmptyElem = true + case "nullempty": + t.NullEmpty = true + case "nullemptyelem": + t.NullEmptyElem = true + case "string": + t.AsString = true + case "binaryset": + t.AsBinSet = true + case "numberset": + t.AsNumSet = true + case "stringset": + t.AsStrSet = true + case "unixtime": + t.AsUnixTime = true + } + } +} diff --git a/feature/dynamodbstreams/attributevalue/tag_test.go b/feature/dynamodbstreams/attributevalue/tag_test.go new file mode 100644 index 00000000000..e772a8486fa --- /dev/null +++ b/feature/dynamodbstreams/attributevalue/tag_test.go @@ -0,0 +1,47 @@ +package attributevalue + +import ( + "reflect" + "testing" +) + +func TestTagParse(t *testing.T) { + cases := []struct { + in reflect.StructTag + json, av bool + expect tag + }{ + {`json:""`, true, false, tag{}}, + {`json:"name"`, true, false, tag{Name: "name"}}, + {`json:"name,omitempty"`, true, false, tag{Name: "name", OmitEmpty: true}}, + {`json:"-"`, true, false, tag{Ignore: true}}, + {`json:",omitempty"`, true, false, tag{OmitEmpty: true}}, + {`json:",string"`, true, false, tag{AsString: true}}, + {`dynamodbav:""`, false, true, tag{}}, + {`dynamodbav:","`, false, true, tag{}}, + {`dynamodbav:"name"`, false, true, tag{Name: "name"}}, + {`dynamodbav:"name"`, false, true, tag{Name: "name"}}, + {`dynamodbav:"-"`, false, true, tag{Ignore: true}}, + {`dynamodbav:",omitempty"`, false, true, tag{OmitEmpty: true}}, + {`dynamodbav:",omitemptyelem"`, false, true, tag{OmitEmptyElem: true}}, + {`dynamodbav:",string"`, false, true, tag{AsString: true}}, + {`dynamodbav:",binaryset"`, false, true, tag{AsBinSet: true}}, + {`dynamodbav:",numberset"`, false, true, tag{AsNumSet: true}}, + {`dynamodbav:",stringset"`, false, true, tag{AsStrSet: true}}, + {`dynamodbav:",stringset,omitemptyelem"`, false, true, tag{AsStrSet: true, OmitEmptyElem: true}}, + {`dynamodbav:"name,stringset,omitemptyelem"`, false, true, tag{Name: "name", AsStrSet: true, OmitEmptyElem: true}}, + } + + for i, c := range cases { + actual := tag{} + if c.json { + actual.parseStructTag("json", c.in) + } + if c.av { + actual.parseAVTag(c.in) + } + if e, a := c.expect, actual; !reflect.DeepEqual(e, a) { + t.Errorf("case %d, expect %v, got %v", i, e, a) + } + } +} diff --git a/service/dynamodb/deserializers.go b/service/dynamodb/deserializers.go index cb1c1ade423..a1d82fb792c 100644 --- a/service/dynamodb/deserializers.go +++ b/service/dynamodb/deserializers.go @@ -6537,11 +6537,10 @@ func awsAwsjson10_deserializeDocumentAttributeMap(v *map[string]types.AttributeV for key, value := range shape { var parsedVal types.AttributeValue mapVar := parsedVal - destAddr := &mapVar - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&mapVar, value); err != nil { return err } - parsedVal = *destAddr + parsedVal = mapVar mv[key] = parsedVal } @@ -6585,7 +6584,7 @@ func awsAwsjson10_deserializeDocumentAttributeNameList(v *[]string, value interf return nil } -func awsAwsjson10_deserializeDocumentAttributeValue(v **types.AttributeValue, value interface{}) error { +func awsAwsjson10_deserializeDocumentAttributeValue(v *types.AttributeValue, value interface{}) error { if v == nil { return fmt.Errorf("unexpected nil of type %T", v) } @@ -6598,16 +6597,12 @@ func awsAwsjson10_deserializeDocumentAttributeValue(v **types.AttributeValue, va return fmt.Errorf("unexpected JSON type %v", value) } - var sv *types.AttributeValue - if *v == nil { - sv = &types.AttributeValue{} - } else { - sv = *v - } - + var uv types.AttributeValue +loop: for key, value := range shape { switch key { case "B": + var mv []byte if value != nil { jtv, ok := value.(string) if !ok { @@ -6617,76 +6612,108 @@ func awsAwsjson10_deserializeDocumentAttributeValue(v **types.AttributeValue, va if err != nil { return fmt.Errorf("failed to base64 decode BinaryAttributeValue, %w", err) } - sv.B = dv + mv = dv } + uv = &types.AttributeValueMemberB{Value: mv} + break loop case "BOOL": + var mv bool if value != nil { jtv, ok := value.(bool) if !ok { return fmt.Errorf("expected BooleanAttributeValue to be of type *bool, got %T instead", value) } - sv.BOOL = ptr.Bool(jtv) + mv = jtv } + uv = &types.AttributeValueMemberBOOL{Value: mv} + break loop case "BS": - if err := awsAwsjson10_deserializeDocumentBinarySetAttributeValue(&sv.BS, value); err != nil { + var mv [][]byte + if err := awsAwsjson10_deserializeDocumentBinarySetAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberBS{Value: mv} + break loop case "L": - if err := awsAwsjson10_deserializeDocumentListAttributeValue(&sv.L, value); err != nil { + var mv []types.AttributeValue + if err := awsAwsjson10_deserializeDocumentListAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberL{Value: mv} + break loop case "M": - if err := awsAwsjson10_deserializeDocumentMapAttributeValue(&sv.M, value); err != nil { + var mv map[string]types.AttributeValue + if err := awsAwsjson10_deserializeDocumentMapAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberM{Value: mv} + break loop case "N": + var mv string if value != nil { jtv, ok := value.(string) if !ok { return fmt.Errorf("expected NumberAttributeValue to be of type string, got %T instead", value) } - sv.N = ptr.String(jtv) + mv = jtv } + uv = &types.AttributeValueMemberN{Value: mv} + break loop case "NS": - if err := awsAwsjson10_deserializeDocumentNumberSetAttributeValue(&sv.NS, value); err != nil { + var mv []string + if err := awsAwsjson10_deserializeDocumentNumberSetAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberNS{Value: mv} + break loop case "NULL": + var mv bool if value != nil { jtv, ok := value.(bool) if !ok { return fmt.Errorf("expected NullAttributeValue to be of type *bool, got %T instead", value) } - sv.NULL = ptr.Bool(jtv) + mv = jtv } + uv = &types.AttributeValueMemberNULL{Value: mv} + break loop case "S": + var mv string if value != nil { jtv, ok := value.(string) if !ok { return fmt.Errorf("expected StringAttributeValue to be of type string, got %T instead", value) } - sv.S = ptr.String(jtv) + mv = jtv } + uv = &types.AttributeValueMemberS{Value: mv} + break loop case "SS": - if err := awsAwsjson10_deserializeDocumentStringSetAttributeValue(&sv.SS, value); err != nil { + var mv []string + if err := awsAwsjson10_deserializeDocumentStringSetAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberSS{Value: mv} + break loop default: - _, _ = key, value + uv = &types.UnknownUnionMember{Tag: key} + // TODO: FIX ME + _ = value + break loop } } - *v = sv + *v = uv return nil } @@ -9384,11 +9411,10 @@ func awsAwsjson10_deserializeDocumentItemCollectionKeyAttributeMap(v *map[string for key, value := range shape { var parsedVal types.AttributeValue mapVar := parsedVal - destAddr := &mapVar - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&mapVar, value); err != nil { return err } - parsedVal = *destAddr + parsedVal = mapVar mv[key] = parsedVal } @@ -9710,11 +9736,10 @@ func awsAwsjson10_deserializeDocumentKey(v *map[string]types.AttributeValue, val for key, value := range shape { var parsedVal types.AttributeValue mapVar := parsedVal - destAddr := &mapVar - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&mapVar, value); err != nil { return err } - parsedVal = *destAddr + parsedVal = mapVar mv[key] = parsedVal } @@ -9963,11 +9988,9 @@ func awsAwsjson10_deserializeDocumentListAttributeValue(v *[]types.AttributeValu for _, value := range shape { var col types.AttributeValue - destAddr := &col - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&col, value); err != nil { return err } - col = *destAddr cv = append(cv, col) } @@ -10201,11 +10224,10 @@ func awsAwsjson10_deserializeDocumentMapAttributeValue(v *map[string]types.Attri for key, value := range shape { var parsedVal types.AttributeValue mapVar := parsedVal - destAddr := &mapVar - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&mapVar, value); err != nil { return err } - parsedVal = *destAddr + parsedVal = mapVar mv[key] = parsedVal } @@ -10696,11 +10718,10 @@ func awsAwsjson10_deserializeDocumentPutItemInputAttributeMap(v *map[string]type for key, value := range shape { var parsedVal types.AttributeValue mapVar := parsedVal - destAddr := &mapVar - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&mapVar, value); err != nil { return err } - parsedVal = *destAddr + parsedVal = mapVar mv[key] = parsedVal } diff --git a/service/dynamodb/serializers.go b/service/dynamodb/serializers.go index 5466a5af285..47fc871d88c 100644 --- a/service/dynamodb/serializers.go +++ b/service/dynamodb/serializers.go @@ -2079,70 +2079,65 @@ func awsAwsjson10_serializeDocumentAttributeUpdates(v map[string]types.Attribute return nil } -func awsAwsjson10_serializeDocumentAttributeValue(v *types.AttributeValue, value smithyjson.Value) error { +func awsAwsjson10_serializeDocumentAttributeValue(v types.AttributeValue, value smithyjson.Value) error { object := value.Object() defer object.Close() - if v.B != nil { - ok := object.Key("B") - ok.Base64EncodeBytes(v.B) - } + switch uv := v.(type) { + case *types.AttributeValueMemberB: + av := object.Key("B") + av.Base64EncodeBytes(uv.Value) - if v.BOOL != nil { - ok := object.Key("BOOL") - ok.Boolean(*v.BOOL) - } + case *types.AttributeValueMemberBOOL: + av := object.Key("BOOL") + av.Boolean(uv.Value) - if v.BS != nil { - ok := object.Key("BS") - if err := awsAwsjson10_serializeDocumentBinarySetAttributeValue(v.BS, ok); err != nil { + case *types.AttributeValueMemberBS: + av := object.Key("BS") + if err := awsAwsjson10_serializeDocumentBinarySetAttributeValue(uv.Value, av); err != nil { return err } - } - if v.L != nil { - ok := object.Key("L") - if err := awsAwsjson10_serializeDocumentListAttributeValue(v.L, ok); err != nil { + case *types.AttributeValueMemberL: + av := object.Key("L") + if err := awsAwsjson10_serializeDocumentListAttributeValue(uv.Value, av); err != nil { return err } - } - if v.M != nil { - ok := object.Key("M") - if err := awsAwsjson10_serializeDocumentMapAttributeValue(v.M, ok); err != nil { + case *types.AttributeValueMemberM: + av := object.Key("M") + if err := awsAwsjson10_serializeDocumentMapAttributeValue(uv.Value, av); err != nil { return err } - } - if v.N != nil { - ok := object.Key("N") - ok.String(*v.N) - } + case *types.AttributeValueMemberN: + av := object.Key("N") + av.String(uv.Value) - if v.NS != nil { - ok := object.Key("NS") - if err := awsAwsjson10_serializeDocumentNumberSetAttributeValue(v.NS, ok); err != nil { + case *types.AttributeValueMemberNS: + av := object.Key("NS") + if err := awsAwsjson10_serializeDocumentNumberSetAttributeValue(uv.Value, av); err != nil { return err } - } - if v.NULL != nil { - ok := object.Key("NULL") - ok.Boolean(*v.NULL) - } + case *types.AttributeValueMemberNULL: + av := object.Key("NULL") + av.Boolean(uv.Value) - if v.S != nil { - ok := object.Key("S") - ok.String(*v.S) - } + case *types.AttributeValueMemberS: + av := object.Key("S") + av.String(uv.Value) - if v.SS != nil { - ok := object.Key("SS") - if err := awsAwsjson10_serializeDocumentStringSetAttributeValue(v.SS, ok); err != nil { + case *types.AttributeValueMemberSS: + av := object.Key("SS") + if err := awsAwsjson10_serializeDocumentStringSetAttributeValue(uv.Value, av); err != nil { return err } - } + default: + return fmt.Errorf("attempted to serialize unknown member type %T for union %T", uv, v) + + } return nil } @@ -2152,7 +2147,10 @@ func awsAwsjson10_serializeDocumentAttributeValueList(v []types.AttributeValue, for i := range v { av := array.Value() - if err := awsAwsjson10_serializeDocumentAttributeValue(&v[i], av); err != nil { + if vv := v[i]; vv == nil { + continue + } + if err := awsAwsjson10_serializeDocumentAttributeValue(v[i], av); err != nil { return err } } @@ -2595,8 +2593,10 @@ func awsAwsjson10_serializeDocumentExpressionAttributeValueMap(v map[string]type for key := range v { om := object.Key(key) - mapVar := v[key] - if err := awsAwsjson10_serializeDocumentAttributeValue(&mapVar, om); err != nil { + if vv := v[key]; vv == nil { + continue + } + if err := awsAwsjson10_serializeDocumentAttributeValue(v[key], om); err != nil { return err } } @@ -2810,8 +2810,10 @@ func awsAwsjson10_serializeDocumentKey(v map[string]types.AttributeValue, value for key := range v { om := object.Key(key) - mapVar := v[key] - if err := awsAwsjson10_serializeDocumentAttributeValue(&mapVar, om); err != nil { + if vv := v[key]; vv == nil { + continue + } + if err := awsAwsjson10_serializeDocumentAttributeValue(v[key], om); err != nil { return err } } @@ -2922,7 +2924,10 @@ func awsAwsjson10_serializeDocumentListAttributeValue(v []types.AttributeValue, for i := range v { av := array.Value() - if err := awsAwsjson10_serializeDocumentAttributeValue(&v[i], av); err != nil { + if vv := v[i]; vv == nil { + continue + } + if err := awsAwsjson10_serializeDocumentAttributeValue(v[i], av); err != nil { return err } } @@ -2974,8 +2979,10 @@ func awsAwsjson10_serializeDocumentMapAttributeValue(v map[string]types.Attribut for key := range v { om := object.Key(key) - mapVar := v[key] - if err := awsAwsjson10_serializeDocumentAttributeValue(&mapVar, om); err != nil { + if vv := v[key]; vv == nil { + continue + } + if err := awsAwsjson10_serializeDocumentAttributeValue(v[key], om); err != nil { return err } } @@ -3113,8 +3120,10 @@ func awsAwsjson10_serializeDocumentPutItemInputAttributeMap(v map[string]types.A for key := range v { om := object.Key(key) - mapVar := v[key] - if err := awsAwsjson10_serializeDocumentAttributeValue(&mapVar, om); err != nil { + if vv := v[key]; vv == nil { + continue + } + if err := awsAwsjson10_serializeDocumentAttributeValue(v[key], om); err != nil { return err } } diff --git a/service/dynamodb/types/types.go b/service/dynamodb/types/types.go index 773b604a869..48aa57fe5ac 100644 --- a/service/dynamodb/types/types.go +++ b/service/dynamodb/types/types.go @@ -54,50 +54,91 @@ type AttributeDefinition struct { // For more information, see Data Types // (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) // in the Amazon DynamoDB Developer Guide. -type AttributeValue struct { +type AttributeValue interface { + isAttributeValue() +} + +// An attribute of type String. For example: "S": "Hello" +type AttributeValueMemberS struct { + Value string +} + +func (*AttributeValueMemberS) isAttributeValue() {} + +// An attribute of type Number. For example: "N": "123.45" Numbers are sent across +// the network to DynamoDB as strings, to maximize compatibility across languages +// and libraries. However, DynamoDB treats them as number type attributes for +// mathematical operations. +type AttributeValueMemberN struct { + Value string +} - // An attribute of type Binary. For example: "B": - // "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" - B []byte +func (*AttributeValueMemberN) isAttributeValue() {} - // An attribute of type Boolean. For example: "BOOL": true - BOOL *bool +// An attribute of type Binary. For example: "B": +// "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" +type AttributeValueMemberB struct { + Value []byte +} - // An attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", - // "U25vd3k="] - BS [][]byte +func (*AttributeValueMemberB) isAttributeValue() {} - // An attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": - // "Coffee"}, {"N", "3.14159"}] - L []AttributeValue +// An attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" +// ,"Zebra"] +type AttributeValueMemberSS struct { + Value []string +} - // An attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": - // "35"}} - M map[string]AttributeValue +func (*AttributeValueMemberSS) isAttributeValue() {} - // An attribute of type Number. For example: "N": "123.45" Numbers are sent across - // the network to DynamoDB as strings, to maximize compatibility across languages - // and libraries. However, DynamoDB treats them as number type attributes for - // mathematical operations. - N *string +// An attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", +// "3.14"] Numbers are sent across the network to DynamoDB as strings, to maximize +// compatibility across languages and libraries. However, DynamoDB treats them as +// number type attributes for mathematical operations. +type AttributeValueMemberNS struct { + Value []string +} - // An attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", - // "3.14"] Numbers are sent across the network to DynamoDB as strings, to maximize - // compatibility across languages and libraries. However, DynamoDB treats them as - // number type attributes for mathematical operations. - NS []string +func (*AttributeValueMemberNS) isAttributeValue() {} - // An attribute of type Null. For example: "NULL": true - NULL *bool +// An attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", +// "U25vd3k="] +type AttributeValueMemberBS struct { + Value [][]byte +} - // An attribute of type String. For example: "S": "Hello" - S *string +func (*AttributeValueMemberBS) isAttributeValue() {} - // An attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" - // ,"Zebra"] - SS []string +// An attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": +// "35"}} +type AttributeValueMemberM struct { + Value map[string]AttributeValue } +func (*AttributeValueMemberM) isAttributeValue() {} + +// An attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": +// "Coffee"}, {"N", "3.14159"}] +type AttributeValueMemberL struct { + Value []AttributeValue +} + +func (*AttributeValueMemberL) isAttributeValue() {} + +// An attribute of type Null. For example: "NULL": true +type AttributeValueMemberNULL struct { + Value bool +} + +func (*AttributeValueMemberNULL) isAttributeValue() {} + +// An attribute of type Boolean. For example: "BOOL": true +type AttributeValueMemberBOOL struct { + Value bool +} + +func (*AttributeValueMemberBOOL) isAttributeValue() {} + // For the UpdateItem operation, represents the attributes to be modified, the // action to perform on each, and the new value for each. You cannot use UpdateItem // to update any primary key attributes. Instead, you will need to delete the item, @@ -174,7 +215,7 @@ type AttributeValueUpdate struct { // For more information, see Data Types // (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) // in the Amazon DynamoDB Developer Guide. - Value *AttributeValue + Value AttributeValue } // Represents the properties of the scaling policy. @@ -1015,7 +1056,7 @@ type ExpectedAttributeValue struct { // data itself. For more information, see Data Types // (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) // in the Amazon DynamoDB Developer Guide. - Value *AttributeValue + Value AttributeValue } // Represents the properties of the exported table. @@ -2769,3 +2810,12 @@ type WriteRequest struct { // A request to perform a PutItem operation. PutRequest *PutRequest } + +// UnknownUnionMember is returned when a union member is returned over the wire, +// but has an unknown tag. +type UnknownUnionMember struct { + Tag string + Value []byte +} + +func (*UnknownUnionMember) isAttributeValue() {} diff --git a/service/dynamodbstreams/deserializers.go b/service/dynamodbstreams/deserializers.go index 0493fe398f6..0eccea22a15 100644 --- a/service/dynamodbstreams/deserializers.go +++ b/service/dynamodbstreams/deserializers.go @@ -686,11 +686,10 @@ func awsAwsjson10_deserializeDocumentAttributeMap(v *map[string]types.AttributeV for key, value := range shape { var parsedVal types.AttributeValue mapVar := parsedVal - destAddr := &mapVar - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&mapVar, value); err != nil { return err } - parsedVal = *destAddr + parsedVal = mapVar mv[key] = parsedVal } @@ -698,7 +697,7 @@ func awsAwsjson10_deserializeDocumentAttributeMap(v *map[string]types.AttributeV return nil } -func awsAwsjson10_deserializeDocumentAttributeValue(v **types.AttributeValue, value interface{}) error { +func awsAwsjson10_deserializeDocumentAttributeValue(v *types.AttributeValue, value interface{}) error { if v == nil { return fmt.Errorf("unexpected nil of type %T", v) } @@ -711,16 +710,12 @@ func awsAwsjson10_deserializeDocumentAttributeValue(v **types.AttributeValue, va return fmt.Errorf("unexpected JSON type %v", value) } - var sv *types.AttributeValue - if *v == nil { - sv = &types.AttributeValue{} - } else { - sv = *v - } - + var uv types.AttributeValue +loop: for key, value := range shape { switch key { case "B": + var mv []byte if value != nil { jtv, ok := value.(string) if !ok { @@ -730,76 +725,108 @@ func awsAwsjson10_deserializeDocumentAttributeValue(v **types.AttributeValue, va if err != nil { return fmt.Errorf("failed to base64 decode BinaryAttributeValue, %w", err) } - sv.B = dv + mv = dv } + uv = &types.AttributeValueMemberB{Value: mv} + break loop case "BOOL": + var mv bool if value != nil { jtv, ok := value.(bool) if !ok { return fmt.Errorf("expected BooleanAttributeValue to be of type *bool, got %T instead", value) } - sv.BOOL = ptr.Bool(jtv) + mv = jtv } + uv = &types.AttributeValueMemberBOOL{Value: mv} + break loop case "BS": - if err := awsAwsjson10_deserializeDocumentBinarySetAttributeValue(&sv.BS, value); err != nil { + var mv [][]byte + if err := awsAwsjson10_deserializeDocumentBinarySetAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberBS{Value: mv} + break loop case "L": - if err := awsAwsjson10_deserializeDocumentListAttributeValue(&sv.L, value); err != nil { + var mv []types.AttributeValue + if err := awsAwsjson10_deserializeDocumentListAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberL{Value: mv} + break loop case "M": - if err := awsAwsjson10_deserializeDocumentMapAttributeValue(&sv.M, value); err != nil { + var mv map[string]types.AttributeValue + if err := awsAwsjson10_deserializeDocumentMapAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberM{Value: mv} + break loop case "N": + var mv string if value != nil { jtv, ok := value.(string) if !ok { return fmt.Errorf("expected NumberAttributeValue to be of type string, got %T instead", value) } - sv.N = ptr.String(jtv) + mv = jtv } + uv = &types.AttributeValueMemberN{Value: mv} + break loop case "NS": - if err := awsAwsjson10_deserializeDocumentNumberSetAttributeValue(&sv.NS, value); err != nil { + var mv []string + if err := awsAwsjson10_deserializeDocumentNumberSetAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberNS{Value: mv} + break loop case "NULL": + var mv bool if value != nil { jtv, ok := value.(bool) if !ok { return fmt.Errorf("expected NullAttributeValue to be of type *bool, got %T instead", value) } - sv.NULL = ptr.Bool(jtv) + mv = jtv } + uv = &types.AttributeValueMemberNULL{Value: mv} + break loop case "S": + var mv string if value != nil { jtv, ok := value.(string) if !ok { return fmt.Errorf("expected StringAttributeValue to be of type string, got %T instead", value) } - sv.S = ptr.String(jtv) + mv = jtv } + uv = &types.AttributeValueMemberS{Value: mv} + break loop case "SS": - if err := awsAwsjson10_deserializeDocumentStringSetAttributeValue(&sv.SS, value); err != nil { + var mv []string + if err := awsAwsjson10_deserializeDocumentStringSetAttributeValue(&mv, value); err != nil { return err } + uv = &types.AttributeValueMemberSS{Value: mv} + break loop default: - _, _ = key, value + uv = &types.UnknownUnionMember{Tag: key} + // TODO: FIX ME + _ = value + break loop } } - *v = sv + *v = uv return nil } @@ -1117,11 +1144,9 @@ func awsAwsjson10_deserializeDocumentListAttributeValue(v *[]types.AttributeValu for _, value := range shape { var col types.AttributeValue - destAddr := &col - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&col, value); err != nil { return err } - col = *destAddr cv = append(cv, col) } @@ -1152,11 +1177,10 @@ func awsAwsjson10_deserializeDocumentMapAttributeValue(v *map[string]types.Attri for key, value := range shape { var parsedVal types.AttributeValue mapVar := parsedVal - destAddr := &mapVar - if err := awsAwsjson10_deserializeDocumentAttributeValue(&destAddr, value); err != nil { + if err := awsAwsjson10_deserializeDocumentAttributeValue(&mapVar, value); err != nil { return err } - parsedVal = *destAddr + parsedVal = mapVar mv[key] = parsedVal } diff --git a/service/dynamodbstreams/types/types.go b/service/dynamodbstreams/types/types.go index 681a5a6079c..f969bab362a 100644 --- a/service/dynamodbstreams/types/types.go +++ b/service/dynamodbstreams/types/types.go @@ -11,50 +11,91 @@ import ( // For more information, see Data Types // (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) // in the Amazon DynamoDB Developer Guide. -type AttributeValue struct { +type AttributeValue interface { + isAttributeValue() +} + +// An attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" +// ,"Zebra"] +type AttributeValueMemberSS struct { + Value []string +} + +func (*AttributeValueMemberSS) isAttributeValue() {} + +// An attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", +// "U25vd3k="] +type AttributeValueMemberBS struct { + Value [][]byte +} - // An attribute of type Binary. For example: "B": - // "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" - B []byte +func (*AttributeValueMemberBS) isAttributeValue() {} - // An attribute of type Boolean. For example: "BOOL": true - BOOL *bool +// An attribute of type Number. For example: "N": "123.45" Numbers are sent across +// the network to DynamoDB as strings, to maximize compatibility across languages +// and libraries. However, DynamoDB treats them as number type attributes for +// mathematical operations. +type AttributeValueMemberN struct { + Value string +} - // An attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", - // "U25vd3k="] - BS [][]byte +func (*AttributeValueMemberN) isAttributeValue() {} - // An attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": - // "Coffee"}, {"N", "3.14159"}] - L []AttributeValue +// An attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": +// "Coffee"}, {"N", "3.14159"}] +type AttributeValueMemberL struct { + Value []AttributeValue +} - // An attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": - // "35"}} - M map[string]AttributeValue +func (*AttributeValueMemberL) isAttributeValue() {} - // An attribute of type Number. For example: "N": "123.45" Numbers are sent across - // the network to DynamoDB as strings, to maximize compatibility across languages - // and libraries. However, DynamoDB treats them as number type attributes for - // mathematical operations. - N *string +// An attribute of type Boolean. For example: "BOOL": true +type AttributeValueMemberBOOL struct { + Value bool +} - // An attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", - // "3.14"] Numbers are sent across the network to DynamoDB as strings, to maximize - // compatibility across languages and libraries. However, DynamoDB treats them as - // number type attributes for mathematical operations. - NS []string +func (*AttributeValueMemberBOOL) isAttributeValue() {} - // An attribute of type Null. For example: "NULL": true - NULL *bool +// An attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": +// "35"}} +type AttributeValueMemberM struct { + Value map[string]AttributeValue +} - // An attribute of type String. For example: "S": "Hello" - S *string +func (*AttributeValueMemberM) isAttributeValue() {} - // An attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" - // ,"Zebra"] - SS []string +// An attribute of type Null. For example: "NULL": true +type AttributeValueMemberNULL struct { + Value bool } +func (*AttributeValueMemberNULL) isAttributeValue() {} + +// An attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", +// "3.14"] Numbers are sent across the network to DynamoDB as strings, to maximize +// compatibility across languages and libraries. However, DynamoDB treats them as +// number type attributes for mathematical operations. +type AttributeValueMemberNS struct { + Value []string +} + +func (*AttributeValueMemberNS) isAttributeValue() {} + +// An attribute of type Binary. For example: "B": +// "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" +type AttributeValueMemberB struct { + Value []byte +} + +func (*AttributeValueMemberB) isAttributeValue() {} + +// An attribute of type String. For example: "S": "Hello" +type AttributeValueMemberS struct { + Value string +} + +func (*AttributeValueMemberS) isAttributeValue() {} + // Contains details about the type of identity that made the request. type Identity struct { @@ -305,3 +346,12 @@ type StreamRecord struct { // - both the new and the old item images of the item. StreamViewType StreamViewType } + +// UnknownUnionMember is returned when a union member is returned over the wire, +// but has an unknown tag. +type UnknownUnionMember struct { + Tag string + Value []byte +} + +func (*UnknownUnionMember) isAttributeValue() {}