Skip to content

Commit

Permalink
WithUnknownValue to treat unknown values as a non-error
Browse files Browse the repository at this point in the history
This introduces a new option "WithUnknownValue" that can be used to
specify some value to use for unknown keys. Normally, bexpr will error
(and this continues to be the default). However, an alternate zero value
behavior can be specified to use that instead.

This is useful for situations where you want to treat empty values as
say... empty string so that an expression generally fails but with a
false rather than an error.

This also updates to pointerstructure 1.2.1 which fixes on "not found"
case that wasn't wrapping ErrNotFound properly.
  • Loading branch information
mitchellh committed Nov 2, 2021
1 parent b515551 commit 8e71c43
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 9 deletions.
8 changes: 7 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,16 @@ 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))
return evaluate(eval.ast, datum,
WithTagName(eval.tagName),
WithHookFn(eval.valueTransformationHook),
WithUnknownValue(eval.unknownVal),
)
}
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
11 changes: 11 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,6 +45,16 @@ 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,
Expand Down

0 comments on commit 8e71c43

Please sign in to comment.