vendor: update all dependencies to latest versions

This commit is contained in:
Nick Craig-Wood 2017-09-30 15:27:27 +01:00
parent 911d121bb9
commit b017fcfe9a
3048 changed files with 537057 additions and 189681 deletions

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)
}
}
})
}
}