github.com/aavshr/aws-sdk-go@v1.41.3/service/dynamodb/expression/expression.go (about) 1 package expression 2 3 import ( 4 "fmt" 5 "sort" 6 7 "github.com/aavshr/aws-sdk-go/aws" 8 "github.com/aavshr/aws-sdk-go/service/dynamodb" 9 ) 10 11 // expressionType specifies the type of Expression. Declaring this type is used 12 // to eliminate magic strings 13 type expressionType string 14 15 const ( 16 projection expressionType = "projection" 17 keyCondition = "keyCondition" 18 condition = "condition" 19 filter = "filter" 20 update = "update" 21 ) 22 23 // Implement the Sort interface 24 type typeList []expressionType 25 26 func (l typeList) Len() int { 27 return len(l) 28 } 29 30 func (l typeList) Less(i, j int) bool { 31 return string(l[i]) < string(l[j]) 32 } 33 34 func (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 // } 58 type 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) 72 func 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 // } 101 func (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. 139 func (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() 181 func (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() 205 func (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() 229 func (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() 253 func (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() 277 func (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 // } 308 type 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. 316 type 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 // } 344 func (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 // } 364 func (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 // } 384 func (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 // } 404 func (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 // } 428 func (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 // } 454 func (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 // } 480 func (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 487 func (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. 510 type 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. 521 type 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. 529 func (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. 586 func 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. 599 func 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. 612 func 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. 622 func (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. 630 func (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 }