Skip to content

Commit

Permalink
RE-1240 Support missing fields (#13)
Browse files Browse the repository at this point in the history
* RE-1240 Support missing fields

* RE-1240 fix eval attr path stack

* RE-1240 fix test

* RE-1240 niltozerovalue format
  • Loading branch information
ezerozen authored Jan 13, 2025
1 parent c310ea1 commit 7f02f0f
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 123 deletions.
13 changes: 10 additions & 3 deletions evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ package rules

import "github.com/conekta/Conekta-Golang-Rules-Engine/parser"

func Evaluate(rule string, items map[string]interface{}) (bool, error) {
ev, err := parser.NewEvaluator(rule)
func Evaluate(rule string, items map[string]interface{}, opts ...parser.EvaluatorConfigOption) (bool, error) {
ev, err := parser.NewEvaluator(rule, opts...)
if err != nil {
return false, err
}
return ev.Process(items)
res, err := ev.Process(items)
if err != nil {
return false, err
}
if err = ev.LastDebugErr(); err != nil {
return res, err
}
return res, nil
}
80 changes: 80 additions & 0 deletions evaluate_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package rules

import (
"errors"
"testing"

"github.com/stretchr/testify/require"

"github.com/conekta/Conekta-Golang-Rules-Engine/parser"
)

func TestEvaluateAttrPathValue(t *testing.T) {
Expand All @@ -19,6 +22,23 @@ func TestEvaluateAttrPathValue(t *testing.T) {
require.True(t, res)
}

func TestEvaluateAttrPathValueNil(t *testing.T) {
res, err := Evaluate(`x.c.d > x.c.e`, map[string]interface{}{
"x": map[string]interface{}{
"c": map[string]interface{}{
"d": (*int)(nil),
"e": 1,
},
},
})
var nestedError *parser.NestedError
if errors.As(err, &nestedError) &&
!errors.Is(nestedError.Err, parser.ErrEvalOperandMissing) {
require.NoError(t, err)
}
require.False(t, res)
}

func TestEvaluateBasic(t *testing.T) {
res, err := Evaluate(`x.c.d eq "abc"`, map[string]interface{}{
"x": map[string]interface{}{
Expand All @@ -29,7 +49,67 @@ func TestEvaluateBasic(t *testing.T) {
})
require.NoError(t, err)
require.True(t, res)
}

func TestEvaluateMissingField(t *testing.T) {
res, err := Evaluate(`x.c.e eq "abc"`, map[string]interface{}{
"x": map[string]interface{}{
"c": map[string]interface{}{
"d": "abc",
},
},
})
var nestedError *parser.NestedError
if errors.As(err, &nestedError) &&
!errors.Is(nestedError.Err, parser.ErrEvalOperandMissing) {
require.NoError(t, err)
}
require.False(t, res)
}

func TestEvaluateMissingFieldWithNilToZeroValue(t *testing.T) {
res, err := Evaluate(`x.c.e eq ""`, map[string]interface{}{
"x": map[string]interface{}{
"c": map[string]interface{}{
"d": "abc",
},
},
}, parser.WithNilToZeroValue())
var nestedError *parser.NestedError
if errors.As(err, &nestedError) &&
!errors.Is(nestedError.Err, parser.ErrEvalOperandMissing) {
require.NoError(t, err)
}
require.True(t, res)
}

func TestEvaluateMissingFieldEqEmpty(t *testing.T) {
res, err := Evaluate(`x.c.e eq ""`, map[string]interface{}{
"x": map[string]interface{}{
"c": map[string]interface{}{
"d": "abc",
},
},
})
var nestedError *parser.NestedError
if errors.As(err, &nestedError) &&
!errors.Is(nestedError.Err, parser.ErrEvalOperandMissing) {
require.NoError(t, err)
}
require.False(t, res)
}

func TestEvaluateNilField(t *testing.T) {
res, err := Evaluate(`x.c.e eq "abc"`, map[string]interface{}{
"x": (*int)(nil),
"e": nil,
})
var nestedError *parser.NestedError
if errors.As(err, &nestedError) &&
!errors.Is(nestedError.Err, parser.ErrEvalOperandMissing) {
require.NoError(t, err)
}
require.False(t, res)
}

func TestSum(t *testing.T) {
Expand Down
8 changes: 6 additions & 2 deletions parser/bool_operation.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package parser

type BoolOperation struct {
BaseOperation
NullOperation
}

func (o *BoolOperation) get(left Operand, right Operand) (bool, bool, error) {
if left == nil {
return false, false, ErrEvalOperandMissing
if isNil(left) {
if !o.config.NilToZeroValue {
return false, false, ErrEvalOperandMissing
}
left = false
}
leftVal, ok := left.(bool)
if !ok {
Expand Down
13 changes: 13 additions & 0 deletions parser/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package parser

type EvaluatorConfigOption func(*EvaluatorConfig)

type EvaluatorConfig struct {
NilToZeroValue bool
}

func WithNilToZeroValue() EvaluatorConfigOption {
return func(e *EvaluatorConfig) {
e.NilToZeroValue = true
}
}
15 changes: 11 additions & 4 deletions parser/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ type Evaluator struct {
tree antlr.ParseTree

testHookPanic func()
config EvaluatorConfig
}

func NewEvaluator(rule string) (ret *Evaluator, retErr error) {
func NewEvaluator(rule string, opts ...EvaluatorConfigOption) (ret *Evaluator, retErr error) {
// antlr lib has panics for exceptions so we have to put a recover here
// in the unlikely case there is an exception
defer func() {
Expand All @@ -24,6 +25,11 @@ func NewEvaluator(rule string) (ret *Evaluator, retErr error) {
retErr = fmt.Errorf("%q", info)
}
}()
config := EvaluatorConfig{}
for _, opt := range opts {
opt(&config)
}

input := antlr.NewInputStream(rule)
lex := NewJsonQueryLexer(input)
lex.RemoveErrorListeners()
Expand All @@ -34,8 +40,9 @@ func NewEvaluator(rule string) (ret *Evaluator, retErr error) {
tree := p.Query()

return &Evaluator{
rule: rule,
tree: tree,
rule: rule,
tree: tree,
config: config,
}, nil
}

Expand All @@ -60,7 +67,7 @@ func (e *Evaluator) Process(items map[string]interface{}) (ret bool, retErr erro
}
}()

visitor := NewJsonQueryVisitorImpl(items)
visitor := NewJsonQueryVisitorImpl(items, e.config)
result := visitor.Visit(e.tree)
e.lastDebugErr = visitor.debugErr
if e.testHookPanic != nil {
Expand Down
8 changes: 6 additions & 2 deletions parser/float_operation.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package parser

type FloatOperation struct {
BaseOperation
NullOperation
}

func (o *FloatOperation) get(left Operand, right Operand) (float64, float64, error) {
if left == nil {
return 0, 0, ErrEvalOperandMissing
if isNil(left) {
if !o.config.NilToZeroValue {
return 0, 0, ErrEvalOperandMissing
}
left = 0
}
leftVal, err := toFloat(left)
if err != nil {
Expand Down
8 changes: 6 additions & 2 deletions parser/int_operation.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package parser

type IntOperation struct {
BaseOperation
NullOperation
}

func (o *IntOperation) get(left Operand, right Operand) (int, int, error) {
if left == nil {
return 0, 0, ErrEvalOperandMissing
if isNil(left) {
if !o.config.NilToZeroValue {
return 0, 0, ErrEvalOperandMissing
}
left = 0
}
leftVal, err := toInt(left)
if err != nil {
Expand Down
Loading

0 comments on commit 7f02f0f

Please sign in to comment.