From be73ea12140f59c52398578538fa3669c17e7c6e Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Fri, 21 Apr 2023 09:57:59 -0700 Subject: [PATCH] missing map key match operator dispositions Implements MatchOperator.NotPresentDisposition to determine an operator's behavior when a map key in the selector is not found. Fixes #35 Replaces #36 --- evaluate.go | 34 +++++++++++++++++++++++++++++++--- evaluate_test.go | 19 ++++++++++++++++++- grammar/ast.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/evaluate.go b/evaluate.go index ba3db26..ae6a3b0 100644 --- a/evaluate.go +++ b/evaluate.go @@ -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{ @@ -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 { diff --git a/evaluate_test.go b/evaluate_test.go index 3acaece..697af79 100644 --- a/evaluate_test.go +++ b/evaluate_test.go @@ -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"`}, }, }, } diff --git a/grammar/ast.go b/grammar/ast.go index bf6e6c1..b3bd400 100644 --- a/grammar/ast.go +++ b/grammar/ast.go @@ -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"] == is false. Nothing is equal to a missing key + return false + case MatchNotEqual: + // ...M["x"] != 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 is false. Nothing matches a missing key + return false + case MatchNotMatches: + // M["x"] not matches 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{}