Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

missing map key match operator dispositions #38

Merged
merged 1 commit into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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