@@ -2,11 +2,13 @@ package dynamock
22
33import  (
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 
4957func  (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 
117319func  (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