1package expression
2
3import (
4	"fmt"
5	"sort"
6
7	"github.com/aws/aws-sdk-go/aws"
8	"github.com/aws/aws-sdk-go/service/dynamodb"
9)
10
11// expressionType specifies the type of Expression. Declaring this type is used
12// to eliminate magic strings
13type expressionType string
14
15const (
16	projection   expressionType = "projection"
17	keyCondition                = "keyCondition"
18	condition                   = "condition"
19	filter                      = "filter"
20	update                      = "update"
21)
22
23// Implement the Sort interface
24type typeList []expressionType
25
26func (l typeList) Len() int {
27	return len(l)
28}
29
30func (l typeList) Less(i, j int) bool {
31	return string(l[i]) < string(l[j])
32}
33
34func (l typeList) Swap(i, j int) {
35	l[i], l[j] = l[j], l[i]
36}
37
38// Builder represents the struct that builds the Expression struct. Methods such
39// as WithProjection() and WithCondition() can add different kinds of DynamoDB
40// Expressions to the Builder. The method Build() creates an Expression struct
41// with the specified types of DynamoDB Expressions.
42//
43// Example:
44//
45//     keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
46//     proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
47//
48//     builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
49//     expr := builder.Build()
50//
51//     queryInput := dynamodb.QueryInput{
52//       KeyConditionExpression:    expr.KeyCondition(),
53//       ProjectionExpression:      expr.Projection(),
54//       ExpressionAttributeNames:  expr.Names(),
55//       ExpressionAttributeValues: expr.Values(),
56//       TableName: aws.String("SomeTable"),
57//     }
58type Builder struct {
59	expressionMap map[expressionType]treeBuilder
60}
61
62// NewBuilder returns an empty Builder struct. Methods such as WithProjection()
63// and WithCondition() can add different kinds of DynamoDB Expressions to the
64// Builder. The method Build() creates an Expression struct with the specified
65// types of DynamoDB Expressions.
66//
67// Example:
68//
69//     keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
70//     proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
71//     builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
72func NewBuilder() Builder {
73	return Builder{}
74}
75
76// Build builds an Expression struct representing multiple types of DynamoDB
77// Expressions. Getter methods on the resulting Expression struct returns the
78// DynamoDB Expression strings as well as the maps that correspond to
79// ExpressionAttributeNames and ExpressionAttributeValues. Calling Build() on an
80// empty Builder returns the typed error EmptyParameterError.
81//
82// Example:
83//
84//     // keyCond represents the Key Condition Expression
85//     keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
86//     // proj represents the Projection Expression
87//     proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
88//
89//     // Add keyCond and proj to builder as a Key Condition and Projection
90//     // respectively
91//     builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
92//     expr := builder.Build()
93//
94//     queryInput := dynamodb.QueryInput{
95//       KeyConditionExpression:    expr.KeyCondition(),
96//       ProjectionExpression:      expr.Projection(),
97//       ExpressionAttributeNames:  expr.Names(),
98//       ExpressionAttributeValues: expr.Values(),
99//       TableName: aws.String("SomeTable"),
100//     }
101func (b Builder) Build() (Expression, error) {
102	if b.expressionMap == nil {
103		return Expression{}, newUnsetParameterError("Build", "Builder")
104	}
105
106	aliasList, expressionMap, err := b.buildChildTrees()
107	if err != nil {
108		return Expression{}, err
109	}
110
111	expression := Expression{
112		expressionMap: expressionMap,
113	}
114
115	if len(aliasList.namesList) != 0 {
116		namesMap := map[string]*string{}
117		for ind, val := range aliasList.namesList {
118			namesMap[fmt.Sprintf("#%v", ind)] = aws.String(val)
119		}
120		expression.namesMap = namesMap
121	}
122
123	if len(aliasList.valuesList) != 0 {
124		valuesMap := map[string]*dynamodb.AttributeValue{}
125		for i := 0; i < len(aliasList.valuesList); i++ {
126			valuesMap[fmt.Sprintf(":%v", i)] = &aliasList.valuesList[i]
127		}
128		expression.valuesMap = valuesMap
129	}
130
131	return expression, nil
132}
133
134// buildChildTrees compiles the list of treeBuilders that are the children of
135// the argument Builder. The returned aliasList represents all the alias tokens
136// used in the expression strings. The returned map[string]string maps the type
137// of expression (i.e. "condition", "update") to the appropriate expression
138// string.
139func (b Builder) buildChildTrees() (aliasList, map[expressionType]string, error) {
140	aList := aliasList{}
141	formattedExpressions := map[expressionType]string{}
142	keys := typeList{}
143
144	for expressionType := range b.expressionMap {
145		keys = append(keys, expressionType)
146	}
147
148	sort.Sort(keys)
149
150	for _, key := range keys {
151		node, err := b.expressionMap[key].buildTree()
152		if err != nil {
153			return aliasList{}, nil, err
154		}
155		formattedExpression, err := node.buildExpressionString(&aList)
156		if err != nil {
157			return aliasList{}, nil, err
158		}
159		formattedExpressions[key] = formattedExpression
160	}
161
162	return aList, formattedExpressions, nil
163}
164
165// WithCondition method adds the argument ConditionBuilder as a Condition
166// Expression to the argument Builder. If the argument Builder already has a
167// ConditionBuilder representing a Condition Expression, WithCondition()
168// overwrites the existing ConditionBuilder.
169//
170// Example:
171//
172//     // let builder be an existing Builder{} and cond be an existing
173//     // ConditionBuilder{}
174//     builder = builder.WithCondition(cond)
175//
176//     // add other DynamoDB Expressions to the builder. let proj be an already
177//     // existing ProjectionBuilder
178//     builder = builder.WithProjection(proj)
179//     // create an Expression struct
180//     expr := builder.Build()
181func (b Builder) WithCondition(conditionBuilder ConditionBuilder) Builder {
182	if b.expressionMap == nil {
183		b.expressionMap = map[expressionType]treeBuilder{}
184	}
185	b.expressionMap[condition] = conditionBuilder
186	return b
187}
188
189// WithProjection method adds the argument ProjectionBuilder as a Projection
190// Expression to the argument Builder. If the argument Builder already has a
191// ProjectionBuilder representing a Projection Expression, WithProjection()
192// overwrites the existing ProjectionBuilder.
193//
194// Example:
195//
196//     // let builder be an existing Builder{} and proj be an existing
197//     // ProjectionBuilder{}
198//     builder = builder.WithProjection(proj)
199//
200//     // add other DynamoDB Expressions to the builder. let cond be an already
201//     // existing ConditionBuilder
202//     builder = builder.WithCondition(cond)
203//     // create an Expression struct
204//     expr := builder.Build()
205func (b Builder) WithProjection(projectionBuilder ProjectionBuilder) Builder {
206	if b.expressionMap == nil {
207		b.expressionMap = map[expressionType]treeBuilder{}
208	}
209	b.expressionMap[projection] = projectionBuilder
210	return b
211}
212
213// WithKeyCondition method adds the argument KeyConditionBuilder as a Key
214// Condition Expression to the argument Builder. If the argument Builder already
215// has a KeyConditionBuilder representing a Key Condition Expression,
216// WithKeyCondition() overwrites the existing KeyConditionBuilder.
217//
218// Example:
219//
220//     // let builder be an existing Builder{} and keyCond be an existing
221//     // KeyConditionBuilder{}
222//     builder = builder.WithKeyCondition(keyCond)
223//
224//     // add other DynamoDB Expressions to the builder. let cond be an already
225//     // existing ConditionBuilder
226//     builder = builder.WithCondition(cond)
227//     // create an Expression struct
228//     expr := builder.Build()
229func (b Builder) WithKeyCondition(keyConditionBuilder KeyConditionBuilder) Builder {
230	if b.expressionMap == nil {
231		b.expressionMap = map[expressionType]treeBuilder{}
232	}
233	b.expressionMap[keyCondition] = keyConditionBuilder
234	return b
235}
236
237// WithFilter method adds the argument ConditionBuilder as a Filter Expression
238// to the argument Builder. If the argument Builder already has a
239// ConditionBuilder representing a Filter Expression, WithFilter()
240// overwrites the existing ConditionBuilder.
241//
242// Example:
243//
244//     // let builder be an existing Builder{} and filt be an existing
245//     // ConditionBuilder{}
246//     builder = builder.WithFilter(filt)
247//
248//     // add other DynamoDB Expressions to the builder. let cond be an already
249//     // existing ConditionBuilder
250//     builder = builder.WithCondition(cond)
251//     // create an Expression struct
252//     expr := builder.Build()
253func (b Builder) WithFilter(filterBuilder ConditionBuilder) Builder {
254	if b.expressionMap == nil {
255		b.expressionMap = map[expressionType]treeBuilder{}
256	}
257	b.expressionMap[filter] = filterBuilder
258	return b
259}
260
261// WithUpdate method adds the argument UpdateBuilder as an Update Expression
262// to the argument Builder. If the argument Builder already has a UpdateBuilder
263// representing a Update Expression, WithUpdate() overwrites the existing
264// UpdateBuilder.
265//
266// Example:
267//
268//     // let builder be an existing Builder{} and update be an existing
269//     // UpdateBuilder{}
270//     builder = builder.WithUpdate(update)
271//
272//     // add other DynamoDB Expressions to the builder. let cond be an already
273//     // existing ConditionBuilder
274//     builder = builder.WithCondition(cond)
275//     // create an Expression struct
276//     expr := builder.Build()
277func (b Builder) WithUpdate(updateBuilder UpdateBuilder) Builder {
278	if b.expressionMap == nil {
279		b.expressionMap = map[expressionType]treeBuilder{}
280	}
281	b.expressionMap[update] = updateBuilder
282	return b
283}
284
285// Expression represents a collection of DynamoDB Expressions. The getter
286// methods of the Expression struct retrieves the formatted DynamoDB
287// Expressions, ExpressionAttributeNames, and ExpressionAttributeValues.
288//
289// Example:
290//
291//     // keyCond represents the Key Condition Expression
292//     keyCond := expression.Key("someKey").Equal(expression.Value("someValue"))
293//     // proj represents the Projection Expression
294//     proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName"))
295//
296//     // Add keyCond and proj to builder as a Key Condition and Projection
297//     // respectively
298//     builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj)
299//     expr := builder.Build()
300//
301//     queryInput := dynamodb.QueryInput{
302//       KeyConditionExpression:    expr.KeyCondition(),
303//       ProjectionExpression:      expr.Projection(),
304//       ExpressionAttributeNames:  expr.Names(),
305//       ExpressionAttributeValues: expr.Values(),
306//       TableName: aws.String("SomeTable"),
307//     }
308type Expression struct {
309	expressionMap map[expressionType]string
310	namesMap      map[string]*string
311	valuesMap     map[string]*dynamodb.AttributeValue
312}
313
314// treeBuilder interface is fulfilled by builder structs that represent
315// different types of Expressions.
316type treeBuilder interface {
317	// buildTree creates the tree structure of exprNodes. The tree structure
318	// of exprNodes are traversed in order to build the string representing
319	// different types of Expressions as well as the maps that represent
320	// ExpressionAttributeNames and ExpressionAttributeValues.
321	buildTree() (exprNode, error)
322}
323
324// Condition returns the *string corresponding to the Condition Expression
325// of the argument Expression. This method is used to satisfy the members of
326// DynamoDB input structs. If the Expression does not have a condition
327// expression this method returns nil.
328//
329// Example:
330//
331//     // let expression be an instance of Expression{}
332//
333//     deleteInput := dynamodb.DeleteItemInput{
334//       ConditionExpression:       expression.Condition(),
335//       ExpressionAttributeNames:  expression.Names(),
336//       ExpressionAttributeValues: expression.Values(),
337//       Key: map[string]*dynamodb.AttributeValue{
338//         "PartitionKey": &dynamodb.AttributeValue{
339//           S: aws.String("SomeKey"),
340//         },
341//       },
342//       TableName: aws.String("SomeTable"),
343//     }
344func (e Expression) Condition() *string {
345	return e.returnExpression(condition)
346}
347
348// Filter returns the *string corresponding to the Filter Expression of the
349// argument Expression. This method is used to satisfy the members of DynamoDB
350// input structs. If the Expression does not have a filter expression this
351// method returns nil.
352//
353// Example:
354//
355//     // let expression be an instance of Expression{}
356//
357//     queryInput := dynamodb.QueryInput{
358//       KeyConditionExpression:    expression.KeyCondition(),
359//       FilterExpression:          expression.Filter(),
360//       ExpressionAttributeNames:  expression.Names(),
361//       ExpressionAttributeValues: expression.Values(),
362//       TableName: aws.String("SomeTable"),
363//     }
364func (e Expression) Filter() *string {
365	return e.returnExpression(filter)
366}
367
368// Projection returns the *string corresponding to the Projection Expression
369// of the argument Expression. This method is used to satisfy the members of
370// DynamoDB input structs. If the Expression does not have a projection
371// expression this method returns nil.
372//
373// Example:
374//
375//     // let expression be an instance of Expression{}
376//
377//     queryInput := dynamodb.QueryInput{
378//       KeyConditionExpression:    expression.KeyCondition(),
379//       ProjectionExpression:      expression.Projection(),
380//       ExpressionAttributeNames:  expression.Names(),
381//       ExpressionAttributeValues: expression.Values(),
382//       TableName: aws.String("SomeTable"),
383//     }
384func (e Expression) Projection() *string {
385	return e.returnExpression(projection)
386}
387
388// KeyCondition returns the *string corresponding to the Key Condition
389// Expression of the argument Expression. This method is used to satisfy the
390// members of DynamoDB input structs. If the argument Expression does not have a
391// KeyConditionExpression, KeyCondition() returns nil.
392//
393// Example:
394//
395//     // let expression be an instance of Expression{}
396//
397//     queryInput := dynamodb.QueryInput{
398//       KeyConditionExpression:    expression.KeyCondition(),
399//       ProjectionExpression:      expression.Projection(),
400//       ExpressionAttributeNames:  expression.Names(),
401//       ExpressionAttributeValues: expression.Values(),
402//       TableName: aws.String("SomeTable"),
403//     }
404func (e Expression) KeyCondition() *string {
405	return e.returnExpression(keyCondition)
406}
407
408// Update returns the *string corresponding to the Update Expression of the
409// argument Expression. This method is used to satisfy the members of DynamoDB
410// input structs. If the argument Expression does not have a UpdateExpression,
411// Update() returns nil.
412//
413// Example:
414//
415//     // let expression be an instance of Expression{}
416//
417//     updateInput := dynamodb.UpdateInput{
418//       Key: map[string]*dynamodb.AttributeValue{
419//         "PartitionKey": {
420//           S: aws.String("someKey"),
421//         },
422//       },
423//       UpdateExpression:          expression.Update(),
424//       ExpressionAttributeNames:  expression.Names(),
425//       ExpressionAttributeValues: expression.Values(),
426//       TableName: aws.String("SomeTable"),
427//     }
428func (e Expression) Update() *string {
429	return e.returnExpression(update)
430}
431
432// Names returns the map[string]*string corresponding to the
433// ExpressionAttributeNames of the argument Expression. This method is used to
434// satisfy the members of DynamoDB input structs. If Expression does not use
435// ExpressionAttributeNames, this method returns nil. The
436// ExpressionAttributeNames and ExpressionAttributeValues member of the input
437// struct must always be assigned when using the Expression struct since all
438// item attribute names and values are aliased. That means that if the
439// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned
440// with the corresponding Names() and Values() methods, the DynamoDB operation
441// will run into a logic error.
442//
443// Example:
444//
445//     // let expression be an instance of Expression{}
446//
447//     queryInput := dynamodb.QueryInput{
448//       KeyConditionExpression:    expression.KeyCondition(),
449//       ProjectionExpression:      expression.Projection(),
450//       ExpressionAttributeNames:  expression.Names(),
451//       ExpressionAttributeValues: expression.Values(),
452//       TableName: aws.String("SomeTable"),
453//     }
454func (e Expression) Names() map[string]*string {
455	return e.namesMap
456}
457
458// Values returns the map[string]*dynamodb.AttributeValue corresponding to
459// the ExpressionAttributeValues of the argument Expression. This method is used
460// to satisfy the members of DynamoDB input structs. If Expression does not use
461// ExpressionAttributeValues, this method returns nil. The
462// ExpressionAttributeNames and ExpressionAttributeValues member of the input
463// struct must always be assigned when using the Expression struct since all
464// item attribute names and values are aliased. That means that if the
465// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned
466// with the corresponding Names() and Values() methods, the DynamoDB operation
467// will run into a logic error.
468//
469// Example:
470//
471//     // let expression be an instance of Expression{}
472//
473//     queryInput := dynamodb.QueryInput{
474//       KeyConditionExpression:    expression.KeyCondition(),
475//       ProjectionExpression:      expression.Projection(),
476//       ExpressionAttributeNames:  expression.Names(),
477//       ExpressionAttributeValues: expression.Values(),
478//       TableName: aws.String("SomeTable"),
479//     }
480func (e Expression) Values() map[string]*dynamodb.AttributeValue {
481	return e.valuesMap
482}
483
484// returnExpression returns *string corresponding to the type of Expression
485// string specified by the expressionType. If there is no corresponding
486// expression available in Expression, the method returns nil
487func (e Expression) returnExpression(expressionType expressionType) *string {
488	if e.expressionMap == nil {
489		return nil
490	}
491	if s, exists := e.expressionMap[expressionType]; exists {
492		return &s
493	}
494	return nil
495}
496
497// exprNode are the generic nodes that represents both Operands and
498// Conditions. The purpose of exprNode is to be able to call an generic
499// recursive function on the top level exprNode to be able to determine a root
500// node in order to deduplicate name aliases.
501// fmtExpr is a string that has escaped characters to refer to
502// names/values/children which needs to be aliased at runtime in order to avoid
503// duplicate values. The rules are as follows:
504//     $n: Indicates that an alias of a name needs to be inserted. The
505//         corresponding name to be alias is in the []names slice.
506//     $v: Indicates that an alias of a value needs to be inserted. The
507//         corresponding value to be alias is in the []values slice.
508//     $c: Indicates that the fmtExpr of a child exprNode needs to be inserted.
509//         The corresponding child node is in the []children slice.
510type exprNode struct {
511	names    []string
512	values   []dynamodb.AttributeValue
513	children []exprNode
514	fmtExpr  string
515}
516
517// aliasList keeps track of all the names we need to alias in the nested
518// struct of conditions and operands. This allows each alias to be unique.
519// aliasList is passed in as a pointer when buildChildTrees is called in
520// order to deduplicate all names within the tree strcuture of the exprNodes.
521type aliasList struct {
522	namesList  []string
523	valuesList []dynamodb.AttributeValue
524}
525
526// buildExpressionString returns a string with aliasing for names/values
527// specified by aliasList. The string corresponds to the expression that the
528// exprNode tree represents.
529func (en exprNode) buildExpressionString(aliasList *aliasList) (string, error) {
530	// Since each exprNode contains a slice of names, values, and children that
531	// correspond to the escaped characters, we an index to traverse the slices
532	index := struct {
533		name, value, children int
534	}{}
535
536	formattedExpression := en.fmtExpr
537
538	for i := 0; i < len(formattedExpression); {
539		if formattedExpression[i] != '$' {
540			i++
541			continue
542		}
543
544		if i == len(formattedExpression)-1 {
545			return "", fmt.Errorf("buildexprNode error: invalid escape character")
546		}
547
548		var alias string
549		var err error
550		// if an escaped character is found, substitute it with the proper alias
551		// TODO consider AST instead of string in the future
552		switch formattedExpression[i+1] {
553		case 'n':
554			alias, err = substitutePath(index.name, en, aliasList)
555			if err != nil {
556				return "", err
557			}
558			index.name++
559
560		case 'v':
561			alias, err = substituteValue(index.value, en, aliasList)
562			if err != nil {
563				return "", err
564			}
565			index.value++
566
567		case 'c':
568			alias, err = substituteChild(index.children, en, aliasList)
569			if err != nil {
570				return "", err
571			}
572			index.children++
573
574		default:
575			return "", fmt.Errorf("buildexprNode error: invalid escape rune %#v", formattedExpression[i+1])
576		}
577		formattedExpression = formattedExpression[:i] + alias + formattedExpression[i+2:]
578		i += len(alias)
579	}
580
581	return formattedExpression, nil
582}
583
584// substitutePath substitutes the escaped character $n with the appropriate
585// alias.
586func substitutePath(index int, node exprNode, aliasList *aliasList) (string, error) {
587	if index >= len(node.names) {
588		return "", fmt.Errorf("substitutePath error: exprNode []names out of range")
589	}
590	str, err := aliasList.aliasPath(node.names[index])
591	if err != nil {
592		return "", err
593	}
594	return str, nil
595}
596
597// substituteValue substitutes the escaped character $v with the appropriate
598// alias.
599func substituteValue(index int, node exprNode, aliasList *aliasList) (string, error) {
600	if index >= len(node.values) {
601		return "", fmt.Errorf("substituteValue error: exprNode []values out of range")
602	}
603	str, err := aliasList.aliasValue(node.values[index])
604	if err != nil {
605		return "", err
606	}
607	return str, nil
608}
609
610// substituteChild substitutes the escaped character $c with the appropriate
611// alias.
612func substituteChild(index int, node exprNode, aliasList *aliasList) (string, error) {
613	if index >= len(node.children) {
614		return "", fmt.Errorf("substituteChild error: exprNode []children out of range")
615	}
616	return node.children[index].buildExpressionString(aliasList)
617}
618
619// aliasValue returns the corresponding alias to the dav value argument. Since
620// values are not deduplicated as of now, all values are just appended to the
621// aliasList and given the index as the alias.
622func (al *aliasList) aliasValue(dav dynamodb.AttributeValue) (string, error) {
623	al.valuesList = append(al.valuesList, dav)
624	return fmt.Sprintf(":%d", len(al.valuesList)-1), nil
625}
626
627// aliasPath returns the corresponding alias to the argument string. The
628// argument is checked against all existing aliasList names in order to avoid
629// duplicate strings getting two different aliases.
630func (al *aliasList) aliasPath(nm string) (string, error) {
631	for ind, name := range al.namesList {
632		if nm == name {
633			return fmt.Sprintf("#%d", ind), nil
634		}
635	}
636	al.namesList = append(al.namesList, nm)
637	return fmt.Sprintf("#%d", len(al.namesList)-1), nil
638}
639