Vendor aws-sdk-go (dep ensure) (#178)

This commit is contained in:
Onur Filiz
2018-04-27 18:18:36 -07:00
committed by Robbie Zhang
parent 228a1f7a5f
commit 8d67098d06
2785 changed files with 1973570 additions and 25 deletions

12220
vendor/github.com/aws/aws-sdk-go/service/dynamodb/api.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
package dynamodb
import (
"bytes"
"hash/crc32"
"io"
"io/ioutil"
"math"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/request"
)
type retryer struct {
client.DefaultRetryer
}
func (d retryer) RetryRules(r *request.Request) time.Duration {
delay := time.Duration(math.Pow(2, float64(r.RetryCount))) * 50
return delay * time.Millisecond
}
func init() {
initClient = func(c *client.Client) {
if c.Config.Retryer == nil {
// Only override the retryer with a custom one if the config
// does not already contain a retryer
setCustomRetryer(c)
}
c.Handlers.Build.PushBack(disableCompression)
c.Handlers.Unmarshal.PushFront(validateCRC32)
}
}
func setCustomRetryer(c *client.Client) {
maxRetries := aws.IntValue(c.Config.MaxRetries)
if c.Config.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
maxRetries = 10
}
c.Retryer = retryer{
DefaultRetryer: client.DefaultRetryer{
NumMaxRetries: maxRetries,
},
}
}
func drainBody(b io.ReadCloser, length int64) (out *bytes.Buffer, err error) {
if length < 0 {
length = 0
}
buf := bytes.NewBuffer(make([]byte, 0, length))
if _, err = buf.ReadFrom(b); err != nil {
return nil, err
}
if err = b.Close(); err != nil {
return nil, err
}
return buf, nil
}
func disableCompression(r *request.Request) {
r.HTTPRequest.Header.Set("Accept-Encoding", "identity")
}
func validateCRC32(r *request.Request) {
if r.Error != nil {
return // already have an error, no need to verify CRC
}
// Checksum validation is off, skip
if aws.BoolValue(r.Config.DisableComputeChecksums) {
return
}
// Try to get CRC from response
header := r.HTTPResponse.Header.Get("X-Amz-Crc32")
if header == "" {
return // No header, skip
}
expected, err := strconv.ParseUint(header, 10, 32)
if err != nil {
return // Could not determine CRC value, skip
}
buf, err := drainBody(r.HTTPResponse.Body, r.HTTPResponse.ContentLength)
if err != nil { // failed to read the response body, skip
return
}
// Reset body for subsequent reads
r.HTTPResponse.Body = ioutil.NopCloser(bytes.NewReader(buf.Bytes()))
// Compute the CRC checksum
crc := crc32.ChecksumIEEE(buf.Bytes())
if crc != uint32(expected) {
// CRC does not match, set a retryable error
r.Retryable = aws.Bool(true)
r.Error = awserr.New("CRC32CheckFailed", "CRC32 integrity check failed", nil)
}
}

View File

@@ -0,0 +1,153 @@
package dynamodb_test
import (
"bytes"
"io/ioutil"
"net/http"
"os"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/awstesting/unit"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
var db *dynamodb.DynamoDB
func TestMain(m *testing.M) {
db = dynamodb.New(unit.Session, &aws.Config{
MaxRetries: aws.Int(2),
})
db.Handlers.Send.Clear() // mock sending
os.Exit(m.Run())
}
func mockCRCResponse(svc *dynamodb.DynamoDB, status int, body, crc string) (req *request.Request) {
header := http.Header{}
header.Set("x-amz-crc32", crc)
req, _ = svc.ListTablesRequest(nil)
req.Handlers.Send.PushBack(func(*request.Request) {
req.HTTPResponse = &http.Response{
ContentLength: int64(len(body)),
StatusCode: status,
Body: ioutil.NopCloser(bytes.NewReader([]byte(body))),
Header: header,
}
})
req.Send()
return
}
func TestDefaultRetryRules(t *testing.T) {
d := dynamodb.New(unit.Session, &aws.Config{MaxRetries: aws.Int(-1)})
if e, a := 10, d.MaxRetries(); e != a {
t.Errorf("expect %d max retries, got %d", e, a)
}
}
func TestCustomRetryRules(t *testing.T) {
d := dynamodb.New(unit.Session, &aws.Config{MaxRetries: aws.Int(2)})
if e, a := 2, d.MaxRetries(); e != a {
t.Errorf("expect %d max retries, got %d", e, a)
}
}
type testCustomRetryer struct {
client.DefaultRetryer
}
func TestCustomRetry_FromConfig(t *testing.T) {
d := dynamodb.New(unit.Session, &aws.Config{
Retryer: testCustomRetryer{client.DefaultRetryer{NumMaxRetries: 9}},
})
if _, ok := d.Retryer.(testCustomRetryer); !ok {
t.Errorf("expect retryer to be testCustomRetryer, but got %T", d.Retryer)
}
if e, a := 9, d.MaxRetries(); e != a {
t.Errorf("expect %d max retries from custom retryer, got %d", e, a)
}
}
func TestValidateCRC32NoHeaderSkip(t *testing.T) {
req := mockCRCResponse(db, 200, "{}", "")
if req.Error != nil {
t.Errorf("expect no error, got %v", req.Error)
}
}
func TestValidateCRC32InvalidHeaderSkip(t *testing.T) {
req := mockCRCResponse(db, 200, "{}", "ABC")
if req.Error != nil {
t.Errorf("expect no error, got %v", req.Error)
}
}
func TestValidateCRC32AlreadyErrorSkip(t *testing.T) {
req := mockCRCResponse(db, 400, "{}", "1234")
if req.Error == nil {
t.Fatalf("expect error, but got none")
}
aerr := req.Error.(awserr.Error)
if aerr.Code() == "CRC32CheckFailed" {
t.Errorf("expect error code not to be CRC32CheckFailed")
}
}
func TestValidateCRC32IsValid(t *testing.T) {
req := mockCRCResponse(db, 200, `{"TableNames":["A"]}`, "3090163698")
if req.Error != nil {
t.Fatalf("expect no error, got %v", req.Error)
}
// CRC check does not affect output parsing
out := req.Data.(*dynamodb.ListTablesOutput)
if e, a := "A", *out.TableNames[0]; e != a {
t.Errorf("expect %q table name, got %q", e, a)
}
}
func TestValidateCRC32DoesNotMatch(t *testing.T) {
req := mockCRCResponse(db, 200, "{}", "1234")
if req.Error == nil {
t.Fatalf("expect error, but got none")
}
aerr := req.Error.(awserr.Error)
if e, a := "CRC32CheckFailed", aerr.Code(); e != a {
t.Errorf("expect %s error code, got %s", e, a)
}
if e, a := 2, req.RetryCount; e != a {
t.Errorf("expect %d retry count, got %d", e, a)
}
}
func TestValidateCRC32DoesNotMatchNoComputeChecksum(t *testing.T) {
svc := dynamodb.New(unit.Session, &aws.Config{
MaxRetries: aws.Int(2),
DisableComputeChecksums: aws.Bool(true),
})
svc.Handlers.Send.Clear() // mock sending
req := mockCRCResponse(svc, 200, `{"TableNames":["A"]}`, "1234")
if req.Error != nil {
t.Fatalf("expect no error, got %v", req.Error)
}
if e, a := 0, req.RetryCount; e != a {
t.Errorf("expect %d retry count, got %d", e, a)
}
// CRC check disabled. Does not affect output parsing
out := req.Data.(*dynamodb.ListTablesOutput)
if e, a := "A", *out.TableNames[0]; e != a {
t.Errorf("expect %q table name, got %q", e, a)
}
}

View File

@@ -0,0 +1,45 @@
// Code generated by private/model/cli/gen-api/main.go. DO NOT EDIT.
// Package dynamodb provides the client and types for making API
// requests to Amazon DynamoDB.
//
// Amazon DynamoDB is a fully managed NoSQL database service that provides fast
// and predictable performance with seamless scalability. DynamoDB lets you
// offload the administrative burdens of operating and scaling a distributed
// database, so that you don't have to worry about hardware provisioning, setup
// and configuration, replication, software patching, or cluster scaling.
//
// With DynamoDB, you can create database tables that can store and retrieve
// any amount of data, and serve any level of request traffic. You can scale
// up or scale down your tables' throughput capacity without downtime or performance
// degradation, and use the AWS Management Console to monitor resource utilization
// and performance metrics.
//
// DynamoDB automatically spreads the data and traffic for your tables over
// a sufficient number of servers to handle your throughput and storage requirements,
// while maintaining consistent and fast performance. All of your data is stored
// on solid state disks (SSDs) and automatically replicated across multiple
// Availability Zones in an AWS region, providing built-in high availability
// and data durability.
//
// See https://docs.aws.amazon.com/goto/WebAPI/dynamodb-2012-08-10 for more information on this service.
//
// See dynamodb package documentation for more information.
// https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/
//
// Using the Client
//
// To contact Amazon DynamoDB with the SDK use the New function to create
// a new service client. With that client you can make API requests to the service.
// These clients are safe to use concurrently.
//
// See the SDK's documentation for more information on how to use the SDK.
// https://docs.aws.amazon.com/sdk-for-go/api/
//
// See aws.Config documentation for more information on configuring SDK clients.
// https://docs.aws.amazon.com/sdk-for-go/api/aws/#Config
//
// See the Amazon DynamoDB client DynamoDB for more
// information on creating client for this service.
// https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/#New
package dynamodb

View File

@@ -0,0 +1,27 @@
/*
AttributeValue Marshaling and Unmarshaling Helpers
Utility helpers to marshal and unmarshal AttributeValue to and
from Go types can be found in the dynamodbattribute sub package. This package
provides has specialized functions for the common ways of working with
AttributeValues. Such as map[string]*AttributeValue, []*AttributeValue, and
directly with *AttributeValue. This is helpful for marshaling Go types for API
operations such as PutItem, and unmarshaling Query and Scan APIs' responses.
See the dynamodbattribute package documentation for more information.
https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/dynamodbattribute/
Expression Builders
The expression package provides utility types and functions to build DynamoDB
expression for type safe construction of API ExpressionAttributeNames, and
ExpressionAttribute Values.
The package represents the various DynamoDB Expressions as structs named
accordingly. For example, ConditionBuilder represents a DynamoDB Condition
Expression, an UpdateBuilder represents a DynamoDB Update Expression, and so on.
See the expression package documentation for more information.
https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/expression/
*/
package dynamodb

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,271 @@
package dynamodbattribute
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
func TestMarshalErrorTypes(t *testing.T) {
var _ awserr.Error = (*InvalidMarshalError)(nil)
var _ awserr.Error = (*unsupportedMarshalTypeError)(nil)
}
func TestMarshalShared(t *testing.T) {
for i, c := range sharedTestCases {
av, err := Marshal(c.expected)
assertConvertTest(t, i, av, c.in, err, c.err)
}
}
func TestMarshalListShared(t *testing.T) {
for i, c := range sharedListTestCases {
av, err := MarshalList(c.expected)
assertConvertTest(t, i, av, c.in, err, c.err)
}
}
func TestMarshalMapShared(t *testing.T) {
for i, c := range sharedMapTestCases {
av, err := MarshalMap(c.expected)
assertConvertTest(t, i, av, c.in, err, c.err)
}
}
type marshalMarshaler struct {
Value string
Value2 int
Value3 bool
Value4 time.Time
}
func (m *marshalMarshaler) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
av.M = map[string]*dynamodb.AttributeValue{
"abc": {S: &m.Value},
"def": {N: aws.String(fmt.Sprintf("%d", m.Value2))},
"ghi": {BOOL: &m.Value3},
"jkl": {S: aws.String(m.Value4.Format(time.RFC3339Nano))},
}
return nil
}
func TestMarshalMashaler(t *testing.T) {
m := &marshalMarshaler{
Value: "value",
Value2: 123,
Value3: true,
Value4: testDate,
}
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("value")},
"def": {N: aws.String("123")},
"ghi": {BOOL: aws.Bool(true)},
"jkl": {S: aws.String("2016-05-03T17:06:26.209072Z")},
},
}
actual, err := Marshal(m)
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 := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Values": {L: []*dynamodb.AttributeValue{
{S: aws.String("abc")},
{S: aws.String("123")},
}},
},
}
m := testOmitEmptyElemListStruct{Values: []string{"abc", "", "123"}}
actual, err := Marshal(m)
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 TestMarshalMapOmitEmptyElem(t *testing.T) {
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Values": {M: map[string]*dynamodb.AttributeValue{
"abc": {N: aws.String("123")},
"klm": {S: aws.String("abc")},
}},
},
}
m := testOmitEmptyElemMapStruct{Values: map[string]interface{}{
"abc": 123.,
"efg": nil,
"hij": "",
"klm": "abc",
}}
actual, err := Marshal(m)
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 testOmitEmptyScalar struct {
IntZero int `dynamodbav:",omitempty"`
IntPtrNil *int `dynamodbav:",omitempty"`
IntPtrSetZero *int `dynamodbav:",omitempty"`
}
func TestMarshalOmitEmpty(t *testing.T) {
expect := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"IntPtrSetZero": {N: aws.String("0")},
},
}
m := testOmitEmptyScalar{IntPtrSetZero: aws.Int(0)}
actual, err := Marshal(m)
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 := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Aint": {
N: aws.String("321"),
},
"Bint": {
N: aws.String("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 := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Normal": {
S: aws.String("1970-01-01T00:02:03Z"),
},
"Tagged": {
N: aws.String("456"),
},
"Typed": {
N: aws.String("789"),
},
},
}
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 := &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Normal": {
S: aws.String("1970-01-01T00:02:03Z"),
},
"Tagged": {
N: aws.String("456"),
},
},
}
if e, a := expect, actual; !reflect.DeepEqual(e, a) {
t.Errorf("expect %v, got %v", e, a)
}
}

View File

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

View File

@@ -0,0 +1,116 @@
package dynamodbattribute
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(), MarshalOptions{SupportJSONTags: true})
for j, f := range fields {
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 TestFieldByName(t *testing.T) {
fields := []field{
{Name: "Abc"}, {Name: "mixCase"}, {Name: "UPPERCASE"},
}
cases := []struct {
Name, FieldName string
Found bool
}{
{"abc", "Abc", true}, {"ABC", "Abc", true}, {"Abc", "Abc", true},
{"123", "", false},
{"ab", "", false},
{"MixCase", "mixCase", true},
{"uppercase", "UPPERCASE", true}, {"UPPERCASE", "UPPERCASE", true},
}
for _, c := range cases {
f, ok := fieldByName(fields, c.Name)
if e, a := c.Found, ok; e != a {
t.Errorf("expect %v, got %v", e, a)
}
if ok {
if e, a := c.FieldName, f.Name; e != a {
t.Errorf("expect %v, got %v", e, a)
}
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,405 @@
package dynamodbattribute
import (
"reflect"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
type testBinarySetStruct struct {
Binarys [][]byte `dynamodbav:",binaryset"`
}
type testNumberSetStruct struct {
Numbers []int `dynamodbav:",numberset"`
}
type testStringSetStruct struct {
Strings []string `dynamodbav:",stringset"`
}
type testIntAsStringStruct struct {
Value int `dynamodbav:",string"`
}
type testOmitEmptyStruct struct {
Value string `dynamodbav:",omitempty"`
Value2 *string `dynamodbav:",omitempty"`
Value3 int
}
type testAliasedString string
type testAliasedStringSlice []string
type testAliasedInt int
type testAliasedIntSlice []int
type testAliasedMap map[string]int
type testAliasedSlice []string
type testAliasedByteSlice []byte
type testAliasedBool bool
type testAliasedBoolSlice []bool
type testAliasedStruct struct {
Value testAliasedString
Value2 testAliasedInt
Value3 testAliasedMap
Value4 testAliasedSlice
Value5 testAliasedByteSlice
Value6 []testAliasedInt
Value7 []testAliasedString
Value8 []testAliasedByteSlice `dynamodbav:",binaryset"`
Value9 []testAliasedInt `dynamodbav:",numberset"`
Value10 []testAliasedString `dynamodbav:",stringset"`
Value11 testAliasedIntSlice
Value12 testAliasedStringSlice
Value13 testAliasedBool
Value14 testAliasedBoolSlice
}
type testNamedPointer *int
var testDate, _ = time.Parse(time.RFC3339, "2016-05-03T17:06:26.209072Z")
var sharedTestCases = []struct {
in *dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{ // Binary slice
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: &[]byte{},
expected: []byte{48, 49},
},
{ // Binary slice
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: &[]byte{},
expected: []byte{48, 49},
},
{ // Binary slice oversized
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: func() *[]byte {
v := make([]byte, 0, 10)
return &v
}(),
expected: []byte{48, 49},
},
{ // Binary slice pointer
in: &dynamodb.AttributeValue{B: []byte{48, 49}},
actual: func() **[]byte {
v := make([]byte, 0, 10)
v2 := &v
return &v2
}(),
expected: []byte{48, 49},
},
{ // Bool
in: &dynamodb.AttributeValue{BOOL: aws.Bool(true)},
actual: new(bool),
expected: true,
},
{ // List
in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
{N: aws.String("123")},
}},
actual: &[]int{},
expected: []int{123},
},
{ // Map, interface
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {N: aws.String("123")},
}},
actual: &map[string]int{},
expected: map[string]int{"abc": 123},
},
{ // Map, struct
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"Abc": {N: aws.String("123")},
}},
actual: &struct{ Abc int }{},
expected: struct{ Abc int }{Abc: 123},
},
{ // Map, struct
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {N: aws.String("123")},
}},
actual: &struct {
Abc int `json:"abc" dynamodbav:"abc"`
}{},
expected: struct {
Abc int `json:"abc" dynamodbav:"abc"`
}{Abc: 123},
},
{ // Number, int
in: &dynamodb.AttributeValue{N: aws.String("123")},
actual: new(int),
expected: 123,
},
{ // Number, Float
in: &dynamodb.AttributeValue{N: aws.String("123.1")},
actual: new(float64),
expected: float64(123.1),
},
{ // Null
in: &dynamodb.AttributeValue{NULL: aws.Bool(true)},
actual: new(string),
expected: "",
},
{ // Null ptr
in: &dynamodb.AttributeValue{NULL: aws.Bool(true)},
actual: new(*string),
expected: nil,
},
{ // String
in: &dynamodb.AttributeValue{S: aws.String("abc")},
actual: new(string),
expected: "abc",
},
{ // Binary Set
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Binarys": {BS: [][]byte{{48, 49}, {50, 51}}},
},
},
actual: &testBinarySetStruct{},
expected: testBinarySetStruct{Binarys: [][]byte{{48, 49}, {50, 51}}},
},
{ // Number Set
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Numbers": {NS: []*string{aws.String("123"), aws.String("321")}},
},
},
actual: &testNumberSetStruct{},
expected: testNumberSetStruct{Numbers: []int{123, 321}},
},
{ // String Set
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Strings": {SS: []*string{aws.String("abc"), aws.String("efg")}},
},
},
actual: &testStringSetStruct{},
expected: testStringSetStruct{Strings: []string{"abc", "efg"}},
},
{ // Int value as string
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Value": {S: aws.String("123")},
},
},
actual: &testIntAsStringStruct{},
expected: testIntAsStringStruct{Value: 123},
},
{ // Omitempty
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Value3": {N: aws.String("0")},
},
},
actual: &testOmitEmptyStruct{},
expected: testOmitEmptyStruct{Value: "", Value2: nil, Value3: 0},
},
{ // aliased type
in: &dynamodb.AttributeValue{
M: map[string]*dynamodb.AttributeValue{
"Value": {S: aws.String("123")},
"Value2": {N: aws.String("123")},
"Value3": {M: map[string]*dynamodb.AttributeValue{
"Key": {N: aws.String("321")},
}},
"Value4": {L: []*dynamodb.AttributeValue{
{S: aws.String("1")},
{S: aws.String("2")},
{S: aws.String("3")},
}},
"Value5": {B: []byte{0, 1, 2}},
"Value6": {L: []*dynamodb.AttributeValue{
{N: aws.String("1")},
{N: aws.String("2")},
{N: aws.String("3")},
}},
"Value7": {L: []*dynamodb.AttributeValue{
{S: aws.String("1")},
{S: aws.String("2")},
{S: aws.String("3")},
}},
"Value8": {BS: [][]byte{
{0, 1, 2}, {3, 4, 5},
}},
"Value9": {NS: []*string{
aws.String("1"),
aws.String("2"),
aws.String("3"),
}},
"Value10": {SS: []*string{
aws.String("1"),
aws.String("2"),
aws.String("3"),
}},
"Value11": {L: []*dynamodb.AttributeValue{
{N: aws.String("1")},
{N: aws.String("2")},
{N: aws.String("3")},
}},
"Value12": {L: []*dynamodb.AttributeValue{
{S: aws.String("1")},
{S: aws.String("2")},
{S: aws.String("3")},
}},
"Value13": {BOOL: aws.Bool(true)},
"Value14": {L: []*dynamodb.AttributeValue{
{BOOL: aws.Bool(true)},
{BOOL: aws.Bool(false)},
{BOOL: aws.Bool(true)},
}},
},
},
actual: &testAliasedStruct{},
expected: testAliasedStruct{
Value: "123", Value2: 123,
Value3: testAliasedMap{
"Key": 321,
},
Value4: testAliasedSlice{"1", "2", "3"},
Value5: testAliasedByteSlice{0, 1, 2},
Value6: []testAliasedInt{1, 2, 3},
Value7: []testAliasedString{"1", "2", "3"},
Value8: []testAliasedByteSlice{
{0, 1, 2},
{3, 4, 5},
},
Value9: []testAliasedInt{1, 2, 3},
Value10: []testAliasedString{"1", "2", "3"},
Value11: testAliasedIntSlice{1, 2, 3},
Value12: testAliasedStringSlice{"1", "2", "3"},
Value13: true,
Value14: testAliasedBoolSlice{true, false, true},
},
},
{
in: &dynamodb.AttributeValue{N: aws.String("123")},
actual: new(testNamedPointer),
expected: testNamedPointer(aws.Int(123)),
},
{ // time.Time
in: &dynamodb.AttributeValue{S: aws.String("2016-05-03T17:06:26.209072Z")},
actual: new(time.Time),
expected: testDate,
},
{ // time.Time List
in: &dynamodb.AttributeValue{L: []*dynamodb.AttributeValue{
{S: aws.String("2016-05-03T17:06:26.209072Z")},
{S: aws.String("2016-05-04T17:06:26.209072Z")},
}},
actual: new([]time.Time),
expected: []time.Time{testDate, testDate.Add(24 * time.Hour)},
},
{ // time.Time struct
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("2016-05-03T17:06:26.209072Z")},
}},
actual: &struct {
Abc time.Time `json:"abc" dynamodbav:"abc"`
}{},
expected: struct {
Abc time.Time `json:"abc" dynamodbav:"abc"`
}{Abc: testDate},
},
{ // time.Time ptr struct
in: &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
"abc": {S: aws.String("2016-05-03T17:06:26.209072Z")},
}},
actual: &struct {
Abc *time.Time `json:"abc" dynamodbav:"abc"`
}{},
expected: struct {
Abc *time.Time `json:"abc" dynamodbav:"abc"`
}{Abc: &testDate},
},
}
var sharedListTestCases = []struct {
in []*dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{
in: []*dynamodb.AttributeValue{
{B: []byte{48, 49}},
{BOOL: aws.Bool(true)},
{N: aws.String("123")},
{S: aws.String("123")},
},
actual: func() *[]interface{} {
v := []interface{}{}
return &v
}(),
expected: []interface{}{[]byte{48, 49}, true, 123., "123"},
},
{
in: []*dynamodb.AttributeValue{
{N: aws.String("1")},
{N: aws.String("2")},
{N: aws.String("3")},
},
actual: &[]interface{}{},
expected: []interface{}{1., 2., 3.},
},
}
var sharedMapTestCases = []struct {
in map[string]*dynamodb.AttributeValue
actual, expected interface{}
err error
}{
{
in: map[string]*dynamodb.AttributeValue{
"B": {B: []byte{48, 49}},
"BOOL": {BOOL: aws.Bool(true)},
"N": {N: aws.String("123")},
"S": {S: aws.String("123")},
},
actual: &map[string]interface{}{},
expected: map[string]interface{}{
"B": []byte{48, 49}, "BOOL": true,
"N": 123., "S": "123",
},
},
}
func assertConvertTest(t *testing.T, i int, actual, expected interface{}, err, expectedErr error) {
i++
if expectedErr != nil {
if err != nil {
if e, a := expectedErr, err; !reflect.DeepEqual(e, a) {
t.Errorf("case %d expect %v, got %v", i, e, a)
}
} else {
t.Fatalf("case %d, expected error, %v", i, expectedErr)
}
} else if err != nil {
t.Fatalf("case %d, expect no error, got %v", i, err)
} else {
if e, a := ptrToValue(expected), ptrToValue(actual); !reflect.DeepEqual(e, a) {
t.Errorf("case %d, expect %v, got %v", i, e, a)
}
}
}
func ptrToValue(in interface{}) interface{} {
v := reflect.ValueOf(in)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if !v.IsValid() {
return nil
}
if v.Kind() == reflect.Ptr {
return ptrToValue(v.Interface())
}
return v.Interface()
}

View File

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

View File

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

View File

@@ -0,0 +1,206 @@
// Code generated by private/model/cli/gen-api/main.go. DO NOT EDIT.
// Package dynamodbiface provides an interface to enable mocking the Amazon DynamoDB service client
// for testing your code.
//
// It is important to note that this interface will have breaking changes
// when the service model is updated and adds new API operations, paginators,
// and waiters.
package dynamodbiface
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// DynamoDBAPI provides an interface to enable mocking the
// dynamodb.DynamoDB service client's API operation,
// paginators, and waiters. This make unit testing your code that calls out
// to the SDK's service client's calls easier.
//
// The best way to use this interface is so the SDK's service client's calls
// can be stubbed out for unit testing your code with the SDK without needing
// to inject custom request handlers into the SDK's request pipeline.
//
// // myFunc uses an SDK service client to make a request to
// // Amazon DynamoDB.
// func myFunc(svc dynamodbiface.DynamoDBAPI) bool {
// // Make svc.BatchGetItem request
// }
//
// func main() {
// sess := session.New()
// svc := dynamodb.New(sess)
//
// myFunc(svc)
// }
//
// In your _test.go file:
//
// // Define a mock struct to be used in your unit tests of myFunc.
// type mockDynamoDBClient struct {
// dynamodbiface.DynamoDBAPI
// }
// func (m *mockDynamoDBClient) BatchGetItem(input *dynamodb.BatchGetItemInput) (*dynamodb.BatchGetItemOutput, error) {
// // mock response/functionality
// }
//
// func TestMyFunc(t *testing.T) {
// // Setup Test
// mockSvc := &mockDynamoDBClient{}
//
// myfunc(mockSvc)
//
// // Verify myFunc's functionality
// }
//
// It is important to note that this interface will have breaking changes
// when the service model is updated and adds new API operations, paginators,
// and waiters. Its suggested to use the pattern above for testing, or using
// tooling to generate mocks to satisfy the interfaces.
type DynamoDBAPI interface {
BatchGetItem(*dynamodb.BatchGetItemInput) (*dynamodb.BatchGetItemOutput, error)
BatchGetItemWithContext(aws.Context, *dynamodb.BatchGetItemInput, ...request.Option) (*dynamodb.BatchGetItemOutput, error)
BatchGetItemRequest(*dynamodb.BatchGetItemInput) (*request.Request, *dynamodb.BatchGetItemOutput)
BatchGetItemPages(*dynamodb.BatchGetItemInput, func(*dynamodb.BatchGetItemOutput, bool) bool) error
BatchGetItemPagesWithContext(aws.Context, *dynamodb.BatchGetItemInput, func(*dynamodb.BatchGetItemOutput, bool) bool, ...request.Option) error
BatchWriteItem(*dynamodb.BatchWriteItemInput) (*dynamodb.BatchWriteItemOutput, error)
BatchWriteItemWithContext(aws.Context, *dynamodb.BatchWriteItemInput, ...request.Option) (*dynamodb.BatchWriteItemOutput, error)
BatchWriteItemRequest(*dynamodb.BatchWriteItemInput) (*request.Request, *dynamodb.BatchWriteItemOutput)
CreateBackup(*dynamodb.CreateBackupInput) (*dynamodb.CreateBackupOutput, error)
CreateBackupWithContext(aws.Context, *dynamodb.CreateBackupInput, ...request.Option) (*dynamodb.CreateBackupOutput, error)
CreateBackupRequest(*dynamodb.CreateBackupInput) (*request.Request, *dynamodb.CreateBackupOutput)
CreateGlobalTable(*dynamodb.CreateGlobalTableInput) (*dynamodb.CreateGlobalTableOutput, error)
CreateGlobalTableWithContext(aws.Context, *dynamodb.CreateGlobalTableInput, ...request.Option) (*dynamodb.CreateGlobalTableOutput, error)
CreateGlobalTableRequest(*dynamodb.CreateGlobalTableInput) (*request.Request, *dynamodb.CreateGlobalTableOutput)
CreateTable(*dynamodb.CreateTableInput) (*dynamodb.CreateTableOutput, error)
CreateTableWithContext(aws.Context, *dynamodb.CreateTableInput, ...request.Option) (*dynamodb.CreateTableOutput, error)
CreateTableRequest(*dynamodb.CreateTableInput) (*request.Request, *dynamodb.CreateTableOutput)
DeleteBackup(*dynamodb.DeleteBackupInput) (*dynamodb.DeleteBackupOutput, error)
DeleteBackupWithContext(aws.Context, *dynamodb.DeleteBackupInput, ...request.Option) (*dynamodb.DeleteBackupOutput, error)
DeleteBackupRequest(*dynamodb.DeleteBackupInput) (*request.Request, *dynamodb.DeleteBackupOutput)
DeleteItem(*dynamodb.DeleteItemInput) (*dynamodb.DeleteItemOutput, error)
DeleteItemWithContext(aws.Context, *dynamodb.DeleteItemInput, ...request.Option) (*dynamodb.DeleteItemOutput, error)
DeleteItemRequest(*dynamodb.DeleteItemInput) (*request.Request, *dynamodb.DeleteItemOutput)
DeleteTable(*dynamodb.DeleteTableInput) (*dynamodb.DeleteTableOutput, error)
DeleteTableWithContext(aws.Context, *dynamodb.DeleteTableInput, ...request.Option) (*dynamodb.DeleteTableOutput, error)
DeleteTableRequest(*dynamodb.DeleteTableInput) (*request.Request, *dynamodb.DeleteTableOutput)
DescribeBackup(*dynamodb.DescribeBackupInput) (*dynamodb.DescribeBackupOutput, error)
DescribeBackupWithContext(aws.Context, *dynamodb.DescribeBackupInput, ...request.Option) (*dynamodb.DescribeBackupOutput, error)
DescribeBackupRequest(*dynamodb.DescribeBackupInput) (*request.Request, *dynamodb.DescribeBackupOutput)
DescribeContinuousBackups(*dynamodb.DescribeContinuousBackupsInput) (*dynamodb.DescribeContinuousBackupsOutput, error)
DescribeContinuousBackupsWithContext(aws.Context, *dynamodb.DescribeContinuousBackupsInput, ...request.Option) (*dynamodb.DescribeContinuousBackupsOutput, error)
DescribeContinuousBackupsRequest(*dynamodb.DescribeContinuousBackupsInput) (*request.Request, *dynamodb.DescribeContinuousBackupsOutput)
DescribeGlobalTable(*dynamodb.DescribeGlobalTableInput) (*dynamodb.DescribeGlobalTableOutput, error)
DescribeGlobalTableWithContext(aws.Context, *dynamodb.DescribeGlobalTableInput, ...request.Option) (*dynamodb.DescribeGlobalTableOutput, error)
DescribeGlobalTableRequest(*dynamodb.DescribeGlobalTableInput) (*request.Request, *dynamodb.DescribeGlobalTableOutput)
DescribeLimits(*dynamodb.DescribeLimitsInput) (*dynamodb.DescribeLimitsOutput, error)
DescribeLimitsWithContext(aws.Context, *dynamodb.DescribeLimitsInput, ...request.Option) (*dynamodb.DescribeLimitsOutput, error)
DescribeLimitsRequest(*dynamodb.DescribeLimitsInput) (*request.Request, *dynamodb.DescribeLimitsOutput)
DescribeTable(*dynamodb.DescribeTableInput) (*dynamodb.DescribeTableOutput, error)
DescribeTableWithContext(aws.Context, *dynamodb.DescribeTableInput, ...request.Option) (*dynamodb.DescribeTableOutput, error)
DescribeTableRequest(*dynamodb.DescribeTableInput) (*request.Request, *dynamodb.DescribeTableOutput)
DescribeTimeToLive(*dynamodb.DescribeTimeToLiveInput) (*dynamodb.DescribeTimeToLiveOutput, error)
DescribeTimeToLiveWithContext(aws.Context, *dynamodb.DescribeTimeToLiveInput, ...request.Option) (*dynamodb.DescribeTimeToLiveOutput, error)
DescribeTimeToLiveRequest(*dynamodb.DescribeTimeToLiveInput) (*request.Request, *dynamodb.DescribeTimeToLiveOutput)
GetItem(*dynamodb.GetItemInput) (*dynamodb.GetItemOutput, error)
GetItemWithContext(aws.Context, *dynamodb.GetItemInput, ...request.Option) (*dynamodb.GetItemOutput, error)
GetItemRequest(*dynamodb.GetItemInput) (*request.Request, *dynamodb.GetItemOutput)
ListBackups(*dynamodb.ListBackupsInput) (*dynamodb.ListBackupsOutput, error)
ListBackupsWithContext(aws.Context, *dynamodb.ListBackupsInput, ...request.Option) (*dynamodb.ListBackupsOutput, error)
ListBackupsRequest(*dynamodb.ListBackupsInput) (*request.Request, *dynamodb.ListBackupsOutput)
ListGlobalTables(*dynamodb.ListGlobalTablesInput) (*dynamodb.ListGlobalTablesOutput, error)
ListGlobalTablesWithContext(aws.Context, *dynamodb.ListGlobalTablesInput, ...request.Option) (*dynamodb.ListGlobalTablesOutput, error)
ListGlobalTablesRequest(*dynamodb.ListGlobalTablesInput) (*request.Request, *dynamodb.ListGlobalTablesOutput)
ListTables(*dynamodb.ListTablesInput) (*dynamodb.ListTablesOutput, error)
ListTablesWithContext(aws.Context, *dynamodb.ListTablesInput, ...request.Option) (*dynamodb.ListTablesOutput, error)
ListTablesRequest(*dynamodb.ListTablesInput) (*request.Request, *dynamodb.ListTablesOutput)
ListTablesPages(*dynamodb.ListTablesInput, func(*dynamodb.ListTablesOutput, bool) bool) error
ListTablesPagesWithContext(aws.Context, *dynamodb.ListTablesInput, func(*dynamodb.ListTablesOutput, bool) bool, ...request.Option) error
ListTagsOfResource(*dynamodb.ListTagsOfResourceInput) (*dynamodb.ListTagsOfResourceOutput, error)
ListTagsOfResourceWithContext(aws.Context, *dynamodb.ListTagsOfResourceInput, ...request.Option) (*dynamodb.ListTagsOfResourceOutput, error)
ListTagsOfResourceRequest(*dynamodb.ListTagsOfResourceInput) (*request.Request, *dynamodb.ListTagsOfResourceOutput)
PutItem(*dynamodb.PutItemInput) (*dynamodb.PutItemOutput, error)
PutItemWithContext(aws.Context, *dynamodb.PutItemInput, ...request.Option) (*dynamodb.PutItemOutput, error)
PutItemRequest(*dynamodb.PutItemInput) (*request.Request, *dynamodb.PutItemOutput)
Query(*dynamodb.QueryInput) (*dynamodb.QueryOutput, error)
QueryWithContext(aws.Context, *dynamodb.QueryInput, ...request.Option) (*dynamodb.QueryOutput, error)
QueryRequest(*dynamodb.QueryInput) (*request.Request, *dynamodb.QueryOutput)
QueryPages(*dynamodb.QueryInput, func(*dynamodb.QueryOutput, bool) bool) error
QueryPagesWithContext(aws.Context, *dynamodb.QueryInput, func(*dynamodb.QueryOutput, bool) bool, ...request.Option) error
RestoreTableFromBackup(*dynamodb.RestoreTableFromBackupInput) (*dynamodb.RestoreTableFromBackupOutput, error)
RestoreTableFromBackupWithContext(aws.Context, *dynamodb.RestoreTableFromBackupInput, ...request.Option) (*dynamodb.RestoreTableFromBackupOutput, error)
RestoreTableFromBackupRequest(*dynamodb.RestoreTableFromBackupInput) (*request.Request, *dynamodb.RestoreTableFromBackupOutput)
RestoreTableToPointInTime(*dynamodb.RestoreTableToPointInTimeInput) (*dynamodb.RestoreTableToPointInTimeOutput, error)
RestoreTableToPointInTimeWithContext(aws.Context, *dynamodb.RestoreTableToPointInTimeInput, ...request.Option) (*dynamodb.RestoreTableToPointInTimeOutput, error)
RestoreTableToPointInTimeRequest(*dynamodb.RestoreTableToPointInTimeInput) (*request.Request, *dynamodb.RestoreTableToPointInTimeOutput)
Scan(*dynamodb.ScanInput) (*dynamodb.ScanOutput, error)
ScanWithContext(aws.Context, *dynamodb.ScanInput, ...request.Option) (*dynamodb.ScanOutput, error)
ScanRequest(*dynamodb.ScanInput) (*request.Request, *dynamodb.ScanOutput)
ScanPages(*dynamodb.ScanInput, func(*dynamodb.ScanOutput, bool) bool) error
ScanPagesWithContext(aws.Context, *dynamodb.ScanInput, func(*dynamodb.ScanOutput, bool) bool, ...request.Option) error
TagResource(*dynamodb.TagResourceInput) (*dynamodb.TagResourceOutput, error)
TagResourceWithContext(aws.Context, *dynamodb.TagResourceInput, ...request.Option) (*dynamodb.TagResourceOutput, error)
TagResourceRequest(*dynamodb.TagResourceInput) (*request.Request, *dynamodb.TagResourceOutput)
UntagResource(*dynamodb.UntagResourceInput) (*dynamodb.UntagResourceOutput, error)
UntagResourceWithContext(aws.Context, *dynamodb.UntagResourceInput, ...request.Option) (*dynamodb.UntagResourceOutput, error)
UntagResourceRequest(*dynamodb.UntagResourceInput) (*request.Request, *dynamodb.UntagResourceOutput)
UpdateContinuousBackups(*dynamodb.UpdateContinuousBackupsInput) (*dynamodb.UpdateContinuousBackupsOutput, error)
UpdateContinuousBackupsWithContext(aws.Context, *dynamodb.UpdateContinuousBackupsInput, ...request.Option) (*dynamodb.UpdateContinuousBackupsOutput, error)
UpdateContinuousBackupsRequest(*dynamodb.UpdateContinuousBackupsInput) (*request.Request, *dynamodb.UpdateContinuousBackupsOutput)
UpdateGlobalTable(*dynamodb.UpdateGlobalTableInput) (*dynamodb.UpdateGlobalTableOutput, error)
UpdateGlobalTableWithContext(aws.Context, *dynamodb.UpdateGlobalTableInput, ...request.Option) (*dynamodb.UpdateGlobalTableOutput, error)
UpdateGlobalTableRequest(*dynamodb.UpdateGlobalTableInput) (*request.Request, *dynamodb.UpdateGlobalTableOutput)
UpdateItem(*dynamodb.UpdateItemInput) (*dynamodb.UpdateItemOutput, error)
UpdateItemWithContext(aws.Context, *dynamodb.UpdateItemInput, ...request.Option) (*dynamodb.UpdateItemOutput, error)
UpdateItemRequest(*dynamodb.UpdateItemInput) (*request.Request, *dynamodb.UpdateItemOutput)
UpdateTable(*dynamodb.UpdateTableInput) (*dynamodb.UpdateTableOutput, error)
UpdateTableWithContext(aws.Context, *dynamodb.UpdateTableInput, ...request.Option) (*dynamodb.UpdateTableOutput, error)
UpdateTableRequest(*dynamodb.UpdateTableInput) (*request.Request, *dynamodb.UpdateTableOutput)
UpdateTimeToLive(*dynamodb.UpdateTimeToLiveInput) (*dynamodb.UpdateTimeToLiveOutput, error)
UpdateTimeToLiveWithContext(aws.Context, *dynamodb.UpdateTimeToLiveInput, ...request.Option) (*dynamodb.UpdateTimeToLiveOutput, error)
UpdateTimeToLiveRequest(*dynamodb.UpdateTimeToLiveInput) (*request.Request, *dynamodb.UpdateTimeToLiveOutput)
WaitUntilTableExists(*dynamodb.DescribeTableInput) error
WaitUntilTableExistsWithContext(aws.Context, *dynamodb.DescribeTableInput, ...request.WaiterOption) error
WaitUntilTableNotExists(*dynamodb.DescribeTableInput) error
WaitUntilTableNotExistsWithContext(aws.Context, *dynamodb.DescribeTableInput, ...request.WaiterOption) error
}
var _ DynamoDBAPI = (*dynamodb.DynamoDB)(nil)

View File

@@ -0,0 +1,143 @@
// Code generated by private/model/cli/gen-api/main.go. DO NOT EDIT.
package dynamodb
const (
// ErrCodeBackupInUseException for service response error code
// "BackupInUseException".
//
// There is another ongoing conflicting backup control plane operation on the
// table. The backups is either being created, deleted or restored to a table.
ErrCodeBackupInUseException = "BackupInUseException"
// ErrCodeBackupNotFoundException for service response error code
// "BackupNotFoundException".
//
// Backup not found for the given BackupARN.
ErrCodeBackupNotFoundException = "BackupNotFoundException"
// ErrCodeConditionalCheckFailedException for service response error code
// "ConditionalCheckFailedException".
//
// A condition specified in the operation could not be evaluated.
ErrCodeConditionalCheckFailedException = "ConditionalCheckFailedException"
// ErrCodeContinuousBackupsUnavailableException for service response error code
// "ContinuousBackupsUnavailableException".
//
// Backups have not yet been enabled for this table.
ErrCodeContinuousBackupsUnavailableException = "ContinuousBackupsUnavailableException"
// ErrCodeGlobalTableAlreadyExistsException for service response error code
// "GlobalTableAlreadyExistsException".
//
// The specified global table already exists.
ErrCodeGlobalTableAlreadyExistsException = "GlobalTableAlreadyExistsException"
// ErrCodeGlobalTableNotFoundException for service response error code
// "GlobalTableNotFoundException".
//
// The specified global table does not exist.
ErrCodeGlobalTableNotFoundException = "GlobalTableNotFoundException"
// ErrCodeInternalServerError for service response error code
// "InternalServerError".
//
// An error occurred on the server side.
ErrCodeInternalServerError = "InternalServerError"
// ErrCodeInvalidRestoreTimeException for service response error code
// "InvalidRestoreTimeException".
//
// An invalid restore time was specified. RestoreDateTime must be between EarliestRestorableDateTime
// and LatestRestorableDateTime.
ErrCodeInvalidRestoreTimeException = "InvalidRestoreTimeException"
// ErrCodeItemCollectionSizeLimitExceededException for service response error code
// "ItemCollectionSizeLimitExceededException".
//
// An item collection is too large. This exception is only returned for tables
// that have one or more local secondary indexes.
ErrCodeItemCollectionSizeLimitExceededException = "ItemCollectionSizeLimitExceededException"
// ErrCodeLimitExceededException for service response error code
// "LimitExceededException".
//
// Up to 50 CreateBackup operations are allowed per second, per account. There
// is no limit to the number of daily on-demand backups that can be taken.
//
// Up to 10 simultaneous table operations are allowed per account. These operations
// include CreateTable, UpdateTable, DeleteTable,UpdateTimeToLive, RestoreTableFromBackup,
// and RestoreTableToPointInTime.
//
// For tables with secondary indexes, only one of those tables can be in the
// CREATING state at any point in time. Do not attempt to create more than one
// such table simultaneously.
//
// The total limit of tables in the ACTIVE state is 250.
ErrCodeLimitExceededException = "LimitExceededException"
// ErrCodePointInTimeRecoveryUnavailableException for service response error code
// "PointInTimeRecoveryUnavailableException".
//
// Point in time recovery has not yet been enabled for this source table.
ErrCodePointInTimeRecoveryUnavailableException = "PointInTimeRecoveryUnavailableException"
// ErrCodeProvisionedThroughputExceededException for service response error code
// "ProvisionedThroughputExceededException".
//
// Your request rate is too high. The AWS SDKs for DynamoDB automatically retry
// requests that receive this exception. Your request is eventually successful,
// unless your retry queue is too large to finish. Reduce the frequency of requests
// and use exponential backoff. For more information, go to Error Retries and
// Exponential Backoff (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.RetryAndBackoff)
// in the Amazon DynamoDB Developer Guide.
ErrCodeProvisionedThroughputExceededException = "ProvisionedThroughputExceededException"
// ErrCodeReplicaAlreadyExistsException for service response error code
// "ReplicaAlreadyExistsException".
//
// The specified replica is already part of the global table.
ErrCodeReplicaAlreadyExistsException = "ReplicaAlreadyExistsException"
// ErrCodeReplicaNotFoundException for service response error code
// "ReplicaNotFoundException".
//
// The specified replica is no longer part of the global table.
ErrCodeReplicaNotFoundException = "ReplicaNotFoundException"
// ErrCodeResourceInUseException for service response error code
// "ResourceInUseException".
//
// The operation conflicts with the resource's availability. For example, you
// attempted to recreate an existing table, or tried to delete a table currently
// in the CREATING state.
ErrCodeResourceInUseException = "ResourceInUseException"
// ErrCodeResourceNotFoundException for service response error code
// "ResourceNotFoundException".
//
// The operation tried to access a nonexistent table or index. The resource
// might not be specified correctly, or its status might not be ACTIVE.
ErrCodeResourceNotFoundException = "ResourceNotFoundException"
// ErrCodeTableAlreadyExistsException for service response error code
// "TableAlreadyExistsException".
//
// A target table with the specified name already exists.
ErrCodeTableAlreadyExistsException = "TableAlreadyExistsException"
// ErrCodeTableInUseException for service response error code
// "TableInUseException".
//
// A target table with the specified name is either being created or deleted.
ErrCodeTableInUseException = "TableInUseException"
// ErrCodeTableNotFoundException for service response error code
// "TableNotFoundException".
//
// A source table with the name TableName does not currently exist within the
// subscriber's account.
ErrCodeTableNotFoundException = "TableNotFoundException"
)

View File

@@ -0,0 +1,677 @@
// Code generated by private/model/cli/gen-api/main.go. DO NOT EDIT.
package dynamodb_test
import (
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
var _ time.Duration
var _ strings.Reader
var _ aws.Config
func parseTime(layout, value string) *time.Time {
t, err := time.Parse(layout, value)
if err != nil {
panic(err)
}
return &t
}
// To retrieve multiple items from a table
//
// This example reads multiple items from the Music table using a batch of three GetItem
// requests. Only the AlbumTitle attribute is returned.
func ExampleDynamoDB_BatchGetItem_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.BatchGetItemInput{
RequestItems: map[string]*dynamodb.KeysAndAttributes{
"Music": {
Keys: []map[string]*dynamodb.AttributeValue{
{
"Artist": &dynamodb.AttributeValue{
S: aws.String("No One You Know"),
},
"SongTitle": &dynamodb.AttributeValue{
S: aws.String("Call Me Today"),
},
},
{
"Artist": &dynamodb.AttributeValue{
S: aws.String("Acme Band"),
},
"SongTitle": &dynamodb.AttributeValue{
S: aws.String("Happy Day"),
},
},
{
"Artist": &dynamodb.AttributeValue{
S: aws.String("No One You Know"),
},
"SongTitle": &dynamodb.AttributeValue{
S: aws.String("Scared of My Shadow"),
},
},
},
ProjectionExpression: aws.String("AlbumTitle"),
},
},
}
result, err := svc.BatchGetItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To add multiple items to a table
//
// This example adds three new items to the Music table using a batch of three PutItem
// requests.
func ExampleDynamoDB_BatchWriteItem_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.BatchWriteItemInput{
RequestItems: map[string][]*dynamodb.WriteRequest{
"Music": {
{
PutRequest: &dynamodb.PutRequest{
Item: map[string]*dynamodb.AttributeValue{
"AlbumTitle": {
S: aws.String("Somewhat Famous"),
},
"Artist": {
S: aws.String("No One You Know"),
},
"SongTitle": {
S: aws.String("Call Me Today"),
},
},
},
},
{
PutRequest: &dynamodb.PutRequest{
Item: map[string]*dynamodb.AttributeValue{
"AlbumTitle": {
S: aws.String("Songs About Life"),
},
"Artist": {
S: aws.String("Acme Band"),
},
"SongTitle": {
S: aws.String("Happy Day"),
},
},
},
},
{
PutRequest: &dynamodb.PutRequest{
Item: map[string]*dynamodb.AttributeValue{
"AlbumTitle": {
S: aws.String("Blue Sky Blues"),
},
"Artist": {
S: aws.String("No One You Know"),
},
"SongTitle": {
S: aws.String("Scared of My Shadow"),
},
},
},
},
},
},
}
result, err := svc.BatchWriteItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeItemCollectionSizeLimitExceededException:
fmt.Println(dynamodb.ErrCodeItemCollectionSizeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To create a table
//
// This example creates a table named Music.
func ExampleDynamoDB_CreateTable_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.CreateTableInput{
AttributeDefinitions: []*dynamodb.AttributeDefinition{
{
AttributeName: aws.String("Artist"),
AttributeType: aws.String("S"),
},
{
AttributeName: aws.String("SongTitle"),
AttributeType: aws.String("S"),
},
},
KeySchema: []*dynamodb.KeySchemaElement{
{
AttributeName: aws.String("Artist"),
KeyType: aws.String("HASH"),
},
{
AttributeName: aws.String("SongTitle"),
KeyType: aws.String("RANGE"),
},
},
ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(5),
WriteCapacityUnits: aws.Int64(5),
},
TableName: aws.String("Music"),
}
result, err := svc.CreateTable(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeResourceInUseException:
fmt.Println(dynamodb.ErrCodeResourceInUseException, aerr.Error())
case dynamodb.ErrCodeLimitExceededException:
fmt.Println(dynamodb.ErrCodeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To delete an item
//
// This example deletes an item from the Music table.
func ExampleDynamoDB_DeleteItem_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.DeleteItemInput{
Key: map[string]*dynamodb.AttributeValue{
"Artist": {
S: aws.String("No One You Know"),
},
"SongTitle": {
S: aws.String("Scared of My Shadow"),
},
},
TableName: aws.String("Music"),
}
result, err := svc.DeleteItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeConditionalCheckFailedException:
fmt.Println(dynamodb.ErrCodeConditionalCheckFailedException, aerr.Error())
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeItemCollectionSizeLimitExceededException:
fmt.Println(dynamodb.ErrCodeItemCollectionSizeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To delete a table
//
// This example deletes the Music table.
func ExampleDynamoDB_DeleteTable_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.DeleteTableInput{
TableName: aws.String("Music"),
}
result, err := svc.DeleteTable(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeResourceInUseException:
fmt.Println(dynamodb.ErrCodeResourceInUseException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeLimitExceededException:
fmt.Println(dynamodb.ErrCodeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To determine capacity limits per table and account, in the current AWS region
//
// The following example returns the maximum read and write capacity units per table,
// and for the AWS account, in the current AWS region.
func ExampleDynamoDB_DescribeLimits_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.DescribeLimitsInput{}
result, err := svc.DescribeLimits(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To describe a table
//
// This example describes the Music table.
func ExampleDynamoDB_DescribeTable_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.DescribeTableInput{
TableName: aws.String("Music"),
}
result, err := svc.DescribeTable(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To read an item from a table
//
// This example retrieves an item from the Music table. The table has a partition key
// and a sort key (Artist and SongTitle), so you must specify both of these attributes.
func ExampleDynamoDB_GetItem_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.GetItemInput{
Key: map[string]*dynamodb.AttributeValue{
"Artist": {
S: aws.String("Acme Band"),
},
"SongTitle": {
S: aws.String("Happy Day"),
},
},
TableName: aws.String("Music"),
}
result, err := svc.GetItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To list tables
//
// This example lists all of the tables associated with the current AWS account and
// endpoint.
func ExampleDynamoDB_ListTables_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.ListTablesInput{}
result, err := svc.ListTables(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To add an item to a table
//
// This example adds a new item to the Music table.
func ExampleDynamoDB_PutItem_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.PutItemInput{
Item: map[string]*dynamodb.AttributeValue{
"AlbumTitle": {
S: aws.String("Somewhat Famous"),
},
"Artist": {
S: aws.String("No One You Know"),
},
"SongTitle": {
S: aws.String("Call Me Today"),
},
},
ReturnConsumedCapacity: aws.String("TOTAL"),
TableName: aws.String("Music"),
}
result, err := svc.PutItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeConditionalCheckFailedException:
fmt.Println(dynamodb.ErrCodeConditionalCheckFailedException, aerr.Error())
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeItemCollectionSizeLimitExceededException:
fmt.Println(dynamodb.ErrCodeItemCollectionSizeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To query an item
//
// This example queries items in the Music table. The table has a partition key and
// sort key (Artist and SongTitle), but this query only specifies the partition key
// value. It returns song titles by the artist named "No One You Know".
func ExampleDynamoDB_Query_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.QueryInput{
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":v1": {
S: aws.String("No One You Know"),
},
},
KeyConditionExpression: aws.String("Artist = :v1"),
ProjectionExpression: aws.String("SongTitle"),
TableName: aws.String("Music"),
}
result, err := svc.Query(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To scan a table
//
// This example scans the entire Music table, and then narrows the results to songs
// by the artist "No One You Know". For each item, only the album title and song title
// are returned.
func ExampleDynamoDB_Scan_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.ScanInput{
ExpressionAttributeNames: map[string]*string{
"AT": aws.String("AlbumTitle"),
"ST": aws.String("SongTitle"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":a": {
S: aws.String("No One You Know"),
},
},
FilterExpression: aws.String("Artist = :a"),
ProjectionExpression: aws.String("#ST, #AT"),
TableName: aws.String("Music"),
}
result, err := svc.Scan(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To update an item in a table
//
// This example updates an item in the Music table. It adds a new attribute (Year) and
// modifies the AlbumTitle attribute. All of the attributes in the item, as they appear
// after the update, are returned in the response.
func ExampleDynamoDB_UpdateItem_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.UpdateItemInput{
ExpressionAttributeNames: map[string]*string{
"#AT": aws.String("AlbumTitle"),
"#Y": aws.String("Year"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":t": {
S: aws.String("Louder Than Ever"),
},
":y": {
N: aws.String("2015"),
},
},
Key: map[string]*dynamodb.AttributeValue{
"Artist": {
S: aws.String("Acme Band"),
},
"SongTitle": {
S: aws.String("Happy Day"),
},
},
ReturnValues: aws.String("ALL_NEW"),
TableName: aws.String("Music"),
UpdateExpression: aws.String("SET #Y = :y, #AT = :t"),
}
result, err := svc.UpdateItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeConditionalCheckFailedException:
fmt.Println(dynamodb.ErrCodeConditionalCheckFailedException, aerr.Error())
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeItemCollectionSizeLimitExceededException:
fmt.Println(dynamodb.ErrCodeItemCollectionSizeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// To modify a table's provisioned throughput
//
// This example increases the provisioned read and write capacity on the Music table.
func ExampleDynamoDB_UpdateTable_shared00() {
svc := dynamodb.New(session.New())
input := &dynamodb.UpdateTableInput{
ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(10),
WriteCapacityUnits: aws.Int64(10),
},
TableName: aws.String("MusicCollection"),
}
result, err := svc.UpdateTable(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeResourceInUseException:
fmt.Println(dynamodb.ErrCodeResourceInUseException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeLimitExceededException:
fmt.Println(dynamodb.ErrCodeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
/*
Package expression provides types and functions to create Amazon DynamoDB
Expression strings, ExpressionAttributeNames maps, and ExpressionAttributeValues
maps.
Using the Package
The package represents the various DynamoDB Expressions as structs named
accordingly. For example, ConditionBuilder represents a DynamoDB Condition
Expression, an UpdateBuilder represents a DynamoDB Update Expression, and so on.
The following example shows a sample ConditionExpression and how to build an
equilvalent ConditionBuilder
// Let :a be an ExpressionAttributeValue representing the string "No One You
// Know"
condExpr := "Artist = :a"
condBuilder := expression.Name("Artist").Equal(expression.Value("No One You Know"))
In order to retrieve the formatted DynamoDB Expression strings, call the getter
methods on the Expression struct. To create the Expression struct, call the
Build() method on the Builder struct. Because some input structs, such as
QueryInput, can have multiple DynamoDB Expressions, multiple structs
representing various DynamoDB Expressions can be added to the Builder struct.
The following example shows a generic usage of the whole package.
filt := expression.Name("Artist").Equal(expression.Value("No One You Know"))
proj := expression.NamesList(expression.Name("SongTitle"), expression.Name("AlbumTitle"))
expr, err := expression.NewBuilder().WithFilter(filt).WithProjection(proj).Build()
if err != nil {
fmt.Println(err)
}
input := &dynamodb.ScanInput{
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
FilterExpression: expr.Filter(),
ProjectionExpression: expr.Projection(),
TableName: aws.String("Music"),
}
The ExpressionAttributeNames and ExpressionAttributeValues member of the input
struct must always be assigned when using the Expression struct because all item
attribute names and values are aliased. That means that if the
ExpressionAttributeNames and ExpressionAttributeValues member is not assigned
with the corresponding Names() and Values() methods, the DynamoDB operation will
run into a logic error.
*/
package expression

View File

@@ -0,0 +1,59 @@
package expression
import (
"fmt"
)
// InvalidParameterError is returned if invalid parameters are encountered. This
// error specifically refers to situations where parameters are non-empty but
// have an invalid syntax/format. The error message includes the function
// that returned the error originally and the parameter type that was deemed
// invalid.
//
// Example:
//
// // err is of type InvalidParameterError
// _, err := expression.Name("foo..bar").BuildOperand()
type InvalidParameterError struct {
parameterType string
functionName string
}
func (ipe InvalidParameterError) Error() string {
return fmt.Sprintf("%s error: invalid parameter: %s", ipe.functionName, ipe.parameterType)
}
func newInvalidParameterError(funcName, paramType string) InvalidParameterError {
return InvalidParameterError{
parameterType: paramType,
functionName: funcName,
}
}
// UnsetParameterError is returned if parameters are empty and uninitialized.
// This error is returned if opaque structs (ConditionBuilder, NameBuilder,
// Builder, etc) are initialized outside of functions in the package, since all
// structs in the package are designed to be initialized with functions.
//
// Example:
//
// // err is of type UnsetParameterError
// _, err := expression.Builder{}.Build()
// _, err := expression.NewBuilder().
// WithCondition(expression.ConditionBuilder{}).
// Build()
type UnsetParameterError struct {
parameterType string
functionName string
}
func (upe UnsetParameterError) Error() string {
return fmt.Sprintf("%s error: unset parameter: %s", upe.functionName, upe.parameterType)
}
func newUnsetParameterError(funcName, paramType string) UnsetParameterError {
return UnsetParameterError{
parameterType: paramType,
functionName: funcName,
}
}

View File

@@ -0,0 +1,51 @@
// +build go1.7
package expression
import (
"testing"
)
func TestInvalidParameterError(t *testing.T) {
cases := []struct {
name string
input InvalidParameterError
expected string
}{
{
name: "invalid error",
input: newInvalidParameterError("func", "param"),
expected: "func error: invalid parameter: param",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual := c.input.Error()
if e, a := c.expected, actual; e != a {
t.Errorf("expect %v, got %v", e, a)
}
})
}
}
func TestUnsetParameterError(t *testing.T) {
cases := []struct {
name string
input UnsetParameterError
expected string
}{
{
name: "unset error",
input: newUnsetParameterError("func", "param"),
expected: "func error: unset parameter: param",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual := c.input.Error()
if e, a := c.expected, actual; e != a {
t.Errorf("expect %v, got %v", e, a)
}
})
}
}

View File

@@ -0,0 +1,315 @@
package expression_test
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/expression"
)
// Using Projection Expression
//
// This example queries items in the Music table. The table has a partition key and
// sort key (Artist and SongTitle), but this query only specifies the partition key
// value. It returns song titles by the artist named "No One You Know".
func ExampleBuilder_WithProjection() {
svc := dynamodb.New(session.New())
// Construct the Key condition builder
keyCond := expression.Key("Artist").Equal(expression.Value("No One You Know"))
// Create the project expression builder with a names list.
proj := expression.NamesList(expression.Name("SongTitle"))
// Combine the key condition, and projection together as a DynamoDB expression
// builder.
expr, err := expression.NewBuilder().
WithKeyCondition(keyCond).
WithProjection(proj).
Build()
if err != nil {
fmt.Println(err)
}
// Use the built expression to populate the DynamoDB Query's API input
// parameters.
input := &dynamodb.QueryInput{
ExpressionAttributeValues: expr.Values(),
KeyConditionExpression: expr.KeyCondition(),
ProjectionExpression: expr.Projection(),
TableName: aws.String("Music"),
}
result, err := svc.Query(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// Using Key Condition Expression
//
// This example queries items in the Music table. The table has a partition key and
// sort key (Artist and SongTitle), but this query only specifies the partition key
// value. It returns song titles by the artist named "No One You Know".
func ExampleBuilder_WithKeyCondition() {
svc := dynamodb.New(session.New())
// Construct the Key condition builder
keyCond := expression.Key("Artist").Equal(expression.Value("No One You Know"))
// Create the project expression builder with a names list.
proj := expression.NamesList(expression.Name("SongTitle"))
// Combine the key condition, and projection together as a DynamoDB expression
// builder.
expr, err := expression.NewBuilder().
WithKeyCondition(keyCond).
WithProjection(proj).
Build()
if err != nil {
fmt.Println(err)
}
// Use the built expression to populate the DynamoDB Query's API input
// parameters.
input := &dynamodb.QueryInput{
ExpressionAttributeValues: expr.Values(),
KeyConditionExpression: expr.KeyCondition(),
ProjectionExpression: expr.Projection(),
TableName: aws.String("Music"),
}
result, err := svc.Query(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// Using Filter Expression
//
// This example scans the entire Music table, and then narrows the results to songs
// by the artist "No One You Know". For each item, only the album title and song title
// are returned.
func ExampleBuilder_WithFilter() {
svc := dynamodb.New(session.New())
// Construct the filter builder with a name and value.
filt := expression.Name("Artist").Equal(expression.Value("No One You Know"))
// Create the names list projection of names to project.
proj := expression.NamesList(
expression.Name("AlbumTitle"),
expression.Name("SongTitle"),
)
// Using the filter and projections create a DynamoDB expression from the two.
expr, err := expression.NewBuilder().
WithFilter(filt).
WithProjection(proj).
Build()
if err != nil {
fmt.Println(err)
}
// Use the built expression to populate the DynamoDB Scan API input parameters.
input := &dynamodb.ScanInput{
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
FilterExpression: expr.Filter(),
ProjectionExpression: expr.Projection(),
TableName: aws.String("Music"),
}
result, err := svc.Scan(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// Using Update Expression
//
// This example updates an item in the Music table. It adds a new attribute (Year) and
// modifies the AlbumTitle attribute. All of the attributes in the item, as they appear
// after the update, are returned in the response.
func ExampleBuilder_WithUpdate() {
svc := dynamodb.New(session.New())
// Create an update to set two fields in the table.
update := expression.Set(
expression.Name("Year"),
expression.Value(2015),
).Set(
expression.Name("AlbumTitle"),
expression.Value("Louder Than Ever"),
)
// Create the DynamoDB expression from the Update.
expr, err := expression.NewBuilder().
WithUpdate(update).
Build()
// Use the built expression to populate the DynamoDB UpdateItem API
// input parameters.
input := &dynamodb.UpdateItemInput{
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
Key: map[string]*dynamodb.AttributeValue{
"Artist": {
S: aws.String("Acme Band"),
},
"SongTitle": {
S: aws.String("Happy Day"),
},
},
ReturnValues: aws.String("ALL_NEW"),
TableName: aws.String("Music"),
UpdateExpression: expr.Update(),
}
result, err := svc.UpdateItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeConditionalCheckFailedException:
fmt.Println(dynamodb.ErrCodeConditionalCheckFailedException, aerr.Error())
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeItemCollectionSizeLimitExceededException:
fmt.Println(dynamodb.ErrCodeItemCollectionSizeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}
// Using Condition Expression
//
// This example deletes an item from the Music table if the rating is lower than
// 7.
func ExampleBuilder_WithCondition() {
svc := dynamodb.New(session.New())
// Create a condition where the Rating field must be less than 7.
cond := expression.Name("Rating").LessThan(expression.Value(7))
// Create a DynamoDB expression from the condition.
expr, err := expression.NewBuilder().
WithCondition(cond).
Build()
if err != nil {
fmt.Println(err)
}
// Use the built expression to populate the DeleteItem API operation with the
// condition expression.
input := &dynamodb.DeleteItemInput{
Key: map[string]*dynamodb.AttributeValue{
"Artist": {
S: aws.String("No One You Know"),
},
"SongTitle": {
S: aws.String("Scared of My Shadow"),
},
},
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
ConditionExpression: expr.Condition(),
TableName: aws.String("Music"),
}
result, err := svc.DeleteItem(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case dynamodb.ErrCodeConditionalCheckFailedException:
fmt.Println(dynamodb.ErrCodeConditionalCheckFailedException, aerr.Error())
case dynamodb.ErrCodeProvisionedThroughputExceededException:
fmt.Println(dynamodb.ErrCodeProvisionedThroughputExceededException, aerr.Error())
case dynamodb.ErrCodeResourceNotFoundException:
fmt.Println(dynamodb.ErrCodeResourceNotFoundException, aerr.Error())
case dynamodb.ErrCodeItemCollectionSizeLimitExceededException:
fmt.Println(dynamodb.ErrCodeItemCollectionSizeLimitExceededException, aerr.Error())
case dynamodb.ErrCodeInternalServerError:
fmt.Println(dynamodb.ErrCodeInternalServerError, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
fmt.Println(result)
}

View File

@@ -0,0 +1,635 @@
package expression
import (
"fmt"
"sort"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// expressionType specifies the type of Expression. Declaring this type is used
// to eliminate magic strings
type expressionType string
const (
projection expressionType = "projection"
keyCondition = "keyCondition"
condition = "condition"
filter = "filter"
update = "update"
)
// Implement the Sort interface
type typeList []expressionType
func (l typeList) Len() int {
return len(l)
}
func (l typeList) Less(i, j int) bool {
return string(l[i]) < string(l[j])
}
func (l typeList) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}
// Builder represents the struct that builds the Expression struct. Methods such
// as WithProjection() and WithCondition() can add different kinds of DynamoDB
// Expressions to the Builder. The method Build() creates an Expression struct
// with the specified types of DynamoDB Expressions.
//
// Example:
//
// keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
//
// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
// expression := builder.Build()
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// ProjectionExpression: expression.Projection(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
type Builder struct {
expressionMap map[expressionType]treeBuilder
}
// NewBuilder returns an empty Builder struct. Methods such as WithProjection()
// and WithCondition() can add different kinds of DynamoDB Expressions to the
// Builder. The method Build() creates an Expression struct with the specified
// types of DynamoDB Expressions.
//
// Example:
//
// keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
func NewBuilder() Builder {
return Builder{}
}
// Build builds an Expression struct representing multiple types of DynamoDB
// Expressions. Getter methods on the resulting Expression struct returns the
// DynamoDB Expression strings as well as the maps that correspond to
// ExpressionAttributeNames and ExpressionAttributeValues. Calling Build() on an
// empty Builder returns the typed error EmptyParameterError.
//
// Example:
//
// // keyCond represents the Key Condition Expression
// keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
// // proj represents the Projection Expression
// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
//
// // Add keyCond and proj to builder as a Key Condition and Projection
// // respectively
// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
// expression := builder.Build()
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// ProjectionExpression: expression.Projection(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
func (b Builder) Build() (Expression, error) {
if b.expressionMap == nil {
return Expression{}, newUnsetParameterError("Build", "Builder")
}
aliasList, expressionMap, err := b.buildChildTrees()
if err != nil {
return Expression{}, err
}
expression := Expression{
expressionMap: expressionMap,
}
if len(aliasList.namesList) != 0 {
namesMap := map[string]*string{}
for ind, val := range aliasList.namesList {
namesMap[fmt.Sprintf("#%v", ind)] = aws.String(val)
}
expression.namesMap = namesMap
}
if len(aliasList.valuesList) != 0 {
valuesMap := map[string]*dynamodb.AttributeValue{}
for i := 0; i < len(aliasList.valuesList); i++ {
valuesMap[fmt.Sprintf(":%v", i)] = &aliasList.valuesList[i]
}
expression.valuesMap = valuesMap
}
return expression, nil
}
// buildChildTrees compiles the list of treeBuilders that are the children of
// the argument Builder. The returned aliasList represents all the alias tokens
// used in the expression strings. The returned map[string]string maps the type
// of expression (i.e. "condition", "update") to the appropriate expression
// string.
func (b Builder) buildChildTrees() (aliasList, map[expressionType]string, error) {
aList := aliasList{}
formattedExpressions := map[expressionType]string{}
keys := typeList{}
for expressionType := range b.expressionMap {
keys = append(keys, expressionType)
}
sort.Sort(keys)
for _, key := range keys {
node, err := b.expressionMap[key].buildTree()
if err != nil {
return aliasList{}, nil, err
}
formattedExpression, err := node.buildExpressionString(&aList)
if err != nil {
return aliasList{}, nil, err
}
formattedExpressions[key] = formattedExpression
}
return aList, formattedExpressions, nil
}
// WithCondition method adds the argument ConditionBuilder as a Condition
// Expression to the argument Builder. If the argument Builder already has a
// ConditionBuilder representing a Condition Expression, WithCondition()
// overwrites the existing ConditionBuilder.
//
// Example:
//
// // let builder be an existing Builder{} and cond be an existing
// // ConditionBuilder{}
// builder = builder.WithCondition(cond)
//
// // add other DynamoDB Expressions to the builder. let proj be an already
// // existing ProjectionBuilder
// builder = builder.WithProjection(proj)
// // create an Expression struct
// expression := builder.Build()
func (b Builder) WithCondition(conditionBuilder ConditionBuilder) Builder {
if b.expressionMap == nil {
b.expressionMap = map[expressionType]treeBuilder{}
}
b.expressionMap[condition] = conditionBuilder
return b
}
// WithProjection method adds the argument ProjectionBuilder as a Projection
// Expression to the argument Builder. If the argument Builder already has a
// ProjectionBuilder representing a Projection Expression, WithProjection()
// overwrites the existing ProjectionBuilder.
//
// Example:
//
// // let builder be an existing Builder{} and proj be an existing
// // ProjectionBuilder{}
// builder = builder.WithProjection(proj)
//
// // add other DynamoDB Expressions to the builder. let cond be an already
// // existing ConditionBuilder
// builder = builder.WithCondition(cond)
// // create an Expression struct
// expression := builder.Build()
func (b Builder) WithProjection(projectionBuilder ProjectionBuilder) Builder {
if b.expressionMap == nil {
b.expressionMap = map[expressionType]treeBuilder{}
}
b.expressionMap[projection] = projectionBuilder
return b
}
// WithKeyCondition method adds the argument KeyConditionBuilder as a Key
// Condition Expression to the argument Builder. If the argument Builder already
// has a KeyConditionBuilder representing a Key Condition Expression,
// WithKeyCondition() overwrites the existing KeyConditionBuilder.
//
// Example:
//
// // let builder be an existing Builder{} and keyCond be an existing
// // KeyConditionBuilder{}
// builder = builder.WithKeyCondition(keyCond)
//
// // add other DynamoDB Expressions to the builder. let cond be an already
// // existing ConditionBuilder
// builder = builder.WithCondition(cond)
// // create an Expression struct
// expression := builder.Build()
func (b Builder) WithKeyCondition(keyConditionBuilder KeyConditionBuilder) Builder {
if b.expressionMap == nil {
b.expressionMap = map[expressionType]treeBuilder{}
}
b.expressionMap[keyCondition] = keyConditionBuilder
return b
}
// WithFilter method adds the argument ConditionBuilder as a Filter Expression
// to the argument Builder. If the argument Builder already has a
// ConditionBuilder representing a Filter Expression, WithFilter()
// overwrites the existing ConditionBuilder.
//
// Example:
//
// // let builder be an existing Builder{} and filt be an existing
// // ConditionBuilder{}
// builder = builder.WithFilter(filt)
//
// // add other DynamoDB Expressions to the builder. let cond be an already
// // existing ConditionBuilder
// builder = builder.WithCondition(cond)
// // create an Expression struct
// expression := builder.Build()
func (b Builder) WithFilter(filterBuilder ConditionBuilder) Builder {
if b.expressionMap == nil {
b.expressionMap = map[expressionType]treeBuilder{}
}
b.expressionMap[filter] = filterBuilder
return b
}
// WithUpdate method adds the argument UpdateBuilder as an Update Expression
// to the argument Builder. If the argument Builder already has a UpdateBuilder
// representing a Update Expression, WithUpdate() overwrites the existing
// UpdateBuilder.
//
// Example:
//
// // let builder be an existing Builder{} and update be an existing
// // UpdateBuilder{}
// builder = builder.WithUpdate(update)
//
// // add other DynamoDB Expressions to the builder. let cond be an already
// // existing ConditionBuilder
// builder = builder.WithCondition(cond)
// // create an Expression struct
// expression := builder.Build()
func (b Builder) WithUpdate(updateBuilder UpdateBuilder) Builder {
if b.expressionMap == nil {
b.expressionMap = map[expressionType]treeBuilder{}
}
b.expressionMap[update] = updateBuilder
return b
}
// Expression represents a collection of DynamoDB Expressions. The getter
// methods of the Expression struct retrieves the formatted DynamoDB
// Expressions, ExpressionAttributeNames, and ExpressionAttributeValues.
//
// Example:
//
// // keyCond represents the Key Condition Expression
// keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
// // proj represents the Projection Expression
// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
//
// // Add keyCond and proj to builder as a Key Condition and Projection
// // respectively
// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
// expression := builder.Build()
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// ProjectionExpression: expression.Projection(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
type Expression struct {
expressionMap map[expressionType]string
namesMap map[string]*string
valuesMap map[string]*dynamodb.AttributeValue
}
// treeBuilder interface is fulfilled by builder structs that represent
// different types of Expressions.
type treeBuilder interface {
// buildTree creates the tree structure of exprNodes. The tree structure
// of exprNodes are traversed in order to build the string representing
// different types of Expressions as well as the maps that represent
// ExpressionAttributeNames and ExpressionAttributeValues.
buildTree() (exprNode, error)
}
// Condition returns the *string corresponding to the Condition Expression
// of the argument Expression. This method is used to satisfy the members of
// DynamoDB input structs. If the Expression does not have a condition
// expression this method returns nil.
//
// Example:
//
// // let expression be an instance of Expression{}
//
// deleteInput := dynamodb.DeleteItemInput{
// ConditionExpression: expression.Condition(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// Key: map[string]*dynamodb.AttributeValue{
// "PartitionKey": &dynamodb.AttributeValue{
// S: aws.String("SomeKey"),
// },
// },
// TableName: aws.String("SomeTable"),
// }
func (e Expression) Condition() *string {
return e.returnExpression(condition)
}
// Filter returns the *string corresponding to the Filter Expression of the
// argument Expression. This method is used to satisfy the members of DynamoDB
// input structs. If the Expression does not have a filter expression this
// method returns nil.
//
// Example:
//
// // let expression be an instance of Expression{}
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// FilterExpression: expression.Filter(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
func (e Expression) Filter() *string {
return e.returnExpression(filter)
}
// Projection returns the *string corresponding to the Projection Expression
// of the argument Expression. This method is used to satisfy the members of
// DynamoDB input structs. If the Expression does not have a projection
// expression this method returns nil.
//
// Example:
//
// // let expression be an instance of Expression{}
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// ProjectionExpression: expression.Projection(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
func (e Expression) Projection() *string {
return e.returnExpression(projection)
}
// KeyCondition returns the *string corresponding to the Key Condition
// Expression of the argument Expression. This method is used to satisfy the
// members of DynamoDB input structs. If the argument Expression does not have a
// KeyConditionExpression, KeyCondition() returns nil.
//
// Example:
//
// // let expression be an instance of Expression{}
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// ProjectionExpression: expression.Projection(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
func (e Expression) KeyCondition() *string {
return e.returnExpression(keyCondition)
}
// Update returns the *string corresponding to the Update Expression of the
// argument Expression. This method is used to satisfy the members of DynamoDB
// input structs. If the argument Expression does not have a UpdateExpression,
// Update() returns nil.
//
// Example:
//
// // let expression be an instance of Expression{}
//
// updateInput := dynamodb.UpdateInput{
// Key: map[string]*dynamodb.AttributeValue{
// "PartitionKey": {
// S: aws.String("someKey"),
// },
// },
// UpdateExpression: expression.Update(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
func (e Expression) Update() *string {
return e.returnExpression(update)
}
// Names returns the map[string]*string corresponding to the
// ExpressionAttributeNames of the argument Expression. This method is used to
// satisfy the members of DynamoDB input structs. If Expression does not use
// ExpressionAttributeNames, this method returns nil. The
// ExpressionAttributeNames and ExpressionAttributeValues member of the input
// struct must always be assigned when using the Expression struct since all
// item attribute names and values are aliased. That means that if the
// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned
// with the corresponding Names() and Values() methods, the DynamoDB operation
// will run into a logic error.
//
// Example:
//
// // let expression be an instance of Expression{}
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// ProjectionExpression: expression.Projection(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
func (e Expression) Names() map[string]*string {
return e.namesMap
}
// Values returns the map[string]*dynamodb.AttributeValue corresponding to
// the ExpressionAttributeValues of the argument Expression. This method is used
// to satisfy the members of DynamoDB input structs. If Expression does not use
// ExpressionAttributeValues, this method returns nil. The
// ExpressionAttributeNames and ExpressionAttributeValues member of the input
// struct must always be assigned when using the Expression struct since all
// item attribute names and values are aliased. That means that if the
// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned
// with the corresponding Names() and Values() methods, the DynamoDB operation
// will run into a logic error.
//
// Example:
//
// // let expression be an instance of Expression{}
//
// queryInput := dynamodb.QueryInput{
// KeyConditionExpression: expression.KeyCondition(),
// ProjectionExpression: expression.Projection(),
// ExpressionAttributeNames: expression.Names(),
// ExpressionAttributeValues: expression.Values(),
// TableName: aws.String("SomeTable"),
// }
func (e Expression) Values() map[string]*dynamodb.AttributeValue {
return e.valuesMap
}
// returnExpression returns *string corresponding to the type of Expression
// string specified by the expressionType. If there is no corresponding
// expression available in Expression, the method returns nil
func (e Expression) returnExpression(expressionType expressionType) *string {
if e.expressionMap == nil {
return nil
}
return aws.String(e.expressionMap[expressionType])
}
// exprNode are the generic nodes that represents both Operands and
// Conditions. The purpose of exprNode is to be able to call an generic
// recursive function on the top level exprNode to be able to determine a root
// node in order to deduplicate name aliases.
// fmtExpr is a string that has escaped characters to refer to
// names/values/children which needs to be aliased at runtime in order to avoid
// duplicate values. The rules are as follows:
// $n: Indicates that an alias of a name needs to be inserted. The
// corresponding name to be alias is in the []names slice.
// $v: Indicates that an alias of a value needs to be inserted. The
// corresponding value to be alias is in the []values slice.
// $c: Indicates that the fmtExpr of a child exprNode needs to be inserted.
// The corresponding child node is in the []children slice.
type exprNode struct {
names []string
values []dynamodb.AttributeValue
children []exprNode
fmtExpr string
}
// aliasList keeps track of all the names we need to alias in the nested
// struct of conditions and operands. This allows each alias to be unique.
// aliasList is passed in as a pointer when buildChildTrees is called in
// order to deduplicate all names within the tree strcuture of the exprNodes.
type aliasList struct {
namesList []string
valuesList []dynamodb.AttributeValue
}
// buildExpressionString returns a string with aliasing for names/values
// specified by aliasList. The string corresponds to the expression that the
// exprNode tree represents.
func (en exprNode) buildExpressionString(aliasList *aliasList) (string, error) {
// Since each exprNode contains a slice of names, values, and children that
// correspond to the escaped characters, we an index to traverse the slices
index := struct {
name, value, children int
}{}
formattedExpression := en.fmtExpr
for i := 0; i < len(formattedExpression); {
if formattedExpression[i] != '$' {
i++
continue
}
if i == len(formattedExpression)-1 {
return "", fmt.Errorf("buildexprNode error: invalid escape character")
}
var alias string
var err error
// if an escaped character is found, substitute it with the proper alias
// TODO consider AST instead of string in the future
switch formattedExpression[i+1] {
case 'n':
alias, err = substitutePath(index.name, en, aliasList)
if err != nil {
return "", err
}
index.name++
case 'v':
alias, err = substituteValue(index.value, en, aliasList)
if err != nil {
return "", err
}
index.value++
case 'c':
alias, err = substituteChild(index.children, en, aliasList)
if err != nil {
return "", err
}
index.children++
default:
return "", fmt.Errorf("buildexprNode error: invalid escape rune %#v", formattedExpression[i+1])
}
formattedExpression = formattedExpression[:i] + alias + formattedExpression[i+2:]
i += len(alias)
}
return formattedExpression, nil
}
// substitutePath substitutes the escaped character $n with the appropriate
// alias.
func substitutePath(index int, node exprNode, aliasList *aliasList) (string, error) {
if index >= len(node.names) {
return "", fmt.Errorf("substitutePath error: exprNode []names out of range")
}
str, err := aliasList.aliasPath(node.names[index])
if err != nil {
return "", err
}
return str, nil
}
// substituteValue substitutes the escaped character $v with the appropriate
// alias.
func substituteValue(index int, node exprNode, aliasList *aliasList) (string, error) {
if index >= len(node.values) {
return "", fmt.Errorf("substituteValue error: exprNode []values out of range")
}
str, err := aliasList.aliasValue(node.values[index])
if err != nil {
return "", err
}
return str, nil
}
// substituteChild substitutes the escaped character $c with the appropriate
// alias.
func substituteChild(index int, node exprNode, aliasList *aliasList) (string, error) {
if index >= len(node.children) {
return "", fmt.Errorf("substituteChild error: exprNode []children out of range")
}
return node.children[index].buildExpressionString(aliasList)
}
// aliasValue returns the corresponding alias to the dav value argument. Since
// values are not deduplicated as of now, all values are just appended to the
// aliasList and given the index as the alias.
func (al *aliasList) aliasValue(dav dynamodb.AttributeValue) (string, error) {
al.valuesList = append(al.valuesList, dav)
return fmt.Sprintf(":%d", len(al.valuesList)-1), nil
}
// aliasPath returns the corresponding alias to the argument string. The
// argument is checked against all existing aliasList names in order to avoid
// duplicate strings getting two different aliases.
func (al *aliasList) aliasPath(nm string) (string, error) {
for ind, name := range al.namesList {
if nm == name {
return fmt.Sprintf("#%d", ind), nil
}
}
al.namesList = append(al.namesList, nm)
return fmt.Sprintf("#%d", len(al.namesList)-1), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,557 @@
package expression
import (
"fmt"
)
// keyConditionMode specifies the types of the struct KeyConditionBuilder,
// representing the different types of KeyConditions (i.e. And, Or, Between, ...)
type keyConditionMode int
const (
// unsetKeyCond catches errors for unset KeyConditionBuilder structs
unsetKeyCond keyConditionMode = iota
// equalKeyCond represents the Equals KeyCondition
equalKeyCond
// lessThanKeyCond represents the Less Than KeyCondition
lessThanKeyCond
// lessThanEqualKeyCond represents the Less Than Or Equal To KeyCondition
lessThanEqualKeyCond
// greaterThanKeyCond represents the Greater Than KeyCondition
greaterThanKeyCond
// greaterThanEqualKeyCond represents the Greater Than Or Equal To KeyCondition
greaterThanEqualKeyCond
// andKeyCond represents the Logical And KeyCondition
andKeyCond
// betweenKeyCond represents the Between KeyCondition
betweenKeyCond
// beginsWithKeyCond represents the Begins With KeyCondition
beginsWithKeyCond
)
// KeyConditionBuilder represents Key Condition Expressions in DynamoDB.
// KeyConditionBuilders are the building blocks of Expressions.
// More Information at: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.KeyConditionExpressions
type KeyConditionBuilder struct {
operandList []OperandBuilder
keyConditionList []KeyConditionBuilder
mode keyConditionMode
}
// KeyEqual returns a KeyConditionBuilder representing the equality clause
// of the two argument OperandBuilders. The resulting KeyConditionBuilder can be
// used as a part of other Key Condition Expressions or as an argument to the
// WithKeyCondition() method for the Builder struct.
//
// Example:
//
// // keyCondition represents the equal clause of the key "foo" and the
// // value 5
// keyCondition := expression.KeyEqual(expression.Key("foo"), expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
// // Used to make an Builder
// builder := expression.NewBuilder().WithKeyCondition(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyEqual(expression.Key("foo"), expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo = :five"
func KeyEqual(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyConditionBuilder{
operandList: []OperandBuilder{keyBuilder, valueBuilder},
mode: equalKeyCond,
}
}
// Equal returns a KeyConditionBuilder representing the equality clause of
// the two argument OperandBuilders. The resulting KeyConditionBuilder can be
// used as a part of other Key Condition Expressions or as an argument to the
// WithKeyCondition() method for the Builder struct.
//
// Example:
//
// // keyCondition represents the equal clause of the key "foo" and the
// // value 5
// keyCondition := expression.Key("foo").Equal(expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
// // Used to make an Builder
// builder := expression.NewBuilder().WithKeyCondition(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("foo").Equal(expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo = :five"
func (kb KeyBuilder) Equal(valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyEqual(kb, valueBuilder)
}
// KeyLessThan returns a KeyConditionBuilder representing the less than
// clause of the two argument OperandBuilders. The resulting KeyConditionBuilder
// can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the less than clause of the key "foo" and the
// // value 5
// keyCondition := expression.KeyLessThan(expression.Key("foo"), expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyLessThan(expression.Key("foo"), expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo < :five"
func KeyLessThan(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyConditionBuilder{
operandList: []OperandBuilder{keyBuilder, valueBuilder},
mode: lessThanKeyCond,
}
}
// LessThan returns a KeyConditionBuilder representing the less than clause
// of the two argument OperandBuilders. The resulting KeyConditionBuilder can be
// used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the less than clause of the key "foo" and the
// // value 5
// keyCondition := expression.Key("foo").LessThan(expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("foo").LessThan(expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo < :five"
func (kb KeyBuilder) LessThan(valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyLessThan(kb, valueBuilder)
}
// KeyLessThanEqual returns a KeyConditionBuilder representing the less than
// equal to clause of the two argument OperandBuilders. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the less than equal to clause of the key
// // "foo" and the value 5
// keyCondition := expression.KeyLessThanEqual(expression.Key("foo"), expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyLessThanEqual(expression.Key("foo"), expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo <= :five"
func KeyLessThanEqual(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyConditionBuilder{
operandList: []OperandBuilder{keyBuilder, valueBuilder},
mode: lessThanEqualKeyCond,
}
}
// LessThanEqual returns a KeyConditionBuilder representing the less than
// equal to clause of the two argument OperandBuilders. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the less than equal to clause of the key
// // "foo" and the value 5
// keyCondition := expression.Key("foo").LessThanEqual(expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("foo").LessThanEqual(expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo <= :five"
func (kb KeyBuilder) LessThanEqual(valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyLessThanEqual(kb, valueBuilder)
}
// KeyGreaterThan returns a KeyConditionBuilder representing the greater
// than clause of the two argument OperandBuilders. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the greater than clause of the key "foo" and
// // the value 5
// keyCondition := expression.KeyGreaterThan(expression.Key("foo"), expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyGreaterThan(expression.Key("foo"), expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo > :five"
func KeyGreaterThan(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyConditionBuilder{
operandList: []OperandBuilder{keyBuilder, valueBuilder},
mode: greaterThanKeyCond,
}
}
// GreaterThan returns a KeyConditionBuilder representing the greater than
// clause of the two argument OperandBuilders. The resulting KeyConditionBuilder
// can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // key condition represents the greater than clause of the key "foo" and
// // the value 5
// keyCondition := expression.Key("foo").GreaterThan(expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("foo").GreaterThan(expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo > :five"
func (kb KeyBuilder) GreaterThan(valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyGreaterThan(kb, valueBuilder)
}
// KeyGreaterThanEqual returns a KeyConditionBuilder representing the
// greater than equal to clause of the two argument OperandBuilders. The
// resulting KeyConditionBuilder can be used as a part of other Key Condition
// Expressions.
//
// Example:
//
// // keyCondition represents the greater than equal to clause of the key
// // "foo" and the value 5
// keyCondition := expression.KeyGreaterThanEqual(expression.Key("foo"), expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyGreaterThanEqual(expression.Key("foo"), expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo >= :five"
func KeyGreaterThanEqual(keyBuilder KeyBuilder, valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyConditionBuilder{
operandList: []OperandBuilder{keyBuilder, valueBuilder},
mode: greaterThanEqualKeyCond,
}
}
// GreaterThanEqual returns a KeyConditionBuilder representing the greater
// than equal to clause of the two argument OperandBuilders. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the greater than equal to clause of the key
// // "foo" and the value 5
// keyCondition := expression.Key("foo").GreaterThanEqual(expression.Value(5))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("foo").GreaterThanEqual(expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "foo >= :five"
func (kb KeyBuilder) GreaterThanEqual(valueBuilder ValueBuilder) KeyConditionBuilder {
return KeyGreaterThanEqual(kb, valueBuilder)
}
// KeyAnd returns a KeyConditionBuilder representing the logical AND clause
// of the two argument KeyConditionBuilders. The resulting KeyConditionBuilder
// can be used as an argument to the WithKeyCondition() method for the Builder
// struct.
//
// Example:
//
// // keyCondition represents the key condition where the partition key
// // "TeamName" is equal to value "Wildcats" and sort key "Number" is equal
// // to value 1
// keyCondition := expression.KeyAnd(expression.Key("TeamName").Equal(expression.Value("Wildcats")), expression.Key("Number").Equal(expression.Value(1)))
//
// // Used to make an Builder
// builder := expression.NewBuilder().WithKeyCondition(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyAnd(expression.Key("TeamName").Equal(expression.Value("Wildcats")), expression.Key("Number").Equal(expression.Value(1)))
// // Let #NUMBER, :teamName, and :one be ExpressionAttributeName and
// // ExpressionAttributeValues representing the item attribute "Number",
// // the value "Wildcats", and the value 1
// "(TeamName = :teamName) AND (#NUMBER = :one)"
func KeyAnd(left, right KeyConditionBuilder) KeyConditionBuilder {
if left.mode != equalKeyCond {
return KeyConditionBuilder{
mode: andKeyCond,
}
}
if right.mode == andKeyCond {
return KeyConditionBuilder{
mode: andKeyCond,
}
}
return KeyConditionBuilder{
keyConditionList: []KeyConditionBuilder{left, right},
mode: andKeyCond,
}
}
// And returns a KeyConditionBuilder representing the logical AND clause of
// the two argument KeyConditionBuilders. The resulting KeyConditionBuilder can
// be used as an argument to the WithKeyCondition() method for the Builder
// struct.
//
// Example:
//
// // keyCondition represents the key condition where the partition key
// // "TeamName" is equal to value "Wildcats" and sort key "Number" is equal
// // to value 1
// keyCondition := expression.Key("TeamName").Equal(expression.Value("Wildcats")).And(expression.Key("Number").Equal(expression.Value(1)))
//
// // Used to make an Builder
// builder := expression.NewBuilder().WithKeyCondition(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("TeamName").Equal(expression.Value("Wildcats")).And(expression.Key("Number").Equal(expression.Value(1)))
// // Let #NUMBER, :teamName, and :one be ExpressionAttributeName and
// // ExpressionAttributeValues representing the item attribute "Number",
// // the value "Wildcats", and the value 1
// "(TeamName = :teamName) AND (#NUMBER = :one)"
func (kcb KeyConditionBuilder) And(right KeyConditionBuilder) KeyConditionBuilder {
return KeyAnd(kcb, right)
}
// KeyBetween returns a KeyConditionBuilder representing the result of the
// BETWEEN function in DynamoDB Key Condition Expressions. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the boolean key condition of whether the value
// // of the key "foo" is between values 5 and 10
// keyCondition := expression.KeyBetween(expression.Key("foo"), expression.Value(5), expression.Value(10))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyBetween(expression.Key("foo"), expression.Value(5), expression.Value(10))
// // Let :five and :ten be ExpressionAttributeValues representing the
// // values 5 and 10 respectively
// "foo BETWEEN :five AND :ten"
func KeyBetween(keyBuilder KeyBuilder, lower, upper ValueBuilder) KeyConditionBuilder {
return KeyConditionBuilder{
operandList: []OperandBuilder{keyBuilder, lower, upper},
mode: betweenKeyCond,
}
}
// Between returns a KeyConditionBuilder representing the result of the
// BETWEEN function in DynamoDB Key Condition Expressions. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the boolean key condition of whether the value
// // of the key "foo" is between values 5 and 10
// keyCondition := expression.Key("foo").Between(expression.Value(5), expression.Value(10))
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("foo").Between(expression.Value(5), expression.Value(10))
// // Let :five and :ten be ExpressionAttributeValues representing the
// // values 5 and 10 respectively
// "foo BETWEEN :five AND :ten"
func (kb KeyBuilder) Between(lower, upper ValueBuilder) KeyConditionBuilder {
return KeyBetween(kb, lower, upper)
}
// KeyBeginsWith returns a KeyConditionBuilder representing the result of
// the begins_with function in DynamoDB Key Condition Expressions. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the boolean key condition of whether the value
// // of the key "foo" is begins with the prefix "bar"
// keyCondition := expression.KeyBeginsWith(expression.Key("foo"), "bar")
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.KeyBeginsWith(expression.Key("foo"), "bar")
// // Let :bar be an ExpressionAttributeValue representing the value "bar"
// "begins_with(foo, :bar)"
func KeyBeginsWith(keyBuilder KeyBuilder, prefix string) KeyConditionBuilder {
valueBuilder := ValueBuilder{
value: prefix,
}
return KeyConditionBuilder{
operandList: []OperandBuilder{keyBuilder, valueBuilder},
mode: beginsWithKeyCond,
}
}
// BeginsWith returns a KeyConditionBuilder representing the result of the
// begins_with function in DynamoDB Key Condition Expressions. The resulting
// KeyConditionBuilder can be used as a part of other Key Condition Expressions.
//
// Example:
//
// // keyCondition represents the boolean key condition of whether the value
// // of the key "foo" is begins with the prefix "bar"
// keyCondition := expression.Key("foo").BeginsWith("bar")
//
// // Used in another Key Condition Expression
// anotherKeyCondition := expression.Key("partitionKey").Equal(expression.Value("aValue")).And(keyCondition)
//
// Expression Equivalent:
//
// expression.Key("foo").BeginsWith("bar")
// // Let :bar be an ExpressionAttributeValue representing the value "bar"
// "begins_with(foo, :bar)"
func (kb KeyBuilder) BeginsWith(prefix string) KeyConditionBuilder {
return KeyBeginsWith(kb, prefix)
}
// buildTree builds a tree structure of exprNodes based on the tree
// structure of the input KeyConditionBuilder's child KeyConditions/Operands.
// buildTree() satisfies the treeBuilder interface so KeyConditionBuilder can be
// a part of Expression struct.
func (kcb KeyConditionBuilder) buildTree() (exprNode, error) {
childNodes, err := kcb.buildChildNodes()
if err != nil {
return exprNode{}, err
}
ret := exprNode{
children: childNodes,
}
switch kcb.mode {
case equalKeyCond, lessThanKeyCond, lessThanEqualKeyCond, greaterThanKeyCond, greaterThanEqualKeyCond:
return compareBuildKeyCondition(kcb.mode, ret)
case andKeyCond:
return andBuildKeyCondition(kcb, ret)
case betweenKeyCond:
return betweenBuildKeyCondition(ret)
case beginsWithKeyCond:
return beginsWithBuildKeyCondition(ret)
case unsetKeyCond:
return exprNode{}, newUnsetParameterError("buildTree", "KeyConditionBuilder")
default:
return exprNode{}, fmt.Errorf("buildKeyCondition error: unsupported mode: %v", kcb.mode)
}
}
// compareBuildKeyCondition is the function to make exprNodes from Compare
// KeyConditionBuilders. compareBuildKeyCondition is only called by the
// buildKeyCondition method. This function assumes that the argument
// KeyConditionBuilder has the right format.
func compareBuildKeyCondition(keyConditionMode keyConditionMode, node exprNode) (exprNode, error) {
// Create a string with special characters that can be substituted later: $c
switch keyConditionMode {
case equalKeyCond:
node.fmtExpr = "$c = $c"
case lessThanKeyCond:
node.fmtExpr = "$c < $c"
case lessThanEqualKeyCond:
node.fmtExpr = "$c <= $c"
case greaterThanKeyCond:
node.fmtExpr = "$c > $c"
case greaterThanEqualKeyCond:
node.fmtExpr = "$c >= $c"
default:
return exprNode{}, fmt.Errorf("build compare key condition error: unsupported mode: %v", keyConditionMode)
}
return node, nil
}
// andBuildKeyCondition is the function to make exprNodes from And
// KeyConditionBuilders. andBuildKeyCondition is only called by the
// buildKeyCondition method. This function assumes that the argument
// KeyConditionBuilder has the right format.
func andBuildKeyCondition(keyConditionBuilder KeyConditionBuilder, node exprNode) (exprNode, error) {
if len(keyConditionBuilder.keyConditionList) == 0 && len(keyConditionBuilder.operandList) == 0 {
return exprNode{}, newInvalidParameterError("andBuildKeyCondition", "KeyConditionBuilder")
}
// create a string with escaped characters to substitute them with proper
// aliases during runtime
node.fmtExpr = "($c) AND ($c)"
return node, nil
}
// betweenBuildKeyCondition is the function to make exprNodes from Between
// KeyConditionBuilders. betweenBuildKeyCondition is only called by the
// buildKeyCondition method. This function assumes that the argument
// KeyConditionBuilder has the right format.
func betweenBuildKeyCondition(node exprNode) (exprNode, error) {
// Create a string with special characters that can be substituted later: $c
node.fmtExpr = "$c BETWEEN $c AND $c"
return node, nil
}
// beginsWithBuildKeyCondition is the function to make exprNodes from
// BeginsWith KeyConditionBuilders. beginsWithBuildKeyCondition is only
// called by the buildKeyCondition method. This function assumes that the argument
// KeyConditionBuilder has the right format.
func beginsWithBuildKeyCondition(node exprNode) (exprNode, error) {
// Create a string with special characters that can be substituted later: $c
node.fmtExpr = "begins_with ($c, $c)"
return node, nil
}
// buildChildNodes creates the list of the child exprNodes. This avoids
// duplication of code amongst the various buildConditions.
func (kcb KeyConditionBuilder) buildChildNodes() ([]exprNode, error) {
childNodes := make([]exprNode, 0, len(kcb.keyConditionList)+len(kcb.operandList))
for _, keyCondition := range kcb.keyConditionList {
node, err := keyCondition.buildTree()
if err != nil {
return []exprNode{}, err
}
childNodes = append(childNodes, node)
}
for _, operand := range kcb.operandList {
ope, err := operand.BuildOperand()
if err != nil {
return []exprNode{}, err
}
childNodes = append(childNodes, ope.exprNode)
}
return childNodes, nil
}

View File

@@ -0,0 +1,446 @@
// +build go1.7
package expression
import (
"reflect"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// keyCondErrorMode will help with error cases and checking error types
type keyCondErrorMode string
const (
noKeyConditionError keyCondErrorMode = ""
// unsetKeyCondition error will occur when buildTree() is called on an empty
// KeyConditionBuilder
unsetKeyCondition = "unset parameter: KeyConditionBuilder"
// invalidKeyConditionOperand error will occur when an invalid OperandBuilder is used as
// an argument
invalidKeyConditionOperand = "BuildOperand error"
// invalidAndFormat error will occur when the first key condition is not an equal
// clause or if the second key condition is an and condition.
invalidAndFormat = "invalid parameter: KeyConditionBuilder"
)
func TestKeyCompare(t *testing.T) {
cases := []struct {
name string
input KeyConditionBuilder
expectedNode exprNode
err keyCondErrorMode
}{
{
name: "key equal",
input: Key("foo").Equal(Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c = $c",
},
},
{
name: "key less than",
input: Key("foo").LessThan(Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c < $c",
},
},
{
name: "key less than equal",
input: Key("foo").LessThanEqual(Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c <= $c",
},
},
{
name: "key greater than",
input: Key("foo").GreaterThan(Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c > $c",
},
},
{
name: "key greater than equal",
input: Key("foo").GreaterThanEqual(Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c >= $c",
},
},
{
name: "unset KeyConditionBuilder",
input: KeyConditionBuilder{},
err: unsetKeyCondition,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noKeyConditionError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestKeyBetween(t *testing.T) {
cases := []struct {
name string
input KeyConditionBuilder
expectedNode exprNode
err keyCondErrorMode
}{
{
name: "key between",
input: Key("foo").Between(Value(5), Value(10)),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("10"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c BETWEEN $c AND $c",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noKeyConditionError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestKeyBeginsWith(t *testing.T) {
cases := []struct {
name string
input KeyConditionBuilder
expectedNode exprNode
err keyCondErrorMode
}{
{
name: "key begins with",
input: Key("foo").BeginsWith("bar"),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
S: aws.String("bar"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "begins_with ($c, $c)",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noKeyConditionError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestKeyAnd(t *testing.T) {
cases := []struct {
name string
input KeyConditionBuilder
expectedNode exprNode
err keyCondErrorMode
}{
{
name: "key and",
input: Key("foo").Equal(Value(5)).And(Key("bar").BeginsWith("baz")),
expectedNode: exprNode{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c = $c",
},
{
children: []exprNode{
{
names: []string{"bar"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
S: aws.String("baz"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "begins_with ($c, $c)",
},
},
fmtExpr: "($c) AND ($c)",
},
},
{
name: "first condition is not equal",
input: Key("foo").LessThan(Value(5)).And(Key("bar").BeginsWith("baz")),
err: invalidAndFormat,
},
{
name: "second condition is and",
input: Key("foo").Equal(Value(5)).And(Key("bar").Equal(Value(1)).And(Key("baz").BeginsWith("yar"))),
err: invalidAndFormat,
},
{
name: "operand error",
input: Key("").Equal(Value("yikes")),
err: invalidKeyConditionOperand,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noKeyConditionError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestKeyConditionBuildChildNodes(t *testing.T) {
cases := []struct {
name string
input KeyConditionBuilder
expected []exprNode
err keyCondErrorMode
}{
{
name: "build child nodes",
input: Key("foo").Equal(Value("bar")).And(Key("baz").LessThan(Value(10))),
expected: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
S: aws.String("bar"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c = $c",
},
{
children: []exprNode{
{
names: []string{"baz"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("10"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c < $c",
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildChildNodes()
if c.err != noKeyConditionError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expected, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %#v, got %#v", e, a)
}
}
})
}
}

View File

@@ -0,0 +1,620 @@
package expression
import (
"fmt"
"strings"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
// ValueBuilder represents an item attribute value operand and implements the
// OperandBuilder interface. Methods and functions in the package take
// ValueBuilder as an argument and establishes relationships between operands.
// ValueBuilder should only be initialized using the function Value().
//
// Example:
//
// // Create a ValueBuilder representing the string "aValue"
// valueBuilder := expression.Value("aValue")
type ValueBuilder struct {
value interface{}
}
// NameBuilder represents a name of a top level item attribute or a nested
// attribute. Since NameBuilder represents a DynamoDB Operand, it implements the
// OperandBuilder interface. Methods and functions in the package take
// NameBuilder as an argument and establishes relationships between operands.
// NameBuilder should only be initialized using the function Name().
//
// Example:
//
// // Create a NameBuilder representing the item attribute "aName"
// nameBuilder := expression.Name("aName")
type NameBuilder struct {
name string
}
// SizeBuilder represents the output of the function size ("someName"), which
// evaluates to the size of the item attribute defined by "someName". Since
// SizeBuilder represents an operand, SizeBuilder implements the OperandBuilder
// interface. Methods and functions in the package take SizeBuilder as an
// argument and establishes relationships between operands. SizeBuilder should
// only be initialized using the function Size().
//
// Example:
//
// // Create a SizeBuilder representing the size of the item attribute
// // "aName"
// sizeBuilder := expression.Name("aName").Size()
type SizeBuilder struct {
nameBuilder NameBuilder
}
// KeyBuilder represents either the partition key or the sort key, both of which
// are top level attributes to some item in DynamoDB. Since KeyBuilder
// represents an operand, KeyBuilder implements the OperandBuilder interface.
// Methods and functions in the package take KeyBuilder as an argument and
// establishes relationships between operands. However, KeyBuilder should only
// be used to describe Key Condition Expressions. KeyBuilder should only be
// initialized using the function Key().
//
// Example:
//
// // Create a KeyBuilder representing the item key "aKey"
// keyBuilder := expression.Key("aKey")
type KeyBuilder struct {
key string
}
// setValueMode specifies the type of SetValueBuilder. The default value is
// unsetValue so that an UnsetParameterError when BuildOperand() is called on an
// empty SetValueBuilder.
type setValueMode int
const (
unsetValue setValueMode = iota
plusValueMode
minusValueMode
listAppendValueMode
ifNotExistsValueMode
)
// SetValueBuilder represents the outcome of operator functions supported by the
// DynamoDB Set operation. The operator functions are the following:
// Plus() // Represents the "+" operator
// Minus() // Represents the "-" operator
// ListAppend()
// IfNotExists()
// For documentation on the above functions,
// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET
// Since SetValueBuilder represents an operand, it implements the OperandBuilder
// interface. SetValueBuilder structs are used as arguments to the Set()
// function. SetValueBuilders should only initialize a SetValueBuilder using the
// functions listed above.
type SetValueBuilder struct {
leftOperand OperandBuilder
rightOperand OperandBuilder
mode setValueMode
}
// Operand represents an item attribute name or value in DynamoDB. The
// relationship between Operands specified by various builders such as
// ConditionBuilders and UpdateBuilders for example is processed internally to
// write Condition Expressions and Update Expressions respectively.
type Operand struct {
exprNode exprNode
}
// OperandBuilder represents the idea of Operand which are building blocks to
// DynamoDB Expressions. Package methods and functions can establish
// relationships between operands, representing DynamoDB Expressions. The method
// BuildOperand() is called recursively when the Build() method on the type
// Builder is called. BuildOperand() should never be called externally.
// OperandBuilder and BuildOperand() are exported to allow package functions to
// take an interface as an argument.
type OperandBuilder interface {
BuildOperand() (Operand, error)
}
// Name creates a NameBuilder. The argument should represent the desired item
// attribute. It is possible to reference nested item attributes by using
// square brackets for lists and dots for maps. For documentation on specifying
// item attributes,
// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Attributes.html
//
// Example:
//
// // Specify a top-level attribute
// name := expression.Name("TopLevel")
// // Specify a nested attribute
// nested := expression.Name("Record[6].SongList")
// // Use Name() to create a condition expression
// condition := expression.Name("foo").Equal(expression.Name("bar"))
func Name(name string) NameBuilder {
return NameBuilder{
name: name,
}
}
// Value creates a ValueBuilder. The argument should represent the desired item
// attribute. The value is marshalled using the dynamodbattribute package by the
// Build() method for type Builder.
//
// Example:
//
// // Use Value() to create a condition expression
// condition := expression.Name("foo").Equal(expression.Value(10))
func Value(value interface{}) ValueBuilder {
return ValueBuilder{
value: value,
}
}
// Size creates a SizeBuilder representing the size of the item attribute
// specified by the argument NameBuilder. Size() is only valid for certain types
// of item attributes. For documentation,
// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
// SizeBuilder is only a valid operand in Condition Expressions and Filter
// Expressions.
//
// Example:
//
// // Use Size() to create a condition expression
// condition := expression.Name("foo").Size().Equal(expression.Value(10))
//
// Expression Equivalent:
//
// expression.Name("aName").Size()
// "size (aName)"
func (nb NameBuilder) Size() SizeBuilder {
return SizeBuilder{
nameBuilder: nb,
}
}
// Size creates a SizeBuilder representing the size of the item attribute
// specified by the argument NameBuilder. Size() is only valid for certain types
// of item attributes. For documentation,
// see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
// SizeBuilder is only a valid operand in Condition Expressions and Filter
// Expressions.
//
// Example:
//
// // Use Size() to create a condition expression
// condition := expression.Size(expression.Name("foo")).Equal(expression.Value(10))
//
// Expression Equivalent:
//
// expression.Size(expression.Name("aName"))
// "size (aName)"
func Size(nameBuilder NameBuilder) SizeBuilder {
return nameBuilder.Size()
}
// Key creates a KeyBuilder. The argument should represent the desired partition
// key or sort key value. KeyBuilders should only be used to specify
// relationships for Key Condition Expressions. When referring to the partition
// key or sort key in any other Expression, use Name().
//
// Example:
//
// // Use Key() to create a key condition expression
// keyCondition := expression.Key("foo").Equal(expression.Value("bar"))
func Key(key string) KeyBuilder {
return KeyBuilder{
key: key,
}
}
// Plus creates a SetValueBuilder to be used in as an argument to Set(). The
// arguments can either be NameBuilders or ValueBuilders. Plus() only supports
// DynamoDB Number types, so the ValueBuilder must be a Number and the
// NameBuilder must specify an item attribute of type Number.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
//
// Example:
//
// // Use Plus() to set the value of the item attribute "someName" to 5 + 10
// update, err := expression.Set(expression.Name("someName"), expression.Plus(expression.Value(5), expression.Value(10)))
//
// Expression Equivalent:
//
// expression.Plus(expression.Value(5), expression.Value(10))
// // let :five and :ten be ExpressionAttributeValues for the values 5 and
// // 10 respectively.
// ":five + :ten"
func Plus(leftOperand, rightOperand OperandBuilder) SetValueBuilder {
return SetValueBuilder{
leftOperand: leftOperand,
rightOperand: rightOperand,
mode: plusValueMode,
}
}
// Plus creates a SetValueBuilder to be used in as an argument to Set(). The
// arguments can either be NameBuilders or ValueBuilders. Plus() only supports
// DynamoDB Number types, so the ValueBuilder must be a Number and the
// NameBuilder must specify an item attribute of type Number.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
//
// Example:
//
// // Use Plus() to set the value of the item attribute "someName" to the
// // numeric value of item attribute "aName" incremented by 10
// update, err := expression.Set(expression.Name("someName"), expression.Name("aName").Plus(expression.Value(10)))
//
// Expression Equivalent:
//
// expression.Name("aName").Plus(expression.Value(10))
// // let :ten be ExpressionAttributeValues representing the value 10
// "aName + :ten"
func (nb NameBuilder) Plus(rightOperand OperandBuilder) SetValueBuilder {
return Plus(nb, rightOperand)
}
// Plus creates a SetValueBuilder to be used in as an argument to Set(). The
// arguments can either be NameBuilders or ValueBuilders. Plus() only supports
// DynamoDB Number types, so the ValueBuilder must be a Number and the
// NameBuilder must specify an item attribute of type Number.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
//
// Example:
//
// // Use Plus() to set the value of the item attribute "someName" to 5 + 10
// update, err := expression.Set(expression.Name("someName"), expression.Value(5).Plus(expression.Value(10)))
//
// Expression Equivalent:
//
// expression.Value(5).Plus(expression.Value(10))
// // let :five and :ten be ExpressionAttributeValues representing the value
// // 5 and 10 respectively
// ":five + :ten"
func (vb ValueBuilder) Plus(rightOperand OperandBuilder) SetValueBuilder {
return Plus(vb, rightOperand)
}
// Minus creates a SetValueBuilder to be used in as an argument to Set(). The
// arguments can either be NameBuilders or ValueBuilders. Minus() only supports
// DynamoDB Number types, so the ValueBuilder must be a Number and the
// NameBuilder must specify an item attribute of type Number.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
//
// Example:
//
// // Use Minus() to set the value of item attribute "someName" to 5 - 10
// update, err := expression.Set(expression.Name("someName"), expression.Minus(expression.Value(5), expression.Value(10)))
//
// Expression Equivalent:
//
// expression.Minus(expression.Value(5), expression.Value(10))
// // let :five and :ten be ExpressionAttributeValues for the values 5 and
// // 10 respectively.
// ":five - :ten"
func Minus(leftOperand, rightOperand OperandBuilder) SetValueBuilder {
return SetValueBuilder{
leftOperand: leftOperand,
rightOperand: rightOperand,
mode: minusValueMode,
}
}
// Minus creates a SetValueBuilder to be used in as an argument to Set(). The
// arguments can either be NameBuilders or ValueBuilders. Minus() only supports
// DynamoDB Number types, so the ValueBuilder must be a Number and the
// NameBuilder must specify an item attribute of type Number.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
//
// Example:
//
// // Use Minus() to set the value of item attribute "someName" to the
// // numeric value of "aName" decremented by 10
// update, err := expression.Set(expression.Name("someName"), expression.Name("aName").Minus(expression.Value(10)))
//
// Expression Equivalent:
//
// expression.Name("aName").Minus(expression.Value(10)))
// // let :ten be ExpressionAttributeValues represent the value 10
// "aName - :ten"
func (nb NameBuilder) Minus(rightOperand OperandBuilder) SetValueBuilder {
return Minus(nb, rightOperand)
}
// Minus creates a SetValueBuilder to be used in as an argument to Set(). The
// arguments can either be NameBuilders or ValueBuilders. Minus() only supports
// DynamoDB Number types, so the ValueBuilder must be a Number and the
// NameBuilder must specify an item attribute of type Number.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.IncrementAndDecrement
//
// Example:
//
// // Use Minus() to set the value of item attribute "someName" to 5 - 10
// update, err := expression.Set(expression.Name("someName"), expression.Value(5).Minus(expression.Value(10)))
//
// Expression Equivalent:
//
// expression.Value(5).Minus(expression.Value(10))
// // let :five and :ten be ExpressionAttributeValues for the values 5 and
// // 10 respectively.
// ":five - :ten"
func (vb ValueBuilder) Minus(rightOperand OperandBuilder) SetValueBuilder {
return Minus(vb, rightOperand)
}
// ListAppend creates a SetValueBuilder to be used in as an argument to Set().
// The arguments can either be NameBuilders or ValueBuilders. ListAppend() only
// supports DynamoDB List types, so the ValueBuilder must be a List and the
// NameBuilder must specify an item attribute of type List.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.UpdatingListElements
//
// Example:
//
// // Use ListAppend() to set item attribute "someName" to the item
// // attribute "nameOfList" with "some" and "list" appended to it
// update, err := expression.Set(expression.Name("someName"), expression.ListAppend(expression.Name("nameOfList"), expression.Value([]string{"some", "list"})))
//
// Expression Equivalent:
//
// expression.ListAppend(expression.Name("nameOfList"), expression.Value([]string{"some", "list"})
// // let :list be a ExpressionAttributeValue representing the list
// // containing "some" and "list".
// "list_append (nameOfList, :list)"
func ListAppend(leftOperand, rightOperand OperandBuilder) SetValueBuilder {
return SetValueBuilder{
leftOperand: leftOperand,
rightOperand: rightOperand,
mode: listAppendValueMode,
}
}
// ListAppend creates a SetValueBuilder to be used in as an argument to Set().
// The arguments can either be NameBuilders or ValueBuilders. ListAppend() only
// supports DynamoDB List types, so the ValueBuilder must be a List and the
// NameBuilder must specify an item attribute of type List.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.UpdatingListElements
//
// Example:
//
// // Use ListAppend() to set item attribute "someName" to the item
// // attribute "nameOfList" with "some" and "list" appended to it
// update, err := expression.Set(expression.Name("someName"), expression.Name("nameOfList").ListAppend(expression.Value([]string{"some", "list"})))
//
// Expression Equivalent:
//
// expression.Name("nameOfList").ListAppend(expression.Value([]string{"some", "list"})
// // let :list be a ExpressionAttributeValue representing the list
// // containing "some" and "list".
// "list_append (nameOfList, :list)"
func (nb NameBuilder) ListAppend(rightOperand OperandBuilder) SetValueBuilder {
return ListAppend(nb, rightOperand)
}
// ListAppend creates a SetValueBuilder to be used in as an argument to Set().
// The arguments can either be NameBuilders or ValueBuilders. ListAppend() only
// supports DynamoDB List types, so the ValueBuilder must be a List and the
// NameBuilder must specify an item attribute of type List.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.UpdatingListElements
//
// Example:
//
// // Use ListAppend() to set item attribute "someName" to a string list
// // equal to {"a", "list", "some", "list"}
// update, err := expression.Set(expression.Name("someName"), expression.Value([]string{"a", "list"}).ListAppend(expression.Value([]string{"some", "list"})))
//
// Expression Equivalent:
//
// expression.Name([]string{"a", "list"}).ListAppend(expression.Value([]string{"some", "list"})
// // let :list1 and :list2 be a ExpressionAttributeValue representing the
// // list {"a", "list"} and {"some", "list"} respectively
// "list_append (:list1, :list2)"
func (vb ValueBuilder) ListAppend(rightOperand OperandBuilder) SetValueBuilder {
return ListAppend(vb, rightOperand)
}
// IfNotExists creates a SetValueBuilder to be used in as an argument to Set().
// The first argument must be a NameBuilder representing the name where the new
// item attribute is created. The second argument can either be a NameBuilder or
// a ValueBuilder. In the case that it is a NameBuilder, the value of the item
// attribute at the name specified becomes the value of the new item attribute.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.PreventingAttributeOverwrites
//
// Example:
//
// // Use IfNotExists() to set item attribute "someName" to value 5 if
// // "someName" does not exist yet. (Prevents overwrite)
// update, err := expression.Set(expression.Name("someName"), expression.IfNotExists(expression.Name("someName"), expression.Value(5)))
//
// Expression Equivalent:
//
// expression.IfNotExists(expression.Name("someName"), expression.Value(5))
// // let :five be a ExpressionAttributeValue representing the value 5
// "if_not_exists (someName, :five)"
func IfNotExists(name NameBuilder, setValue OperandBuilder) SetValueBuilder {
return SetValueBuilder{
leftOperand: name,
rightOperand: setValue,
mode: ifNotExistsValueMode,
}
}
// IfNotExists creates a SetValueBuilder to be used in as an argument to Set().
// The first argument must be a NameBuilder representing the name where the new
// item attribute is created. The second argument can either be a NameBuilder or
// a ValueBuilder. In the case that it is a NameBuilder, the value of the item
// attribute at the name specified becomes the value of the new item attribute.
// More information: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET.PreventingAttributeOverwrites
//
// Example:
//
// // Use IfNotExists() to set item attribute "someName" to value 5 if
// // "someName" does not exist yet. (Prevents overwrite)
// update, err := expression.Set(expression.Name("someName"), expression.Name("someName").IfNotExists(expression.Value(5)))
//
// Expression Equivalent:
//
// expression.Name("someName").IfNotExists(expression.Value(5))
// // let :five be a ExpressionAttributeValue representing the value 5
// "if_not_exists (someName, :five)"
func (nb NameBuilder) IfNotExists(rightOperand OperandBuilder) SetValueBuilder {
return IfNotExists(nb, rightOperand)
}
// BuildOperand creates an Operand struct which are building blocks to DynamoDB
// Expressions. Package methods and functions can establish relationships
// between operands, representing DynamoDB Expressions. The method
// BuildOperand() is called recursively when the Build() method on the type
// Builder is called. BuildOperand() should never be called externally.
// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved
// words.
// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
func (nb NameBuilder) BuildOperand() (Operand, error) {
if nb.name == "" {
return Operand{}, newUnsetParameterError("BuildOperand", "NameBuilder")
}
node := exprNode{
names: []string{},
}
nameSplit := strings.Split(nb.name, ".")
fmtNames := make([]string, 0, len(nameSplit))
for _, word := range nameSplit {
var substr string
if word == "" {
return Operand{}, newInvalidParameterError("BuildOperand", "NameBuilder")
}
if word[len(word)-1] == ']' {
for j, char := range word {
if char == '[' {
substr = word[j:]
word = word[:j]
break
}
}
}
if word == "" {
return Operand{}, newInvalidParameterError("BuildOperand", "NameBuilder")
}
// Create a string with special characters that can be substituted later: $p
node.names = append(node.names, word)
fmtNames = append(fmtNames, "$n"+substr)
}
node.fmtExpr = strings.Join(fmtNames, ".")
return Operand{
exprNode: node,
}, nil
}
// BuildOperand creates an Operand struct which are building blocks to DynamoDB
// Expressions. Package methods and functions can establish relationships
// between operands, representing DynamoDB Expressions. The method
// BuildOperand() is called recursively when the Build() method on the type
// Builder is called. BuildOperand() should never be called externally.
// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved
// words.
// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
func (vb ValueBuilder) BuildOperand() (Operand, error) {
expr, err := dynamodbattribute.Marshal(vb.value)
if err != nil {
return Operand{}, newInvalidParameterError("BuildOperand", "ValueBuilder")
}
// Create a string with special characters that can be substituted later: $v
operand := Operand{
exprNode: exprNode{
values: []dynamodb.AttributeValue{*expr},
fmtExpr: "$v",
},
}
return operand, nil
}
// BuildOperand creates an Operand struct which are building blocks to DynamoDB
// Expressions. Package methods and functions can establish relationships
// between operands, representing DynamoDB Expressions. The method
// BuildOperand() is called recursively when the Build() method on the type
// Builder is called. BuildOperand() should never be called externally.
// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved
// words.
// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
func (sb SizeBuilder) BuildOperand() (Operand, error) {
operand, err := sb.nameBuilder.BuildOperand()
operand.exprNode.fmtExpr = "size (" + operand.exprNode.fmtExpr + ")"
return operand, err
}
// BuildOperand creates an Operand struct which are building blocks to DynamoDB
// Expressions. Package methods and functions can establish relationships
// between operands, representing DynamoDB Expressions. The method
// BuildOperand() is called recursively when the Build() method on the type
// Builder is called. BuildOperand() should never be called externally.
// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved
// words.
// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
func (kb KeyBuilder) BuildOperand() (Operand, error) {
if kb.key == "" {
return Operand{}, newUnsetParameterError("BuildOperand", "KeyBuilder")
}
ret := Operand{
exprNode: exprNode{
names: []string{kb.key},
fmtExpr: "$n",
},
}
return ret, nil
}
// BuildOperand creates an Operand struct which are building blocks to DynamoDB
// Expressions. Package methods and functions can establish relationships
// between operands, representing DynamoDB Expressions. The method
// BuildOperand() is called recursively when the Build() method on the type
// Builder is called. BuildOperand() should never be called externally.
// BuildOperand() aliases all strings to avoid stepping over DynamoDB's reserved
// words.
// More information on reserved words at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
func (svb SetValueBuilder) BuildOperand() (Operand, error) {
if svb.mode == unsetValue {
return Operand{}, newUnsetParameterError("BuildOperand", "SetValueBuilder")
}
left, err := svb.leftOperand.BuildOperand()
if err != nil {
return Operand{}, err
}
leftNode := left.exprNode
right, err := svb.rightOperand.BuildOperand()
if err != nil {
return Operand{}, err
}
rightNode := right.exprNode
node := exprNode{
children: []exprNode{leftNode, rightNode},
}
switch svb.mode {
case plusValueMode:
node.fmtExpr = "$c + $c"
case minusValueMode:
node.fmtExpr = "$c - $c"
case listAppendValueMode:
node.fmtExpr = "list_append($c, $c)"
case ifNotExistsValueMode:
node.fmtExpr = "if_not_exists($c, $c)"
default:
return Operand{}, fmt.Errorf("build operand error: unsupported mode: %v", svb.mode)
}
return Operand{
exprNode: node,
}, nil
}

View File

@@ -0,0 +1,144 @@
// +build go1.7
package expression
import (
"reflect"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// opeErrorMode will help with error cases and checking error types
type opeErrorMode string
const (
noOperandError opeErrorMode = ""
// unsetName error will occur if an empty string is passed into NameBuilder
unsetName = "unset parameter: NameBuilder"
// invalidName error will occur if a nested name has an empty intermediary
// attribute name (i.e. foo.bar..baz)
invalidName = "invalid parameter: NameBuilder"
// unsetKey error will occur if an empty string is passed into KeyBuilder
unsetKey = "unset parameter: KeyBuilder"
)
func TestBuildOperand(t *testing.T) {
cases := []struct {
name string
input OperandBuilder
expected exprNode
err opeErrorMode
}{
{
name: "basic name",
input: Name("foo"),
expected: exprNode{
names: []string{"foo"},
fmtExpr: "$n",
},
},
{
name: "duplicate name name",
input: Name("foo.foo"),
expected: exprNode{
names: []string{"foo", "foo"},
fmtExpr: "$n.$n",
},
},
{
name: "basic value",
input: Value(5),
expected: exprNode{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
{
name: "nested name",
input: Name("foo.bar"),
expected: exprNode{
names: []string{"foo", "bar"},
fmtExpr: "$n.$n",
},
},
{
name: "nested name with index",
input: Name("foo.bar[0].baz"),
expected: exprNode{
names: []string{"foo", "bar", "baz"},
fmtExpr: "$n.$n[0].$n",
},
},
{
name: "basic size",
input: Name("foo").Size(),
expected: exprNode{
names: []string{"foo"},
fmtExpr: "size ($n)",
},
},
{
name: "key",
input: Key("foo"),
expected: exprNode{
names: []string{"foo"},
fmtExpr: "$n",
},
},
{
name: "unset key error",
input: Key(""),
expected: exprNode{},
err: unsetKey,
},
{
name: "empty name error",
input: Name(""),
expected: exprNode{},
err: unsetName,
},
{
name: "invalid name",
input: Name("foo..bar"),
expected: exprNode{},
err: invalidName,
},
{
name: "invalid index",
input: Name("[foo]"),
expected: exprNode{},
err: invalidName,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
operand, err := c.input.BuildOperand()
if c.err != noOperandError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expected, operand.exprNode; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}

View File

@@ -0,0 +1,148 @@
package expression
import (
"strings"
)
// ProjectionBuilder represents Projection Expressions in DynamoDB.
// ProjectionBuilders are the building blocks of Builders.
// More Information at: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html
type ProjectionBuilder struct {
names []NameBuilder
}
// NamesList returns a ProjectionBuilder representing the list of item
// attribute names specified by the argument NameBuilders. The resulting
// ProjectionBuilder can be used as a part of other ProjectionBuilders or as an
// argument to the WithProjection() method for the Builder struct.
//
// Example:
//
// // projection represents the list of names {"foo", "bar"}
// projection := expression.NamesList(expression.Name("foo"), expression.Name("bar"))
//
// // Used in another Projection Expression
// anotherProjection := expression.AddNames(projection, expression.Name("baz"))
// // Used to make an Builder
// builder := expression.NewBuilder().WithProjection(newProjection)
//
// Expression Equivalent:
//
// expression.NamesList(expression.Name("foo"), expression.Name("bar"))
// "foo, bar"
func NamesList(nameBuilder NameBuilder, namesList ...NameBuilder) ProjectionBuilder {
namesList = append([]NameBuilder{nameBuilder}, namesList...)
return ProjectionBuilder{
names: namesList,
}
}
// NamesList returns a ProjectionBuilder representing the list of item
// attribute names specified by the argument NameBuilders. The resulting
// ProjectionBuilder can be used as a part of other ProjectionBuilders or as an
// argument to the WithProjection() method for the Builder struct.
//
// Example:
//
// // projection represents the list of names {"foo", "bar"}
// projection := expression.Name("foo").NamesList(expression.Name("bar"))
//
// // Used in another Projection Expression
// anotherProjection := expression.AddNames(projection, expression.Name("baz"))
// // Used to make an Builder
// builder := expression.NewBuilder().WithProjection(newProjection)
//
// Expression Equivalent:
//
// expression.Name("foo").NamesList(expression.Name("bar"))
// "foo, bar"
func (nb NameBuilder) NamesList(namesList ...NameBuilder) ProjectionBuilder {
return NamesList(nb, namesList...)
}
// AddNames returns a ProjectionBuilder representing the list of item
// attribute names equivalent to appending all of the argument item attribute
// names to the argument ProjectionBuilder. The resulting ProjectionBuilder can
// be used as a part of other ProjectionBuilders or as an argument to the
// WithProjection() method for the Builder struct.
//
// Example:
//
// // projection represents the list of names {"foo", "bar", "baz", "qux"}
// oldProj := expression.NamesList(expression.Name("foo"), expression.Name("bar"))
// projection := expression.AddNames(oldProj, expression.Name("baz"), expression.Name("qux"))
//
// // Used in another Projection Expression
// anotherProjection := expression.AddNames(projection, expression.Name("quux"))
// // Used to make an Builder
// builder := expression.NewBuilder().WithProjection(newProjection)
//
// Expression Equivalent:
//
// expression.AddNames(expression.NamesList(expression.Name("foo"), expression.Name("bar")), expression.Name("baz"), expression.Name("qux"))
// "foo, bar, baz, qux"
func AddNames(projectionBuilder ProjectionBuilder, namesList ...NameBuilder) ProjectionBuilder {
projectionBuilder.names = append(projectionBuilder.names, namesList...)
return projectionBuilder
}
// AddNames returns a ProjectionBuilder representing the list of item
// attribute names equivalent to appending all of the argument item attribute
// names to the argument ProjectionBuilder. The resulting ProjectionBuilder can
// be used as a part of other ProjectionBuilders or as an argument to the
// WithProjection() method for the Builder struct.
//
// Example:
//
// // projection represents the list of names {"foo", "bar", "baz", "qux"}
// oldProj := expression.NamesList(expression.Name("foo"), expression.Name("bar"))
// projection := oldProj.AddNames(expression.Name("baz"), expression.Name("qux"))
//
// // Used in another Projection Expression
// anotherProjection := expression.AddNames(projection, expression.Name("quux"))
// // Used to make an Builder
// builder := expression.NewBuilder().WithProjection(newProjection)
//
// Expression Equivalent:
//
// expression.NamesList(expression.Name("foo"), expression.Name("bar")).AddNames(expression.Name("baz"), expression.Name("qux"))
// "foo, bar, baz, qux"
func (pb ProjectionBuilder) AddNames(namesList ...NameBuilder) ProjectionBuilder {
return AddNames(pb, namesList...)
}
// buildTree builds a tree structure of exprNodes based on the tree
// structure of the input ProjectionBuilder's child NameBuilders. buildTree()
// satisfies the treeBuilder interface so ProjectionBuilder can be a part of
// Builder and Expression struct.
func (pb ProjectionBuilder) buildTree() (exprNode, error) {
if len(pb.names) == 0 {
return exprNode{}, newUnsetParameterError("buildTree", "ProjectionBuilder")
}
childNodes, err := pb.buildChildNodes()
if err != nil {
return exprNode{}, err
}
ret := exprNode{
children: childNodes,
}
ret.fmtExpr = "$c" + strings.Repeat(", $c", len(pb.names)-1)
return ret, nil
}
// buildChildNodes creates the list of the child exprNodes.
func (pb ProjectionBuilder) buildChildNodes() ([]exprNode, error) {
childNodes := make([]exprNode, 0, len(pb.names))
for _, name := range pb.names {
operand, err := name.BuildOperand()
if err != nil {
return []exprNode{}, err
}
childNodes = append(childNodes, operand.exprNode)
}
return childNodes, nil
}

View File

@@ -0,0 +1,215 @@
// +build go1.7
package expression
import (
"reflect"
"strings"
"testing"
)
// projErrorMode will help with error cases and checking error types
type projErrorMode string
const (
noProjError projErrorMode = ""
// invalidProjectionOperand error will occur when an invalid OperandBuilder is
// used as an argument
invalidProjectionOperand = "BuildOperand error"
// unsetProjection error will occur if the argument ProjectionBuilder is unset
unsetProjection = "unset parameter: ProjectionBuilder"
)
func TestProjectionBuilder(t *testing.T) {
cases := []struct {
name string
input ProjectionBuilder
expectedNode exprNode
err projErrorMode
}{
{
name: "names list function call",
input: NamesList(Name("foo"), Name("bar")),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "$c, $c",
},
},
{
name: "names list method call",
input: Name("foo").NamesList(Name("bar")),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "$c, $c",
},
},
{
name: "add name",
input: Name("foo").NamesList(Name("bar")).AddNames(Name("baz"), Name("qux")),
expectedNode: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
{
names: []string{"baz"},
fmtExpr: "$n",
}, {
names: []string{"qux"},
fmtExpr: "$n",
},
},
fmtExpr: "$c, $c, $c, $c",
},
},
{
name: "invalid operand",
input: NamesList(Name("")),
err: invalidProjectionOperand,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noProjError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestBuildProjection(t *testing.T) {
cases := []struct {
name string
input ProjectionBuilder
expected string
err projErrorMode
}{
{
name: "build projection 3",
input: NamesList(Name("foo"), Name("bar"), Name("baz")),
expected: "$c, $c, $c",
},
{
name: "build projection 5",
input: NamesList(Name("foo"), Name("bar"), Name("baz")).AddNames(Name("qux"), Name("quux")),
expected: "$c, $c, $c, $c, $c",
},
{
name: "empty ProjectionBuilder",
input: ProjectionBuilder{},
err: unsetProjection,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noProjError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expected, actual.fmtExpr; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestBuildProjectionChildNodes(t *testing.T) {
cases := []struct {
name string
input ProjectionBuilder
expected []exprNode
err projErrorMode
}{
{
name: "build child nodes",
input: NamesList(Name("foo"), Name("bar"), Name("baz")),
expected: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
{
names: []string{"baz"},
fmtExpr: "$n",
},
},
},
{
name: "operand error",
input: NamesList(Name("")),
err: invalidProjectionOperand,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noProjError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expected, actual.children; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}

View File

@@ -0,0 +1,391 @@
package expression
import (
"fmt"
"sort"
"strings"
)
// operationMode specifies the types of update operations that the
// updateBuilder is going to represent. The const is in a string to use the
// const value as a map key and as a string when creating the formatted
// expression for the exprNodes.
type operationMode string
const (
setOperation operationMode = "SET"
removeOperation = "REMOVE"
addOperation = "ADD"
deleteOperation = "DELETE"
)
// Implementing the Sort interface
type modeList []operationMode
func (ml modeList) Len() int {
return len(ml)
}
func (ml modeList) Less(i, j int) bool {
return string(ml[i]) < string(ml[j])
}
func (ml modeList) Swap(i, j int) {
ml[i], ml[j] = ml[j], ml[i]
}
// UpdateBuilder represents Update Expressions in DynamoDB. UpdateBuilders
// are the building blocks of the Builder struct. Note that there are different
// update operations in DynamoDB and an UpdateBuilder can represent multiple
// update operations.
// More Information at: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html
type UpdateBuilder struct {
operationList map[operationMode][]operationBuilder
}
// operationBuilder represents specific update actions (SET, REMOVE, ADD,
// DELETE). The mode specifies what type of update action the
// operationBuilder represents.
type operationBuilder struct {
name NameBuilder
value OperandBuilder
mode operationMode
}
// buildOperation builds an exprNode from an operationBuilder. buildOperation
// is called recursively by buildTree in order to create a tree structure
// of exprNodes representing the parent/child relationships between
// UpdateBuilders and operationBuilders.
func (ob operationBuilder) buildOperation() (exprNode, error) {
pathChild, err := ob.name.BuildOperand()
if err != nil {
return exprNode{}, err
}
node := exprNode{
children: []exprNode{pathChild.exprNode},
fmtExpr: "$c",
}
if ob.mode == removeOperation {
return node, nil
}
valueChild, err := ob.value.BuildOperand()
if err != nil {
return exprNode{}, err
}
node.children = append(node.children, valueChild.exprNode)
switch ob.mode {
case setOperation:
node.fmtExpr += " = $c"
case addOperation, deleteOperation:
node.fmtExpr += " $c"
default:
return exprNode{}, fmt.Errorf("build update error: build operation error: unsupported mode: %v", ob.mode)
}
return node, nil
}
// Delete returns an UpdateBuilder representing one Delete operation for
// DynamoDB Update Expressions. The argument name should specify the item
// attribute and the argument value should specify the value to be deleted. The
// resulting UpdateBuilder can be used as an argument to the WithUpdate() method
// for the Builder struct.
//
// Example:
//
// // update represents the delete operation to delete the string value
// // "subsetToDelete" from the item attribute "pathToList"
// update := expression.Delete(expression.Name("pathToList"), expression.Value("subsetToDelete"))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// expression.Delete(expression.Name("pathToList"), expression.Value("subsetToDelete"))
// // let :del be an ExpressionAttributeValue representing the value
// // "subsetToDelete"
// "DELETE pathToList :del"
func Delete(name NameBuilder, value ValueBuilder) UpdateBuilder {
emptyUpdateBuilder := UpdateBuilder{}
return emptyUpdateBuilder.Delete(name, value)
}
// Delete adds a Delete operation to the argument UpdateBuilder. The
// argument name should specify the item attribute and the argument value should
// specify the value to be deleted. The resulting UpdateBuilder can be used as
// an argument to the WithUpdate() method for the Builder struct.
//
// Example:
//
// // Let update represent an already existing update expression. Delete()
// // adds the operation to delete the value "subsetToDelete" from the item
// // attribute "pathToList"
// update := update.Delete(expression.Name("pathToList"), expression.Value("subsetToDelete"))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// Delete(expression.Name("pathToList"), expression.Value("subsetToDelete"))
// // let :del be an ExpressionAttributeValue representing the value
// // "subsetToDelete"
// "DELETE pathToList :del"
func (ub UpdateBuilder) Delete(name NameBuilder, value ValueBuilder) UpdateBuilder {
if ub.operationList == nil {
ub.operationList = map[operationMode][]operationBuilder{}
}
ub.operationList[deleteOperation] = append(ub.operationList[deleteOperation], operationBuilder{
name: name,
value: value,
mode: deleteOperation,
})
return ub
}
// Add returns an UpdateBuilder representing the Add operation for DynamoDB
// Update Expressions. The argument name should specify the item attribute and
// the argument value should specify the value to be added. The resulting
// UpdateBuilder can be used as an argument to the WithUpdate() method for the
// Builder struct.
//
// Example:
//
// // update represents the add operation to add the value 5 to the item
// // attribute "aPath"
// update := expression.Add(expression.Name("aPath"), expression.Value(5))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// expression.Add(expression.Name("aPath"), expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "ADD aPath :5"
func Add(name NameBuilder, value ValueBuilder) UpdateBuilder {
emptyUpdateBuilder := UpdateBuilder{}
return emptyUpdateBuilder.Add(name, value)
}
// Add adds an Add operation to the argument UpdateBuilder. The argument
// name should specify the item attribute and the argument value should specify
// the value to be added. The resulting UpdateBuilder can be used as an argument
// to the WithUpdate() method for the Builder struct.
//
// Example:
//
// // Let update represent an already existing update expression. Add() adds
// // the operation to add the value 5 to the item attribute "aPath"
// update := update.Add(expression.Name("aPath"), expression.Value(5))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// Add(expression.Name("aPath"), expression.Value(5))
// // Let :five be an ExpressionAttributeValue representing the value 5
// "ADD aPath :5"
func (ub UpdateBuilder) Add(name NameBuilder, value ValueBuilder) UpdateBuilder {
if ub.operationList == nil {
ub.operationList = map[operationMode][]operationBuilder{}
}
ub.operationList[addOperation] = append(ub.operationList[addOperation], operationBuilder{
name: name,
value: value,
mode: addOperation,
})
return ub
}
// Remove returns an UpdateBuilder representing the Remove operation for
// DynamoDB Update Expressions. The argument name should specify the item
// attribute to delete. The resulting UpdateBuilder can be used as an argument
// to the WithUpdate() method for the Builder struct.
//
// Example:
//
// // update represents the remove operation to remove the item attribute
// // "itemToRemove"
// update := expression.Remove(expression.Name("itemToRemove"))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// expression.Remove(expression.Name("itemToRemove"))
// "REMOVE itemToRemove"
func Remove(name NameBuilder) UpdateBuilder {
emptyUpdateBuilder := UpdateBuilder{}
return emptyUpdateBuilder.Remove(name)
}
// Remove adds a Remove operation to the argument UpdateBuilder. The
// argument name should specify the item attribute to delete. The resulting
// UpdateBuilder can be used as an argument to the WithUpdate() method for the
// Builder struct.
//
// Example:
//
// // Let update represent an already existing update expression. Remove()
// // adds the operation to remove the item attribute "itemToRemove"
// update := update.Remove(expression.Name("itemToRemove"))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// Remove(expression.Name("itemToRemove"))
// "REMOVE itemToRemove"
func (ub UpdateBuilder) Remove(name NameBuilder) UpdateBuilder {
if ub.operationList == nil {
ub.operationList = map[operationMode][]operationBuilder{}
}
ub.operationList[removeOperation] = append(ub.operationList[removeOperation], operationBuilder{
name: name,
mode: removeOperation,
})
return ub
}
// Set returns an UpdateBuilder representing the Set operation for DynamoDB
// Update Expressions. The argument name should specify the item attribute to
// modify. The argument OperandBuilder should specify the value to modify the
// the item attribute to. The resulting UpdateBuilder can be used as an argument
// to the WithUpdate() method for the Builder struct.
//
// Example:
//
// // update represents the set operation to set the item attribute
// // "itemToSet" to the value "setValue" if the item attribute does not
// // exist yet. (conditional write)
// update := expression.Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue")))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// expression.Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue")))
// // Let :val be an ExpressionAttributeValue representing the value
// // "setValue"
// "SET itemToSet = :val"
func Set(name NameBuilder, operandBuilder OperandBuilder) UpdateBuilder {
emptyUpdateBuilder := UpdateBuilder{}
return emptyUpdateBuilder.Set(name, operandBuilder)
}
// Set adds a Set operation to the argument UpdateBuilder. The argument name
// should specify the item attribute to modify. The argument OperandBuilder
// should specify the value to modify the the item attribute to. The resulting
// UpdateBuilder can be used as an argument to the WithUpdate() method for the
// Builder struct.
//
// Example:
//
// // Let update represent an already existing update expression. Set() adds
// // the operation to to set the item attribute "itemToSet" to the value
// // "setValue" if the item attribute does not exist yet. (conditional
// // write)
// update := update.Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue")))
//
// // Adding more update methods
// anotherUpdate := update.Remove(expression.Name("someName"))
// // Creating a Builder
// builder := Update(update)
//
// Expression Equivalent:
//
// Set(expression.Name("itemToSet"), expression.IfNotExists(expression.Name("itemToSet"), expression.Value("setValue")))
// // Let :val be an ExpressionAttributeValue representing the value
// // "setValue"
// "SET itemToSet = :val"
func (ub UpdateBuilder) Set(name NameBuilder, operandBuilder OperandBuilder) UpdateBuilder {
if ub.operationList == nil {
ub.operationList = map[operationMode][]operationBuilder{}
}
ub.operationList[setOperation] = append(ub.operationList[setOperation], operationBuilder{
name: name,
value: operandBuilder,
mode: setOperation,
})
return ub
}
// buildTree builds a tree structure of exprNodes based on the tree
// structure of the input UpdateBuilder's child UpdateBuilders/Operands.
// buildTree() satisfies the TreeBuilder interface so ProjectionBuilder can be a
// part of Expression struct.
func (ub UpdateBuilder) buildTree() (exprNode, error) {
if ub.operationList == nil {
return exprNode{}, newUnsetParameterError("buildTree", "UpdateBuilder")
}
ret := exprNode{
children: []exprNode{},
}
modes := modeList{}
for mode := range ub.operationList {
modes = append(modes, mode)
}
sort.Sort(modes)
for _, key := range modes {
ret.fmtExpr += string(key) + " $c\n"
childNode, err := buildChildNodes(ub.operationList[key])
if err != nil {
return exprNode{}, err
}
ret.children = append(ret.children, childNode)
}
return ret, nil
}
// buildChildNodes creates the list of the child exprNodes.
func buildChildNodes(operationBuilderList []operationBuilder) (exprNode, error) {
if len(operationBuilderList) == 0 {
return exprNode{}, fmt.Errorf("buildChildNodes error: operationBuilder list is empty")
}
node := exprNode{
children: make([]exprNode, 0, len(operationBuilderList)),
fmtExpr: "$c" + strings.Repeat(", $c", len(operationBuilderList)-1),
}
for _, val := range operationBuilderList {
valNode, err := val.buildOperation()
if err != nil {
return exprNode{}, err
}
node.children = append(node.children, valNode)
}
return node, nil
}

View File

@@ -0,0 +1,771 @@
// +build go1.7
package expression
import (
"reflect"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// updateErrorMode will help with error cases and checking error types
type updateErrorMode string
const (
noUpdateError updateErrorMode = ""
invalidUpdateOperand = "BuildOperand error"
unsetSetValue = "unset parameter: SetValueBuilder"
unsetUpdate = "unset parameter: UpdateBuilder"
emptyOperationBuilderList = "operationBuilder list is empty"
)
func TestBuildOperation(t *testing.T) {
cases := []struct {
name string
input operationBuilder
expected exprNode
err updateErrorMode
}{
{
name: "set operation",
input: operationBuilder{
name: Name("foo"),
value: Value(5),
mode: setOperation,
},
expected: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c = $c",
},
},
{
name: "add operation",
input: operationBuilder{
name: Name("foo"),
value: Value(5),
mode: addOperation,
},
expected: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c $c",
},
},
{
name: "remove operation",
input: operationBuilder{
name: Name("foo"),
mode: removeOperation,
},
expected: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
},
fmtExpr: "$c",
},
},
{
name: "invalid operand",
input: operationBuilder{
name: Name(""),
mode: removeOperation,
},
err: invalidUpdateOperand,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildOperation()
if c.err != noUpdateError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expected, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestUpdateTree(t *testing.T) {
cases := []struct {
name string
input UpdateBuilder
expectedNode exprNode
err updateErrorMode
}{
{
name: "set update",
input: Set(Name("foo"), Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c = $c",
},
},
fmtExpr: "$c",
},
},
fmtExpr: "SET $c\n",
},
},
{
name: "remove update",
input: Remove(Name("foo")),
expectedNode: exprNode{
children: []exprNode{
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
},
fmtExpr: "$c",
},
},
fmtExpr: "$c",
},
},
fmtExpr: "REMOVE $c\n",
},
},
{
name: "add update",
input: Add(Name("foo"), Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c $c",
},
},
fmtExpr: "$c",
},
},
fmtExpr: "ADD $c\n",
},
},
{
name: "delete update",
input: Delete(Name("foo"), Value(5)),
expectedNode: exprNode{
children: []exprNode{
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c $c",
},
},
fmtExpr: "$c",
},
},
fmtExpr: "DELETE $c\n",
},
},
{
name: "multiple sets",
input: Set(Name("foo"), Value(5)).Set(Name("bar"), Value(6)).Set(Name("baz"), Name("qux")),
expectedNode: exprNode{
fmtExpr: "SET $c\n",
children: []exprNode{
{
fmtExpr: "$c, $c, $c",
children: []exprNode{
{
fmtExpr: "$c = $c",
children: []exprNode{
{
fmtExpr: "$n",
names: []string{"foo"},
},
{
fmtExpr: "$v",
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
},
},
},
{
fmtExpr: "$c = $c",
children: []exprNode{
{
fmtExpr: "$n",
names: []string{"bar"},
},
{
fmtExpr: "$v",
values: []dynamodb.AttributeValue{
{
N: aws.String("6"),
},
},
},
},
},
{
fmtExpr: "$c = $c",
children: []exprNode{
{
fmtExpr: "$n",
names: []string{"baz"},
},
{
fmtExpr: "$n",
names: []string{"qux"},
},
},
},
},
},
},
},
},
{
name: "compound update",
input: Add(Name("foo"), Value(5)).Set(Name("foo"), Value(5)).Delete(Name("foo"), Value(5)).Remove(Name("foo")),
expectedNode: exprNode{
children: []exprNode{
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c $c",
},
},
fmtExpr: "$c",
},
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c $c",
},
},
fmtExpr: "$c",
},
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
},
fmtExpr: "$c",
},
},
fmtExpr: "$c",
},
{
children: []exprNode{
{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
},
fmtExpr: "$c = $c",
},
},
fmtExpr: "$c",
},
},
fmtExpr: "ADD $c\nDELETE $c\nREMOVE $c\nSET $c\n",
},
},
{
name: "empty UpdateBuilder",
input: UpdateBuilder{},
err: unsetUpdate,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.buildTree()
if c.err != noUpdateError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expectedNode, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestSetValueBuilder(t *testing.T) {
cases := []struct {
name string
input SetValueBuilder
expected exprNode
err updateErrorMode
}{
{
name: "name plus name",
input: Name("foo").Plus(Name("bar")),
expected: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "$c + $c",
},
},
{
name: "name minus name",
input: Name("foo").Minus(Name("bar")),
expected: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "$c - $c",
},
},
{
name: "list append name and name",
input: Name("foo").ListAppend(Name("bar")),
expected: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "list_append($c, $c)",
},
},
{
name: "if not exists name and name",
input: Name("foo").IfNotExists(Name("bar")),
expected: exprNode{
children: []exprNode{
{
names: []string{"foo"},
fmtExpr: "$n",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "if_not_exists($c, $c)",
},
},
{
name: "value plus name",
input: Value(5).Plus(Name("bar")),
expected: exprNode{
children: []exprNode{
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "$c + $c",
},
},
{
name: "value minus name",
input: Value(5).Minus(Name("bar")),
expected: exprNode{
children: []exprNode{
{
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
fmtExpr: "$v",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "$c - $c",
},
},
{
name: "list append list and name",
input: Value([]int{1, 2, 3}).ListAppend(Name("bar")),
expected: exprNode{
children: []exprNode{
{
values: []dynamodb.AttributeValue{
{
L: []*dynamodb.AttributeValue{
{
N: aws.String("1"),
},
{
N: aws.String("2"),
},
{
N: aws.String("3"),
},
},
},
},
fmtExpr: "$v",
},
{
names: []string{"bar"},
fmtExpr: "$n",
},
},
fmtExpr: "list_append($c, $c)",
},
},
{
name: "unset SetValueBuilder",
input: SetValueBuilder{},
err: unsetSetValue,
},
{
name: "invalid operand error",
input: Name("").Plus(Name("foo")),
err: invalidUpdateOperand,
},
{
name: "invalid operand error",
input: Name("foo").Plus(Name("")),
err: invalidUpdateOperand,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := c.input.BuildOperand()
if c.err != noUpdateError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expected, actual.exprNode; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}
func TestUpdateBuildChildNodes(t *testing.T) {
cases := []struct {
name string
input []operationBuilder
expected exprNode
err updateErrorMode
}{
{
name: "set operand builder",
input: []operationBuilder{
{
mode: setOperation,
name: NameBuilder{
name: "foo",
},
value: ValueBuilder{
value: 5,
},
},
{
mode: setOperation,
name: NameBuilder{
name: "bar",
},
value: ValueBuilder{
value: 6,
},
},
{
mode: setOperation,
name: NameBuilder{
name: "baz",
},
value: ValueBuilder{
value: 7,
},
},
{
mode: setOperation,
name: NameBuilder{
name: "qux",
},
value: ValueBuilder{
value: 8,
},
},
},
expected: exprNode{
fmtExpr: "$c, $c, $c, $c",
children: []exprNode{
{
fmtExpr: "$c = $c",
children: []exprNode{
{
fmtExpr: "$n",
names: []string{"foo"},
},
{
fmtExpr: "$v",
values: []dynamodb.AttributeValue{
{
N: aws.String("5"),
},
},
},
},
},
{
fmtExpr: "$c = $c",
children: []exprNode{
{
fmtExpr: "$n",
names: []string{"bar"},
},
{
fmtExpr: "$v",
values: []dynamodb.AttributeValue{
{
N: aws.String("6"),
},
},
},
},
},
{
fmtExpr: "$c = $c",
children: []exprNode{
{
fmtExpr: "$n",
names: []string{"baz"},
},
{
fmtExpr: "$v",
values: []dynamodb.AttributeValue{
{
N: aws.String("7"),
},
},
},
},
},
{
fmtExpr: "$c = $c",
children: []exprNode{
{
fmtExpr: "$n",
names: []string{"qux"},
},
{
fmtExpr: "$v",
values: []dynamodb.AttributeValue{
{
N: aws.String("8"),
},
},
},
},
},
},
},
},
{
name: "empty operationBuilder list",
input: []operationBuilder{},
err: emptyOperationBuilderList,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := buildChildNodes(c.input)
if c.err != noUpdateError {
if err == nil {
t.Errorf("expect error %q, got no error", c.err)
} else {
if e, a := string(c.err), err.Error(); !strings.Contains(a, e) {
t.Errorf("expect %q error message to be in %q", e, a)
}
}
} else {
if err != nil {
t.Errorf("expect no error, got unexpected Error %q", err)
}
if e, a := c.expected, actual; !reflect.DeepEqual(a, e) {
t.Errorf("expect %v, got %v", e, a)
}
}
})
}
}

View File

@@ -0,0 +1,95 @@
// Code generated by private/model/cli/gen-api/main.go. DO NOT EDIT.
package dynamodb
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/client/metadata"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/signer/v4"
"github.com/aws/aws-sdk-go/private/protocol/jsonrpc"
)
// DynamoDB provides the API operation methods for making requests to
// Amazon DynamoDB. See this package's package overview docs
// for details on the service.
//
// DynamoDB methods are safe to use concurrently. It is not safe to
// modify mutate any of the struct's properties though.
type DynamoDB struct {
*client.Client
}
// Used for custom client initialization logic
var initClient func(*client.Client)
// Used for custom request initialization logic
var initRequest func(*request.Request)
// Service information constants
const (
ServiceName = "dynamodb" // Service endpoint prefix API calls made to.
EndpointsID = ServiceName // Service ID for Regions and Endpoints metadata.
)
// New creates a new instance of the DynamoDB client with a session.
// If additional configuration is needed for the client instance use the optional
// aws.Config parameter to add your extra config.
//
// Example:
// // Create a DynamoDB client from just a session.
// svc := dynamodb.New(mySession)
//
// // Create a DynamoDB client with additional configuration
// svc := dynamodb.New(mySession, aws.NewConfig().WithRegion("us-west-2"))
func New(p client.ConfigProvider, cfgs ...*aws.Config) *DynamoDB {
c := p.ClientConfig(EndpointsID, cfgs...)
return newClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName)
}
// newClient creates, initializes and returns a new service client instance.
func newClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *DynamoDB {
svc := &DynamoDB{
Client: client.New(
cfg,
metadata.ClientInfo{
ServiceName: ServiceName,
SigningName: signingName,
SigningRegion: signingRegion,
Endpoint: endpoint,
APIVersion: "2012-08-10",
JSONVersion: "1.0",
TargetPrefix: "DynamoDB_20120810",
},
handlers,
),
}
// Handlers
svc.Handlers.Sign.PushBackNamed(v4.SignRequestHandler)
svc.Handlers.Build.PushBackNamed(jsonrpc.BuildHandler)
svc.Handlers.Unmarshal.PushBackNamed(jsonrpc.UnmarshalHandler)
svc.Handlers.UnmarshalMeta.PushBackNamed(jsonrpc.UnmarshalMetaHandler)
svc.Handlers.UnmarshalError.PushBackNamed(jsonrpc.UnmarshalErrorHandler)
// Run custom client initialization if present
if initClient != nil {
initClient(svc.Client)
}
return svc
}
// newRequest creates a new request for a DynamoDB operation and runs any
// custom request initialization.
func (c *DynamoDB) newRequest(op *request.Operation, params, data interface{}) *request.Request {
req := c.NewRequest(op, params, data)
// Run custom request initialization if present
if initRequest != nil {
initRequest(req)
}
return req
}

View File

@@ -0,0 +1,107 @@
// Code generated by private/model/cli/gen-api/main.go. DO NOT EDIT.
package dynamodb
import (
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
)
// WaitUntilTableExists uses the DynamoDB API operation
// DescribeTable to wait for a condition to be met before returning.
// If the condition is not met within the max attempt window, an error will
// be returned.
func (c *DynamoDB) WaitUntilTableExists(input *DescribeTableInput) error {
return c.WaitUntilTableExistsWithContext(aws.BackgroundContext(), input)
}
// WaitUntilTableExistsWithContext is an extended version of WaitUntilTableExists.
// With the support for passing in a context and options to configure the
// Waiter and the underlying request options.
//
// The context must be non-nil and will be used for request cancellation. If
// the context is nil a panic will occur. In the future the SDK may create
// sub-contexts for http.Requests. See https://golang.org/pkg/context/
// for more information on using Contexts.
func (c *DynamoDB) WaitUntilTableExistsWithContext(ctx aws.Context, input *DescribeTableInput, opts ...request.WaiterOption) error {
w := request.Waiter{
Name: "WaitUntilTableExists",
MaxAttempts: 25,
Delay: request.ConstantWaiterDelay(20 * time.Second),
Acceptors: []request.WaiterAcceptor{
{
State: request.SuccessWaiterState,
Matcher: request.PathWaiterMatch, Argument: "Table.TableStatus",
Expected: "ACTIVE",
},
{
State: request.RetryWaiterState,
Matcher: request.ErrorWaiterMatch,
Expected: "ResourceNotFoundException",
},
},
Logger: c.Config.Logger,
NewRequest: func(opts []request.Option) (*request.Request, error) {
var inCpy *DescribeTableInput
if input != nil {
tmp := *input
inCpy = &tmp
}
req, _ := c.DescribeTableRequest(inCpy)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return req, nil
},
}
w.ApplyOptions(opts...)
return w.WaitWithContext(ctx)
}
// WaitUntilTableNotExists uses the DynamoDB API operation
// DescribeTable to wait for a condition to be met before returning.
// If the condition is not met within the max attempt window, an error will
// be returned.
func (c *DynamoDB) WaitUntilTableNotExists(input *DescribeTableInput) error {
return c.WaitUntilTableNotExistsWithContext(aws.BackgroundContext(), input)
}
// WaitUntilTableNotExistsWithContext is an extended version of WaitUntilTableNotExists.
// With the support for passing in a context and options to configure the
// Waiter and the underlying request options.
//
// The context must be non-nil and will be used for request cancellation. If
// the context is nil a panic will occur. In the future the SDK may create
// sub-contexts for http.Requests. See https://golang.org/pkg/context/
// for more information on using Contexts.
func (c *DynamoDB) WaitUntilTableNotExistsWithContext(ctx aws.Context, input *DescribeTableInput, opts ...request.WaiterOption) error {
w := request.Waiter{
Name: "WaitUntilTableNotExists",
MaxAttempts: 25,
Delay: request.ConstantWaiterDelay(20 * time.Second),
Acceptors: []request.WaiterAcceptor{
{
State: request.SuccessWaiterState,
Matcher: request.ErrorWaiterMatch,
Expected: "ResourceNotFoundException",
},
},
Logger: c.Config.Logger,
NewRequest: func(opts []request.Option) (*request.Request, error) {
var inCpy *DescribeTableInput
if input != nil {
tmp := *input
inCpy = &tmp
}
req, _ := c.DescribeTableRequest(inCpy)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return req, nil
},
}
w.ApplyOptions(opts...)
return w.WaitWithContext(ctx)
}