Skip to content

Commit 700d3b7

Browse files
authored
Merge pull request #38 from hashicorp/not-pre-disp
missing map key match operator dispositions
2 parents 4f30fe1 + be73ea1 commit 700d3b7

File tree

3 files changed

+85
-4
lines changed

3 files changed

+85
-4
lines changed

evaluate.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,27 @@ func getMatchExprValue(expression *grammar.MatchExpression, rvalue reflect.Kind)
213213
}
214214
}
215215

216+
// evaluateNotPresent is called after a pointerstructure.ErrNotFound is
217+
// encountered during evaluation.
218+
//
219+
// Returns true if the Selector Path's parent is a map as the missing key may
220+
// be handled by the MatchOperator's NotPresentDisposition method.
221+
//
222+
// Returns false if the Selector Path has a length of 1, or if the parent of
223+
// the Selector's Path is not a map, a pointerstructure.ErrrNotFound error is
224+
// returned.
225+
func evaluateNotPresent(ptr pointerstructure.Pointer, datum interface{}) bool {
226+
if len(ptr.Parts) < 2 {
227+
return false
228+
}
229+
230+
// Pop the missing leaf part of the path
231+
ptr.Parts = ptr.Parts[0 : len(ptr.Parts)-1]
232+
233+
val, _ := ptr.Get(datum)
234+
return reflect.ValueOf(val).Kind() == reflect.Map
235+
}
236+
216237
func evaluateMatchExpression(expression *grammar.MatchExpression, datum interface{}, opt ...Option) (bool, error) {
217238
opts := getOpts(opt...)
218239
ptr := pointerstructure.Pointer{
@@ -224,9 +245,16 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac
224245
}
225246
val, err := ptr.Get(datum)
226247
if err != nil {
227-
if errors.Is(err, pointerstructure.ErrNotFound) && opts.withUnknown != nil {
228-
err = nil
229-
val = *opts.withUnknown
248+
if errors.Is(err, pointerstructure.ErrNotFound) {
249+
// Prefer the withUnknown option if set, otherwise defer to NotPresent
250+
// disposition
251+
switch {
252+
case opts.withUnknown != nil:
253+
err = nil
254+
val = *opts.withUnknown
255+
case evaluateNotPresent(ptr, datum):
256+
return expression.Operator.NotPresentDisposition(), nil
257+
}
230258
}
231259

232260
if err != nil {

evaluate_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,30 @@ var evaluateTests map[string]expressionTest = map[string]expressionTest{
302302
{expression: "Nested.MapOfStructs is empty or (Nested.SliceOfInts contains 7 and 9 in Nested.SliceOfInts)", result: true, benchQuick: true},
303303
{expression: "Nested.SliceOfStructs.0.X == 1", result: true},
304304
{expression: "Nested.SliceOfStructs.0.Y == 4", result: false},
305-
{expression: "Nested.Map.notfound == 4", result: false, err: `error finding value in datum: /Nested/Map/notfound at part 2: couldn't find key "notfound"`},
306305
{expression: "Map in Nested", result: false, err: "Cannot perform in/contains operations on type struct for selector: \"Nested\""},
307306
{expression: `"foobar" in "/Nested/SliceOfInfs"`, result: true},
308307
{expression: `"1" in "/Nested/SliceOfInfs"`, result: true},
309308
{expression: `"2" in "/Nested/SliceOfInfs"`, result: false},
310309
{expression: `"true" in "/Nested/SliceOfInfs"`, result: true},
311310
{expression: `"/Nested/Map/email" matches "(foz|foo)@example.com"`, result: true},
311+
// Missing key in map tests
312+
{expression: "Nested.Map.notfound == 4", result: false},
313+
{expression: "Nested.Map.notfound != 4", result: true},
314+
{expression: "4 in Nested.Map.notfound", result: false},
315+
{expression: "4 not in Nested.Map.notfound", result: true},
316+
{expression: "Nested.Map.notfound is empty", result: true},
317+
{expression: "Nested.Map.notfound is not empty", result: false},
318+
{expression: `Nested.Map.notfound matches ".*"`, result: false},
319+
{expression: `Nested.Map.notfound not matches ".*"`, result: true},
320+
// Missing field in struct tests
321+
{expression: "Nested.Notfound == 4", result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
322+
{expression: "Nested.Notfound != 4", result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
323+
{expression: "4 in Nested.Notfound", result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
324+
{expression: "4 not in Nested.Notfound", result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
325+
{expression: "Nested.Notfound is empty", result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
326+
{expression: "Nested.Notfound is not empty", result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
327+
{expression: `Nested.Notfound matches ".*"`, result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
328+
{expression: `Nested.Notfound not matches ".*"`, result: false, err: `error finding value in datum: /Nested/Notfound at part 1: couldn't find key: struct field with name "Notfound"`},
312329
},
313330
},
314331
}

grammar/ast.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,42 @@ func (op MatchOperator) String() string {
8181
}
8282
}
8383

84+
// NotPresentDisposition is called during evaluation when Selector fails to
85+
// find a map key to determine the operator's behavior.
86+
func (op MatchOperator) NotPresentDisposition() bool {
87+
// For a selector M["x"] against a map M that lacks an "x" key...
88+
switch op {
89+
case MatchEqual:
90+
// ...M["x"] == <anything> is false. Nothing is equal to a missing key
91+
return false
92+
case MatchNotEqual:
93+
// ...M["x"] != <anything> is true. Nothing is equal to a missing key
94+
return true
95+
case MatchIn:
96+
// "a" in M["x"] is false. Missing keys contain no values
97+
return false
98+
case MatchNotIn:
99+
// "a" not in M["x"] is true. Missing keys contain no values
100+
return true
101+
case MatchIsEmpty:
102+
// M["x"] is empty is true. Missing keys contain no values
103+
return true
104+
case MatchIsNotEmpty:
105+
// M["x"] is not empty is false. Missing keys contain no values
106+
return false
107+
case MatchMatches:
108+
// M["x"] matches <anything> is false. Nothing matches a missing key
109+
return false
110+
case MatchNotMatches:
111+
// M["x"] not matches <anything> is true. Nothing matches a missing key
112+
return true
113+
default:
114+
// Should never be reached as every operator should explicitly define its
115+
// behavior.
116+
return false
117+
}
118+
}
119+
84120
type MatchValue struct {
85121
Raw string
86122
Converted interface{}

0 commit comments

Comments
 (0)