Skip to content

Commit

Permalink
Merge pull request #38 from hashicorp/not-pre-disp
Browse files Browse the repository at this point in the history
missing map key match operator dispositions
  • Loading branch information
schmichael authored Apr 26, 2023
2 parents 4f30fe1 + be73ea1 commit 700d3b7
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 4 deletions.
34 changes: 31 additions & 3 deletions evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,27 @@ func getMatchExprValue(expression *grammar.MatchExpression, rvalue reflect.Kind)
}
}

// evaluateNotPresent is called after a pointerstructure.ErrNotFound is
// encountered during evaluation.
//
// Returns true if the Selector Path's parent is a map as the missing key may
// be handled by the MatchOperator's NotPresentDisposition method.
//
// Returns false if the Selector Path has a length of 1, or if the parent of
// the Selector's Path is not a map, a pointerstructure.ErrrNotFound error is
// returned.
func evaluateNotPresent(ptr pointerstructure.Pointer, datum interface{}) bool {
if len(ptr.Parts) < 2 {
return false
}

// Pop the missing leaf part of the path
ptr.Parts = ptr.Parts[0 : len(ptr.Parts)-1]

val, _ := ptr.Get(datum)
return reflect.ValueOf(val).Kind() == reflect.Map
}

func evaluateMatchExpression(expression *grammar.MatchExpression, datum interface{}, opt ...Option) (bool, error) {
opts := getOpts(opt...)
ptr := pointerstructure.Pointer{
Expand All @@ -224,9 +245,16 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac
}
val, err := ptr.Get(datum)
if err != nil {
if errors.Is(err, pointerstructure.ErrNotFound) && opts.withUnknown != nil {
err = nil
val = *opts.withUnknown
if errors.Is(err, pointerstructure.ErrNotFound) {
// Prefer the withUnknown option if set, otherwise defer to NotPresent
// disposition
switch {
case opts.withUnknown != nil:
err = nil
val = *opts.withUnknown
case evaluateNotPresent(ptr, datum):
return expression.Operator.NotPresentDisposition(), nil
}
}

if err != nil {
Expand Down
19 changes: 18 additions & 1 deletion evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,30 @@ var evaluateTests map[string]expressionTest = map[string]expressionTest{
{expression: "Nested.MapOfStructs is empty or (Nested.SliceOfInts contains 7 and 9 in Nested.SliceOfInts)", result: true, benchQuick: true},
{expression: "Nested.SliceOfStructs.0.X == 1", result: true},
{expression: "Nested.SliceOfStructs.0.Y == 4", result: false},
{expression: "Nested.Map.notfound == 4", result: false, err: `error finding value in datum: /Nested/Map/notfound at part 2: couldn't find key "notfound"`},
{expression: "Map in Nested", result: false, err: "Cannot perform in/contains operations on type struct for selector: \"Nested\""},
{expression: `"foobar" in "/Nested/SliceOfInfs"`, result: true},
{expression: `"1" in "/Nested/SliceOfInfs"`, result: true},
{expression: `"2" in "/Nested/SliceOfInfs"`, result: false},
{expression: `"true" in "/Nested/SliceOfInfs"`, result: true},
{expression: `"/Nested/Map/email" matches "(foz|foo)@example.com"`, result: true},
// Missing key in map tests
{expression: "Nested.Map.notfound == 4", result: false},
{expression: "Nested.Map.notfound != 4", result: true},
{expression: "4 in Nested.Map.notfound", result: false},
{expression: "4 not in Nested.Map.notfound", result: true},
{expression: "Nested.Map.notfound is empty", result: true},
{expression: "Nested.Map.notfound is not empty", result: false},
{expression: `Nested.Map.notfound matches ".*"`, result: false},
{expression: `Nested.Map.notfound not matches ".*"`, result: true},
// Missing field in struct tests
{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"`},
{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"`},
{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"`},
{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"`},
{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"`},
{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"`},
{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"`},
{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"`},
},
},
}
Expand Down
36 changes: 36 additions & 0 deletions grammar/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,42 @@ func (op MatchOperator) String() string {
}
}

// NotPresentDisposition is called during evaluation when Selector fails to
// find a map key to determine the operator's behavior.
func (op MatchOperator) NotPresentDisposition() bool {
// For a selector M["x"] against a map M that lacks an "x" key...
switch op {
case MatchEqual:
// ...M["x"] == <anything> is false. Nothing is equal to a missing key
return false
case MatchNotEqual:
// ...M["x"] != <anything> is true. Nothing is equal to a missing key
return true
case MatchIn:
// "a" in M["x"] is false. Missing keys contain no values
return false
case MatchNotIn:
// "a" not in M["x"] is true. Missing keys contain no values
return true
case MatchIsEmpty:
// M["x"] is empty is true. Missing keys contain no values
return true
case MatchIsNotEmpty:
// M["x"] is not empty is false. Missing keys contain no values
return false
case MatchMatches:
// M["x"] matches <anything> is false. Nothing matches a missing key
return false
case MatchNotMatches:
// M["x"] not matches <anything> is true. Nothing matches a missing key
return true
default:
// Should never be reached as every operator should explicitly define its
// behavior.
return false
}
}

type MatchValue struct {
Raw string
Converted interface{}
Expand Down

0 comments on commit 700d3b7

Please sign in to comment.