Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
99 changes: 94 additions & 5 deletions evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func getMatchExprValue(expression *grammar.MatchExpression, rvalue reflect.Kind)
// 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
// the Selector's Path is not a map, a pointerstructure.ErrNotFound error is
// returned.
func evaluateNotPresent(ptr pointerstructure.Pointer, datum interface{}) bool {
if len(ptr.Parts) < 2 {
Expand All @@ -237,10 +237,10 @@ func evaluateNotPresent(ptr pointerstructure.Pointer, datum interface{}) bool {
return reflect.ValueOf(val).Kind() == reflect.Map
}

func evaluateMatchExpression(expression *grammar.MatchExpression, datum interface{}, opt ...Option) (bool, error) {
func getValue(datum interface{}, path []string, opt ...Option) (interface{}, bool, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice to have a comment here just detailing what this function is doing (the overall flow with the local variables).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I added some comments now.

opts := getOpts(opt...)
ptr := pointerstructure.Pointer{
Parts: expression.Selector.Path,
Parts: path,
Config: pointerstructure.Config{
TagName: opts.withTagName,
ValueTransformationHook: opts.withHookFn,
Expand All @@ -256,15 +256,31 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac
err = nil
val = *opts.withUnknown
case evaluateNotPresent(ptr, datum):
return expression.Operator.NotPresentDisposition(), nil
return nil, false, nil
}
}

if err != nil {
return false, fmt.Errorf("error finding value in datum: %w", err)
return false, false, fmt.Errorf("error finding value in datum: %w", err)
}
}

return val, true, nil
}

func evaluateMatchExpression(expression *grammar.MatchExpression, datum interface{}, opt ...Option) (bool, error) {
val, present, err := getValue(
datum,
expression.Selector.Path,
opt...,
)
if err != nil {
return false, err
}
if !present {
return expression.Operator.NotPresentDisposition(), nil
}

if jn, ok := val.(json.Number); ok {
if jni, err := jn.Int64(); err == nil {
val = jni
Expand Down Expand Up @@ -314,6 +330,77 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac
}
}

func evaluateCollectionExpression(expression *grammar.CollectionExpression, datum interface{}, opt ...Option) (bool, error) {
val, present, err := getValue(
datum,
expression.Selector.Path,
opt...,
)
if err != nil {
return false, err
}
if !present {
return expression.Type == grammar.AllExpression, nil
}

v := reflect.ValueOf(val)

var keys []reflect.Value
if v.Kind() == reflect.Map {
keys = v.MapKeys()
}

switch v.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
for i := 0; i < v.Len(); i++ {

vars := map[string]interface{}{}

if v.Kind() == reflect.Map {
key := keys[i]
if expression.Key != "_" {
vars[expression.Key] = key.Interface()
}
if expression.Value != "" && expression.Value != "_" {
vars[expression.Value] = v.MapIndex(key).Interface()
}
} else {
value := v.Index(i).Interface()
if expression.Value == "" {
// This means we are using the version with one placeholder
// like "all things as t { ... }". For lists t will iterate
// over the elements, not the indexes to match the behavior
// of the Sentinel for loop (or the "for t in things" in
// Python). This should match what users expect.
if expression.Key != "_" {
vars[expression.Key] = value
}
} else {
if expression.Key != "_" {
vars[expression.Key] = i
}
if expression.Value != "_" {
vars[expression.Value] = value
}
}
}

result, err := evaluate(expression.Inner, pointerstructure.Defaults{vars, datum}, opt...)
if err != nil {
return false, err
}
if (result && expression.Type == grammar.AnyExpression) || (!result && expression.Type == grammar.AllExpression) {
return result, nil
}
}

return expression.Type == grammar.AllExpression, nil

default:
return false, fmt.Errorf(`%s is not a list or a map`, expression.Selector.String())
}
}

func evaluate(ast grammar.Expression, datum interface{}, opt ...Option) (bool, error) {
switch node := ast.(type) {
case *grammar.UnaryExpression:
Expand Down Expand Up @@ -342,6 +429,8 @@ func evaluate(ast grammar.Expression, datum interface{}, opt ...Option) (bool, e
}
case *grammar.MatchExpression:
return evaluateMatchExpression(node, datum, opt...)
case *grammar.CollectionExpression:
return evaluateCollectionExpression(node, datum, opt...)
}
return false, fmt.Errorf("Invalid AST node")
}
22 changes: 20 additions & 2 deletions evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ var evaluateTests map[string]expressionTest = map[string]expressionTest{
{expression: "foo.bar != false", result: true},
{expression: "foo.baz != false", result: false},
{expression: "foo.baz != true", result: true},
{expression: "foo.bar.baz == 3", result: false, err: `error finding value in datum: /foo/bar/baz: at part 2, invalid value kind: bool`},
{expression: "foo.bar.baz == 3", result: false, err: `error finding value in datum: /foo/bar/baz at part 2: invalid value kind (bool)`},
},
},
"Nested Structs and Maps": {
Expand Down Expand Up @@ -311,6 +311,24 @@ var evaluateTests map[string]expressionTest = map[string]expressionTest{
{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},
// all
Comment thread
remilapeyre marked this conversation as resolved.
{expression: `all Nested.SliceOfInts as _ { TopInt == 5 }`, result: true},
{expression: `all Nested.SliceOfInts as i { i != 42 }`, result: true},
{expression: `all Nested.SliceOfInts as i { i == 1 }`, result: false},
{expression: `all Nested.Map as v { v == "bar" }`, result: false},
{expression: `all Nested.Map as v { v != "hello" }`, result: true},
{expression: `all Nested.Map as _, _ { TopInt == 5 }`, result: true},
{expression: `all Nested.Map as k, _ { k != "foo" }`, result: false},
{expression: `all Nested.Map as k, _ { k != "hello" }`, result: true},
{expression: `all Nested.Map as k, v { k != "foo" or v != "baz" }`, result: true},
{expression: `all TopInt as k, v { k != "foo" or v != "baz" }`, err: "TopInt is not a list or a map"},
// any
{expression: `any Nested.SliceOfInts as i { i == 1 }`, result: true},
{expression: `any Nested.SliceOfInts as i { i == 42 }`, result: false},
{expression: `any Nested.Map as v { v != "bar" }`, result: true},
{expression: `any Nested.Map as v { v == "bar" }`, result: true},
{expression: `any Nested.Map as v { v == "hello" }`, result: false},
{expression: `any Nested.Map as k, v { k == "foo" and v == "bar" }`, result: true},
// Missing key in map tests
{expression: "Nested.Map.notfound == 4", result: false},
{expression: "Nested.Map.notfound != 4", result: true},
Expand Down Expand Up @@ -399,7 +417,7 @@ func TestWithHookFn(t *testing.T) {
{expression: `"/I/I"=="bar"`, result: true},
{
expression: `"/S/I"=="foo"`, result: false,
err: "error finding value in datum: /S/I: at part 1, invalid value kind: string",
err: "error finding value in datum: /S/I at part 1: invalid value kind (string)",
},
},
},
Expand Down
11 changes: 10 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
module github.com/hashicorp/go-bexpr

go 1.14
go 1.18

require (
github.com/mitchellh/pointerstructure v1.2.1
github.com/stretchr/testify v1.8.2
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/mitchellh/pointerstructure v1.2.1 => github.com/remilapeyre/pointerstructure v0.0.0-20230906004826-80d813010704
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remilapeyre/pointerstructure v0.0.0-20230906004826-80d813010704 h1:KnesY9UJKkVhupIN3bac36cb6Kc0mTTG7BkHNrdQgSo=
github.com/remilapeyre/pointerstructure v0.0.0-20230906004826-80d813010704/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
25 changes: 25 additions & 0 deletions grammar/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,28 @@ func (expr *MatchExpression) ExpressionDump(w io.Writer, indent string, level in
fmt.Fprintf(w, "%[1]s%[3]s {\n%[2]sSelector: %[4]v\n%[1]s}\n", strings.Repeat(indent, level), strings.Repeat(indent, level+1), expr.Operator.String(), expr.Selector)
}
}

type CollectionExpressionType string

const (
AllExpression CollectionExpressionType = "All"
AnyExpression CollectionExpressionType = "Any"
)

type CollectionExpression struct {
Type CollectionExpressionType
Selector Selector
Inner Expression
Key, Value string
}

func (expr *CollectionExpression) ExpressionDump(w io.Writer, indent string, level int) {
localIndent := strings.Repeat(indent, level)
if expr.Value == "" {
fmt.Fprintf(w, "%s%s %s on %v {\n", localIndent, expr.Type, expr.Key, expr.Selector)
} else {
fmt.Fprintf(w, "%s%s (%s, %s) on %v {\n", localIndent, expr.Type, expr.Key, expr.Value, expr.Selector)
}
expr.Inner.ExpressionDump(w, indent, level+1)
fmt.Fprintf(w, "%s}\n", localIndent)
}
66 changes: 66 additions & 0 deletions grammar/ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,72 @@ func TestAST_Dump(t *testing.T) {
},
expected: "UNKNOWN {\n Is Empty {\n Selector: foo.bar\n }\n Is Empty {\n Selector: foo.bar\n }\n}\n",
},
"All single variation": {
expr: &CollectionExpression{
Key: "k",
Value: "",
Type: AllExpression,
Selector: Selector{
Type: SelectorTypeBexpr,
Path: []string{"obj"},
},
Inner: &MatchExpression{
Selector: Selector{
Type: SelectorTypeBexpr,
Path: []string{"v"},
},
Operator: 0,
Value: &MatchValue{
Raw: "hello",
},
},
},
expected: "All k on obj {\n Equal {\n Selector: v\n Value: \"hello\"\n }\n}\n",
},
"All": {
expr: &CollectionExpression{
Key: "k",
Value: "v",
Type: AllExpression,
Selector: Selector{
Type: SelectorTypeBexpr,
Path: []string{"obj"},
},
Inner: &MatchExpression{
Selector: Selector{
Type: SelectorTypeBexpr,
Path: []string{"v"},
},
Operator: 0,
Value: &MatchValue{
Raw: "hello",
},
},
},
expected: "All (k, v) on obj {\n Equal {\n Selector: v\n Value: \"hello\"\n }\n}\n",
},
"Any": {
expr: &CollectionExpression{
Key: "k",
Value: "_",
Type: AnyExpression,
Selector: Selector{
Type: SelectorTypeBexpr,
Path: []string{"obj"},
},
Inner: &MatchExpression{
Selector: Selector{
Type: SelectorTypeBexpr,
Path: []string{"v"},
},
Operator: 0,
Value: &MatchValue{
Raw: "hello",
},
},
},
expected: "Any (k, _) on obj {\n Equal {\n Selector: v\n Value: \"hello\"\n }\n}\n",
},
}

for name, tcase := range tests {
Expand Down
Loading