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

WithUnknownValue to treat unknown values as a non-error #24

Merged
merged 1 commit into from Nov 3, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
}
}