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