Skip to content

Commit 3271acb

Browse files
authored
Merge pull request #4 from caldwecr/add_update_expression_equivalence_matcher
Add update expression equivalence matcher
2 parents c191a27 + d7ffbd5 commit 3271acb

File tree

3 files changed

+489
-10
lines changed

3 files changed

+489
-10
lines changed

types.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@ type (
4343

4444
// UpdateItemExpectation struct hold expectation field, err, and result
4545
UpdateItemExpectation struct {
46-
attributeUpdates map[string]*dynamodb.AttributeValueUpdate
47-
key map[string]*dynamodb.AttributeValue
48-
table *string
49-
output *dynamodb.UpdateItemOutput
50-
conditionExpression *string
51-
expressionAttributeNames map[string]*string
52-
expressionAttributeValues map[string]*dynamodb.AttributeValue
53-
updateExpression *string
46+
attributeUpdates map[string]*dynamodb.AttributeValueUpdate
47+
key map[string]*dynamodb.AttributeValue
48+
table *string
49+
output *dynamodb.UpdateItemOutput
50+
conditionExpression *string
51+
expressionAttributeNames map[string]*string
52+
expressionAttributeValues map[string]*dynamodb.AttributeValue
53+
updateExpression *string
54+
equivalentUpdateExpression *parsedUpdateExpression
5455
}
5556

5657
// PutItemExpectation struct hold expectation field, err, and result

update_item.go

Lines changed: 211 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package dynamock
22

33
import (
44
"fmt"
5-
"reflect"
6-
75
"github.com/aws/aws-sdk-go/aws"
86
"github.com/aws/aws-sdk-go/aws/request"
97
"github.com/aws/aws-sdk-go/service/dynamodb"
8+
"reflect"
9+
"regexp"
10+
"sort"
11+
"strings"
1012
)
1113

1214
// ToTable - method for set Table expectation
@@ -45,6 +47,12 @@ func (e *UpdateItemExpectation) WithUpdateExpression(expr *string) *UpdateItemEx
4547
return e
4648
}
4749

50+
func (e *UpdateItemExpectation) WithEquivalentUpdateExpression(expr *string) *UpdateItemExpectation {
51+
parsed := parseUpdateExpression(*expr)
52+
e.equivalentUpdateExpression = &parsed
53+
return e
54+
}
55+
4856
// Updates - method for set Updates expectation
4957
func (e *UpdateItemExpectation) Updates(attrs map[string]*dynamodb.AttributeValueUpdate) *UpdateItemExpectation {
5058
e.attributeUpdates = attrs
@@ -104,6 +112,14 @@ func (e *MockDynamoDB) UpdateItem(input *dynamodb.UpdateItemInput) (*dynamodb.Up
104112
}
105113
}
106114

115+
if x.equivalentUpdateExpression != nil {
116+
inputExpr := parseUpdateExpression(*input.UpdateExpression)
117+
err := x.equivalentUpdateExpression.CheckIsEquivalentTo(&inputExpr)
118+
if err != nil {
119+
return &dynamodb.UpdateItemOutput{}, fmt.Errorf("non-equivalent update expressions found: %v", err)
120+
}
121+
}
122+
107123
// delete first element of expectation
108124
e.dynaMock.UpdateItemExpect = append(e.dynaMock.UpdateItemExpect[:0], e.dynaMock.UpdateItemExpect[1:]...)
109125

@@ -113,6 +129,192 @@ func (e *MockDynamoDB) UpdateItem(input *dynamodb.UpdateItemInput) (*dynamodb.Up
113129
return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Update Item Expectation Not Found")
114130
}
115131

132+
type parsedUpdateExpression struct {
133+
ADDExpressions []pathValueExpression
134+
DELETEExpressions []pathValueExpression
135+
REMOVEExpressions []pathExpression
136+
SETExpressions []pathValueExpression
137+
}
138+
139+
func (p *parsedUpdateExpression) CheckIsEquivalentTo(other *parsedUpdateExpression) error {
140+
sort.Slice(p.ADDExpressions, func(i, j int) bool {
141+
return p.ADDExpressions[i].path < p.ADDExpressions[j].path
142+
})
143+
sort.Slice(other.ADDExpressions, func(i, j int) bool {
144+
return other.ADDExpressions[i].path < other.ADDExpressions[j].path
145+
})
146+
if !reflect.DeepEqual(p.ADDExpressions, other.ADDExpressions) {
147+
return fmt.Errorf("ADDExpressions do not match, %v != %v", p.ADDExpressions, other.ADDExpressions)
148+
}
149+
sort.Slice(p.DELETEExpressions, func(i, j int) bool {
150+
return p.DELETEExpressions[i].path < p.DELETEExpressions[j].path
151+
})
152+
sort.Slice(other.DELETEExpressions, func(i, j int) bool {
153+
return other.DELETEExpressions[i].path < other.DELETEExpressions[j].path
154+
})
155+
if !reflect.DeepEqual(p.DELETEExpressions, other.DELETEExpressions) {
156+
return fmt.Errorf("DELETEExpressions do not match, %v != %v", p.DELETEExpressions, other.DELETEExpressions)
157+
}
158+
sort.Slice(p.REMOVEExpressions, func(i, j int) bool {
159+
return p.REMOVEExpressions[i].path < p.REMOVEExpressions[j].path
160+
})
161+
sort.Slice(other.REMOVEExpressions, func(i, j int) bool {
162+
return other.REMOVEExpressions[i].path < other.REMOVEExpressions[j].path
163+
})
164+
if !reflect.DeepEqual(p.REMOVEExpressions, other.REMOVEExpressions) {
165+
return fmt.Errorf("REMOVEExpressions do not match, %v != %v", p.REMOVEExpressions, other.REMOVEExpressions)
166+
}
167+
sort.Slice(p.SETExpressions, func(i, j int) bool {
168+
return p.SETExpressions[i].path < p.SETExpressions[j].path
169+
})
170+
sort.Slice(other.SETExpressions, func(i, j int) bool {
171+
return other.SETExpressions[i].path < other.SETExpressions[j].path
172+
})
173+
if !reflect.DeepEqual(p.SETExpressions, other.SETExpressions) {
174+
return fmt.Errorf("SETExpressions do not match, %v != %v", p.SETExpressions, other.SETExpressions)
175+
}
176+
return nil
177+
}
178+
179+
type operation string
180+
181+
const (
182+
ADD operation = "ADD"
183+
DELETE operation = "DELETE"
184+
REMOVE operation = "REMOVE"
185+
SET operation = "SET"
186+
)
187+
188+
type operationIndexTuple struct {
189+
Index int
190+
Operation operation
191+
}
192+
193+
type pathValueExpression struct {
194+
path string
195+
value string
196+
}
197+
198+
type pathExpression struct {
199+
path string
200+
}
201+
202+
func mustExtractPathValueExpressions(operation operation, expr string) []pathValueExpression {
203+
var re *regexp.Regexp
204+
var subMatchRe *regexp.Regexp
205+
var result []pathValueExpression
206+
switch operation {
207+
case ADD:
208+
re = regexp.MustCompile(`ADD\s+((\S+\s+[\w:#]+\s*,?\s*)+)`)
209+
subMatchRe = regexp.MustCompile(`(\S+)\s+([\w:#]+)\s*,?\s*`)
210+
case DELETE:
211+
re = regexp.MustCompile(`DELETE\s+((\S+\s+[\w:#]+\s*,?\s*)+)`)
212+
subMatchRe = regexp.MustCompile(`(\S+)\s+([\w:#]+)\s*,?\s*`)
213+
case SET:
214+
// SET operations allow the value to two operands with a + or - in between them
215+
// SET operations allow the operand to be a function such as `SET #ri = list_append(#ri, :vals)`
216+
re = regexp.MustCompile(`SET\s+((\S+\s*=\s*[\w:#\(\)\+-,\s=]+\s*,?\s*)+)`)
217+
subMatchRe = regexp.MustCompile(`(\S+)\s*=\s*([\w:#\+-]+(\([\w\s,:#]*\))?)\s*,?\s*`)
218+
}
219+
if re == nil {
220+
return result
221+
}
222+
if subMatchRe == nil {
223+
return result
224+
}
225+
226+
subMatches := re.FindStringSubmatch(expr)
227+
228+
if subMatches == nil {
229+
return result
230+
}
231+
232+
pairMatches := subMatchRe.FindAllStringSubmatch(subMatches[1], -1)
233+
if pairMatches == nil {
234+
return result
235+
}
236+
for _, subMatch := range pairMatches {
237+
result = append(result, pathValueExpression{subMatch[1], subMatch[2]})
238+
}
239+
return result
240+
}
241+
242+
func extractAddPathValuePairs(addExpr string) []pathValueExpression {
243+
return mustExtractPathValueExpressions(ADD, addExpr)
244+
}
245+
246+
func extractDeletePathValuePairs(deleteExpr string) []pathValueExpression {
247+
return mustExtractPathValueExpressions(DELETE, deleteExpr)
248+
}
249+
250+
func extractRemovePath(removeExpr string) []pathExpression {
251+
re := regexp.MustCompile(`REMOVE\s+(([\w:#\[\]]+\s*,?\s*)+)`)
252+
subMatchRe := regexp.MustCompile(`\s*([\w:#\[\]]+)\s*,?\s*`)
253+
subMatches := re.FindStringSubmatch(removeExpr)
254+
var result []pathExpression
255+
if subMatches == nil {
256+
return result
257+
}
258+
pairMatches := subMatchRe.FindAllStringSubmatch(subMatches[1], -1)
259+
if pairMatches == nil {
260+
return result
261+
}
262+
for _, subMatch := range pairMatches {
263+
result = append(result, pathExpression{subMatch[1]})
264+
}
265+
return result
266+
}
267+
268+
func extractSetPathValuePairs(setExpr string) []pathValueExpression {
269+
return mustExtractPathValueExpressions(SET, setExpr)
270+
}
271+
272+
func parseUpdateExpression(updateExpression string) parsedUpdateExpression {
273+
addOp := operationIndexTuple{strings.Index(updateExpression, "ADD"), ADD}
274+
deleteOp := operationIndexTuple{strings.Index(updateExpression, "DELETE"), DELETE}
275+
removeOp := operationIndexTuple{strings.Index(updateExpression, "REMOVE"), REMOVE}
276+
setOp := operationIndexTuple{strings.Index(updateExpression, "SET"), SET}
277+
278+
ops := []operationIndexTuple{
279+
addOp,
280+
deleteOp,
281+
removeOp,
282+
setOp,
283+
}
284+
sort.Slice(ops, func(i, j int) bool {
285+
return ops[i].Index < ops[j].Index
286+
})
287+
288+
result := parsedUpdateExpression{}
289+
for opIdx, op := range ops {
290+
if op.Index < 0 {
291+
// op.Index should be -1 for operations that are not present in an update expression
292+
continue
293+
}
294+
// get the substring for the operation
295+
var substr string
296+
if opIdx+1 < len(ops) {
297+
// We don't need to worry about the case where (opIdx+1).Index is -1, because we're iterating through a
298+
// ascending sorted array.
299+
substr = updateExpression[op.Index:ops[opIdx+1].Index]
300+
} else {
301+
substr = updateExpression[op.Index:]
302+
}
303+
// apply the operation specific parsing
304+
switch op.Operation {
305+
case ADD:
306+
result.ADDExpressions = extractAddPathValuePairs(substr)
307+
case DELETE:
308+
result.DELETEExpressions = extractDeletePathValuePairs(substr)
309+
case REMOVE:
310+
result.REMOVEExpressions = extractRemovePath(substr)
311+
case SET:
312+
result.SETExpressions = extractSetPathValuePairs(substr)
313+
}
314+
}
315+
return result
316+
}
317+
116318
// UpdateItemWithContext - this func will be invoked when test running matching expectation with actual input
117319
func (e *MockDynamoDB) UpdateItemWithContext(ctx aws.Context, input *dynamodb.UpdateItemInput, opt ...request.Option) (*dynamodb.UpdateItemOutput, error) {
118320
if len(e.dynaMock.UpdateItemExpect) > 0 {
@@ -159,6 +361,13 @@ func (e *MockDynamoDB) UpdateItemWithContext(ctx aws.Context, input *dynamodb.Up
159361
return &dynamodb.UpdateItemOutput{}, fmt.Errorf("Expect key %+v but found key %+v", x.updateExpression, input.UpdateExpression)
160362
}
161363
}
364+
if x.equivalentUpdateExpression != nil {
365+
inputExpr := parseUpdateExpression(*input.UpdateExpression)
366+
err := x.equivalentUpdateExpression.CheckIsEquivalentTo(&inputExpr)
367+
if err != nil {
368+
return &dynamodb.UpdateItemOutput{}, fmt.Errorf("non-equivalent update expressions found: %v", err)
369+
}
370+
}
162371
// delete first element of expectation
163372
e.dynaMock.UpdateItemExpect = append(e.dynaMock.UpdateItemExpect[:0], e.dynaMock.UpdateItemExpect[1:]...)
164373

0 commit comments

Comments
 (0)