Skip to content

Commit

Permalink
Merge pull request #24 from hashicorp/feature/unknownval
Browse files Browse the repository at this point in the history
WithUnknownValue to treat unknown values as a non-error
  • Loading branch information
mitchellh committed Nov 3, 2021
2 parents b515551 + 46883af commit 9ef6478
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 9 deletions.
12 changes: 11 additions & 1 deletion bexpr.go
Expand Up @@ -23,6 +23,7 @@ type Evaluator struct {
ast grammar.Expression
tagName string
valueTransformationHook ValueTransformationHookFn
unknownVal *interface{}
}

func CreateEvaluator(expression string, opts ...Option) (*Evaluator, error) {
Expand All @@ -41,11 +42,20 @@ func CreateEvaluator(expression string, opts ...Option) (*Evaluator, error) {
ast: ast.(grammar.Expression),
tagName: parsedOpts.withTagName,
valueTransformationHook: parsedOpts.withHookFn,
unknownVal: parsedOpts.withUnknown,
}

return eval, nil
}

func (eval *Evaluator) Evaluate(datum interface{}) (bool, error) {
return evaluate(eval.ast, datum, WithTagName(eval.tagName), WithHookFn(eval.valueTransformationHook))
opts := []Option{
WithTagName(eval.tagName),
WithHookFn(eval.valueTransformationHook),
}
if eval.unknownVal != nil {
opts = append(opts, WithUnknownValue(*eval.unknownVal))
}

return evaluate(eval.ast, datum, opts...)
}
9 changes: 8 additions & 1 deletion evaluate.go
Expand Up @@ -224,7 +224,14 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac
}
val, err := ptr.Get(datum)
if err != nil {
return false, fmt.Errorf("error finding value in datum: %w", err)
if errors.Is(err, pointerstructure.ErrNotFound) && opts.withUnknown != nil {
err = nil
val = *opts.withUnknown
}

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

if jn, ok := val.(json.Number); ok {
Expand Down
110 changes: 106 additions & 4 deletions evaluate_test.go
@@ -1,10 +1,12 @@
package bexpr

import (
"errors"
"fmt"
"reflect"
"testing"

"github.com/mitchellh/pointerstructure"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -109,7 +111,7 @@ var evaluateTests map[string]expressionTest = map[string]expressionTest{
{expression: "ColonString == `expo:rted`", result: true},
{expression: "ColonString != `expor:ted`", result: true},
{expression: "slash/value == `hello`", result: true},
{expression: "unexported == `unexported`", result: false, err: `error finding value in datum: /unexported at part 0: couldn't find struct field with name "unexported"`},
{expression: "unexported == `unexported`", result: false, err: `error finding value in datum: /unexported at part 0: couldn't find key: struct field with name "unexported"`},
{expression: "Hidden == false", result: false, err: "error finding value in datum: /Hidden at part 0: struct field \"Hidden\" is ignored and cannot be used"},
{expression: "String matches `^ex.*`", result: true, benchQuick: true},
{expression: "String not matches `^anchored.*`", result: true, benchQuick: true},
Expand Down Expand Up @@ -192,7 +194,7 @@ var evaluateTests map[string]expressionTest = map[string]expressionTest{
{expression: "String == `not-it`", result: false, benchQuick: true},
{expression: "String != `exported`", result: false},
{expression: "String != `not-it`", result: true},
{expression: "unexported == `unexported`", result: false, err: `error finding value in datum: /unexported at part 0: couldn't find struct field with name "unexported"`},
{expression: "unexported == `unexported`", result: false, err: `error finding value in datum: /unexported at part 0: couldn't find key: struct field with name "unexported"`},
{expression: "Hidden == false", result: false, err: "error finding value in datum: /Hidden at part 0: struct field \"Hidden\" is ignored and cannot be used"},
},
},
Expand Down Expand Up @@ -411,6 +413,106 @@ func TestWithHookFn(t *testing.T) {
}
}

func TestUnknownVal(t *testing.T) {
t.Parallel()

cases := []struct {
name string
expression string
unknownVal interface{}
result bool
err string
}{
{
name: "key exists",
expression: `key == "foo"`,
unknownVal: "bar",
result: true,
},
{
name: "key does not exist",
expression: `unknown == "bar"`,
unknownVal: "bar",
result: true,
},
{
name: "key does not exist (fail)",
expression: `unknown == "qux"`,
unknownVal: "bar",
result: false,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
expr, err := CreateEvaluator(tc.expression, WithUnknownValue(tc.unknownVal))
require.NoError(t, err)

match, err := expr.Evaluate(map[string]string{
"key": "foo",
})
if tc.err != "" {
require.Error(t, err)
require.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.result, match)
})
}
}

func TestUnknownVal_struct(t *testing.T) {
t.Parallel()

cases := []struct {
name string
expression string
unknownVal interface{}
result bool
err string
}{
{
name: "key exists",
expression: `key == "foo"`,
unknownVal: "bar",
result: true,
},
{
name: "key does not exist",
expression: `unknown == "bar"`,
unknownVal: "bar",
result: true,
},
{
name: "key does not exist (fail)",
expression: `unknown == "qux"`,
unknownVal: "bar",
result: false,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
expr, err := CreateEvaluator(tc.expression, WithUnknownValue(tc.unknownVal))
require.NoError(t, err)

match, err := expr.Evaluate(struct {
Key string `bexpr:"key"`
}{
Key: "foo",
})
if tc.err != "" {
require.Error(t, err)
require.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.result, match)
})
}
}

func TestCustomTag(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -464,14 +566,14 @@ func TestCustomTag(t *testing.T) {
require.NoError(t, err)
require.True(t, match)
} else {
require.Contains(t, err.Error(), "couldn't find struct field")
require.True(t, errors.Is(err, pointerstructure.ErrNotFound))
}
} else {
if tc.bnameFound {
require.NoError(t, err)
require.True(t, match)
} else {
require.Contains(t, err.Error(), "couldn't find struct field")
require.True(t, errors.Is(err, pointerstructure.ErrNotFound))
}
}
})
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -4,6 +4,6 @@ go 1.14

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mitchellh/pointerstructure v1.2.0
github.com/mitchellh/pointerstructure v1.2.1
github.com/stretchr/testify v1.7.0
)
4 changes: 2 additions & 2 deletions go.sum
Expand Up @@ -3,8 +3,8 @@ 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.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
12 changes: 12 additions & 0 deletions options.go
Expand Up @@ -19,6 +19,7 @@ type options struct {
withMaxExpressions uint64
withTagName string
withHookFn ValueTransformationHookFn
withUnknown *interface{}
}

func WithMaxExpressions(maxExprCnt uint64) Option {
Expand All @@ -44,9 +45,20 @@ func WithHookFn(fn ValueTransformationHookFn) Option {
}
}

// WithUnknownValue sets a value that is used for any unknown keys. Normally,
// bexpr will error on any expressions with unknown keys. This can be set to
// instead use a specificed value whenever an unknown key is found. For example,
// this might be set to the empty string "".
func WithUnknownValue(val interface{}) Option {
return func(o *options) {
o.withUnknown = &val
}
}

func getDefaultOptions() options {
return options{
withMaxExpressions: 0,
withTagName: "bexpr",
withUnknown: nil,
}
}

0 comments on commit 9ef6478

Please sign in to comment.