diff --git a/bexpr.go b/bexpr.go index 1dc36f5..d39822a 100644 --- a/bexpr.go +++ b/bexpr.go @@ -23,6 +23,7 @@ type Evaluator struct { ast grammar.Expression tagName string valueTransformationHook ValueTransformationHookFn + unknownVal *interface{} } func CreateEvaluator(expression string, opts ...Option) (*Evaluator, error) { @@ -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...) } diff --git a/evaluate.go b/evaluate.go index 6bf73be..ba3db26 100644 --- a/evaluate.go +++ b/evaluate.go @@ -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 { diff --git a/evaluate_test.go b/evaluate_test.go index 5eeea58..fcecd34 100644 --- a/evaluate_test.go +++ b/evaluate_test.go @@ -1,10 +1,12 @@ package bexpr import ( + "errors" "fmt" "reflect" "testing" + "github.com/mitchellh/pointerstructure" "github.com/stretchr/testify/require" ) @@ -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}, @@ -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"}, }, }, @@ -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() @@ -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)) } } }) diff --git a/go.mod b/go.mod index 81ad7ef..0234235 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index ca1ebb5..ba4b24f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/options.go b/options.go index ca68250..e1a3f69 100644 --- a/options.go +++ b/options.go @@ -19,6 +19,7 @@ type options struct { withMaxExpressions uint64 withTagName string withHookFn ValueTransformationHookFn + withUnknown *interface{} } func WithMaxExpressions(maxExprCnt uint64) Option { @@ -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, } }