Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion bexpr.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ func CreateEvaluator(expression string, opts ...Option) (*Evaluator, error) {
return eval, nil
}

func (eval *Evaluator) Evaluate(datum interface{}) (bool, error) {
func (eval *Evaluator) Evaluate(datum interface{}, variables map[string]string) (bool, error) {
opts := []Option{
WithTagName(eval.tagName),
WithHookFn(eval.valueTransformationHook),
withVariables(variables),
}
if eval.unknownVal != nil {
opts = append(opts, WithUnknownValue(*eval.unknownVal))
Expand Down
51 changes: 30 additions & 21 deletions evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func derefType(rtype reflect.Type) reflect.Type {
return rtype
}

func doMatchMatches(expression *grammar.MatchExpression, value reflect.Value) (bool, error) {
func doMatchMatches(expression *grammar.MatchExpression, value reflect.Value, variables map[string]string) (bool, error) {
if !value.Type().ConvertibleTo(byteSliceTyp) {
return false, fmt.Errorf("Value of type %s is not convertible to []byte", value.Type())
}
Expand All @@ -88,21 +88,21 @@ func doMatchMatches(expression *grammar.MatchExpression, value reflect.Value) (b
return re.Match(value.Convert(byteSliceTyp).Interface().([]byte)), nil
}

func doMatchEqual(expression *grammar.MatchExpression, value reflect.Value) (bool, error) {
func doMatchEqual(expression *grammar.MatchExpression, value reflect.Value, variables map[string]string) (bool, error) {
// NOTE: see preconditions in evaluategrammar.MatchExpressionRecurse
eqFn := primitiveEqualityFn(value.Kind())
if eqFn == nil {
return false, errors.New("unable to find suitable primitive comparison function for matching")
}
matchValue, err := getMatchExprValue(expression, value.Kind())
matchValue, err := getMatchExprValue(expression, value.Type(), variables)
if err != nil {
return false, fmt.Errorf("error getting match value in expression: %w", err)
}
return eqFn(matchValue, value), nil
}

func doMatchIn(expression *grammar.MatchExpression, value reflect.Value) (bool, error) {
matchValue, err := getMatchExprValue(expression, value.Kind())
func doMatchIn(expression *grammar.MatchExpression, value reflect.Value, variables map[string]string) (bool, error) {
matchValue, err := getMatchExprValue(expression, value.Type(), variables)
if err != nil {
return false, fmt.Errorf("error getting match value in expression: %w", err)
}
Expand Down Expand Up @@ -134,7 +134,7 @@ func doMatchIn(expression *grammar.MatchExpression, value reflect.Value) (bool,
// syntax errors, so as a special case in this situation, don't
// error on a strconv.ErrSyntax, just continue on to the next
// element.
matchValue, err = getMatchExprValue(expression, kind)
matchValue, err = getMatchExprValue(expression, itemType, variables)
if err != nil {
if errors.Is(err, strconv.ErrSyntax) {
continue
Expand All @@ -156,7 +156,7 @@ func doMatchIn(expression *grammar.MatchExpression, value reflect.Value) (bool,
// Otherwise it's a concrete type and we can essentially cache the
// answers. First we need to re-derive the match value for equality
// assertion.
matchValue, err = getMatchExprValue(expression, kind)
matchValue, err = getMatchExprValue(expression, itemType, variables)
if err != nil {
return false, fmt.Errorf("error getting match value in expression: %w", err)
}
Expand Down Expand Up @@ -187,29 +187,38 @@ func doMatchIsEmpty(matcher *grammar.MatchExpression, value reflect.Value) (bool
return value.Len() == 0, nil
}

func getMatchExprValue(expression *grammar.MatchExpression, rvalue reflect.Kind) (interface{}, error) {
func getMatchExprValue(expression *grammar.MatchExpression, rvalue reflect.Type, variables map[string]string) (interface{}, error) {
if expression.Value == nil {
return nil, nil
}

switch rvalue {
val := expression.Value.Raw
if expression.Value.IsVariable {
val, _ = variables[expression.Value.Raw]
}

if val == "" {
val = fmt.Sprintf("%v", reflect.Zero(rvalue).Interface())
}

switch rvalue.Kind() {
case reflect.Bool:
return CoerceBool(expression.Value.Raw)
return CoerceBool(val)

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return CoerceInt64(expression.Value.Raw)
return CoerceInt64(val)

case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return CoerceUint64(expression.Value.Raw)
return CoerceUint64(val)

case reflect.Float32:
return CoerceFloat32(expression.Value.Raw)
return CoerceFloat32(val)

case reflect.Float64:
return CoerceFloat64(expression.Value.Raw)
return CoerceFloat64(val)

default:
return expression.Value.Raw, nil
return val, nil
}
}

Expand Down Expand Up @@ -247,17 +256,17 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac
rvalue := reflect.Indirect(reflect.ValueOf(val))
switch expression.Operator {
case grammar.MatchEqual:
return doMatchEqual(expression, rvalue)
return doMatchEqual(expression, rvalue, opts.withVariables)
case grammar.MatchNotEqual:
result, err := doMatchEqual(expression, rvalue)
result, err := doMatchEqual(expression, rvalue, opts.withVariables)
if err == nil {
return !result, nil
}
return false, err
case grammar.MatchIn:
return doMatchIn(expression, rvalue)
return doMatchIn(expression, rvalue, opts.withVariables)
case grammar.MatchNotIn:
result, err := doMatchIn(expression, rvalue)
result, err := doMatchIn(expression, rvalue, opts.withVariables)
if err == nil {
return !result, nil
}
Expand All @@ -271,9 +280,9 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac
}
return false, err
case grammar.MatchMatches:
return doMatchMatches(expression, rvalue)
return doMatchMatches(expression, rvalue, opts.withVariables)
case grammar.MatchNotMatches:
result, err := doMatchMatches(expression, rvalue)
result, err := doMatchMatches(expression, rvalue, opts.withVariables)
if err == nil {
return !result, nil
}
Expand Down
12 changes: 6 additions & 6 deletions evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func TestEvaluate(t *testing.T) {
expr, err := CreateEvaluator(expTest.expression, WithHookFn(expTest.hook))
require.NoError(t, err)

match, err := expr.Evaluate(tcase.value)
match, err := expr.Evaluate(tcase.value, nil)
if expTest.err != "" {
require.Error(t, err)
require.EqualError(t, err, expTest.err)
Expand Down Expand Up @@ -402,7 +402,7 @@ func TestWithHookFn(t *testing.T) {
expr, err := CreateEvaluator(eval.expression, WithHookFn(tc.hook))
require.NoError(t, err)

match, err := expr.Evaluate(tc.in)
match, err := expr.Evaluate(tc.in, nil)
if eval.err != "" {
require.Error(t, err)
require.Equal(t, eval.err, err.Error())
Expand Down Expand Up @@ -452,7 +452,7 @@ func TestUnknownVal(t *testing.T) {

match, err := expr.Evaluate(map[string]string{
"key": "foo",
})
}, nil)
if tc.err != "" {
require.Error(t, err)
require.EqualError(t, err, tc.err)
Expand Down Expand Up @@ -503,7 +503,7 @@ func TestUnknownVal_struct(t *testing.T) {
Key string `bexpr:"key"`
}{
Key: "foo",
})
}, nil)
if tc.err != "" {
require.Error(t, err)
require.EqualError(t, err, tc.err)
Expand Down Expand Up @@ -562,7 +562,7 @@ func TestCustomTag(t *testing.T) {
expr, err := CreateEvaluator(tc.expression, opts...)
require.NoError(t, err)

match, err := expr.Evaluate(ts)
match, err := expr.Evaluate(ts, nil)
if tc.jsonTag {
if tc.jnameFound {
require.NoError(t, err)
Expand Down Expand Up @@ -601,7 +601,7 @@ func BenchmarkEvaluate(b *testing.B) {

b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err = expr.Evaluate(tcase.value)
_, err = expr.Evaluate(tcase.value, nil)
if expTest.err != "" {
require.Error(b, err)
} else {
Expand Down
53 changes: 32 additions & 21 deletions examples/expr-eval/expr-eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,69 +45,80 @@ var data Matchable = Matchable{
Values: []int{1, 2, 3, 4, 5},
},
SliceInternal: []Internal{
Internal{
{
Name: "odd",
Values: []int{1, 3, 5, 7, 9},
},
Internal{
{
Name: "even",
Values: []int{2, 4, 6, 8, 10},
},
Internal{
{
Name: "fib",
Values: []int{0, 1, 1, 2, 3, 5},
},
},
MapInternal: map[string]Internal{
"odd": Internal{
"odd": {
Name: "odd",
Values: []int{1, 3, 5, 7, 9},
},
"even": Internal{
"even": {
Name: "even",
Values: []int{2, 4, 6, 8, 10},
},
"fib": Internal{
"fib": {
Name: "fib",
Values: []int{0, 1, 1, 2, 3, 5},
},
},
}

var expressions []string = []string{
type example struct {
expression string
variables map[string]string
}

var examples []example = []example{
// should error out in creating the evaluator as Foo is not a valid selector
"Foo == 3",
{expression: "Foo == 3"},
// should error out because the field is hidden
"Internal.Hidden == 5",
{expression: "Internal.Hidden == 5"},
// should error out because the field is not exported
"Internal.unexported == 3",
{expression: "Internal.unexported == 3"},
// should evaluate to true
"Map[`abc`] == `def`",
{expression: "Map[`abc`] == `def`"},
// should evaluate to false
"X == 3",
{expression: "X == 3"},
// should evaluate to true
"Internal.fields is not empty",
{expression: "Internal.fields is not empty"},
// should evaluate to false
"MapInternal.fib.Name != fib",
{expression: "MapInternal.fib.Name != fib"},
// should evaluate to true
"odd in MapInternal",
{expression: "odd in MapInternal"},
// variable interpolation - should evaluate to true
{expression: "X == ${value}", variables: map[string]string{"value": "5"}},
// variable interpolation - should evaluate to false
{expression: "X == ${value}", variables: map[string]string{"value": "4"}},
// variable interpolation default value - should evaluate to false
{expression: "X == ${value}"},
}

func main() {
for _, expression := range expressions {
eval, err := bexpr.CreateEvaluator(expression)
for _, ex := range examples {
eval, err := bexpr.CreateEvaluator(ex.expression)

if err != nil {
fmt.Printf("Failed to create evaluator for expression %q: %v\n", expression, err)
fmt.Printf("Failed to create evaluator for expression %q: %v\n", ex.expression, err)
continue
}

result, err := eval.Evaluate(data)
result, err := eval.Evaluate(data, ex.variables)
if err != nil {
fmt.Printf("Failed to run evaluation of expression %q: %v\n", expression, err)
fmt.Printf("Failed to run evaluation of expression %q (variables %#v): %v\n", ex.expression, ex.variables, err)
continue
}

fmt.Printf("Result of expression %q evaluation: %t\n", expression, result)
fmt.Printf("Result of expression %q evaluation (variables: %#v): %t\n", ex.expression, ex.variables, result)
}
}
2 changes: 1 addition & 1 deletion examples/simple/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func main() {
continue
}

result, err := eval.Evaluate(value)
result, err := eval.Evaluate(value, nil)
if err != nil {
fmt.Printf("Failed to run evaluation of expression %q: %v\n", expression, err)
continue
Expand Down
4 changes: 2 additions & 2 deletions filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (f *Filter) Execute(data interface{}) (interface{}, error) {
if !item.CanInterface() {
return nil, fmt.Errorf("Slice/Array value can not be used")
}
result, err := f.evaluator.Evaluate(item.Interface())
result, err := f.evaluator.Evaluate(item.Interface(), nil)
if err != nil {
return nil, err
}
Expand All @@ -76,7 +76,7 @@ func (f *Filter) Execute(data interface{}) (interface{}, error) {
return nil, fmt.Errorf("Map value cannot be used")
}

result, err := f.evaluator.Evaluate(item.Interface())
result, err := f.evaluator.Evaluate(item.Interface(), nil)
if err != nil {
return nil, err
}
Expand Down
5 changes: 3 additions & 2 deletions grammar/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ func (op MatchOperator) String() string {
}

type MatchValue struct {
Raw string
Converted interface{}
Raw string
Converted interface{}
IsVariable bool
}

type UnaryExpression struct {
Expand Down
Loading