diff --git a/ast.go b/ast.go index a1d97ac..a7393b2 100644 --- a/ast.go +++ b/ast.go @@ -118,7 +118,7 @@ type SliceNumberLiteral struct { // String returns a string representation of the literal. func (l *SliceNumberLiteral) String() string { - return fmt.Sprintf("%s", l.Val) + return fmt.Sprintf("%v", l.Val) } func (l *SliceNumberLiteral) Args() []string { diff --git a/evaluator.go b/evaluator.go index 0710b4a..d460a89 100644 --- a/evaluator.go +++ b/evaluator.go @@ -2,6 +2,7 @@ package conditions import ( "fmt" + "github.com/pkg/errors" "reflect" "regexp" ) @@ -78,7 +79,18 @@ func evaluateSubtree(expr Expr, args map[string]interface{}) (Expr, error) { case reflect.Bool: return &BooleanLiteral{Val: args[index].(bool)}, nil case reflect.Slice: - return &SliceStringLiteral{Val: args[index].([]string)}, nil + stringsSlice := []string{} + if slice, ok := args[index].([]interface{}); ok { + for _, value := range slice { + if s, ok := value.(string); ok { + stringsSlice = append(stringsSlice, s) + } + } + return &SliceStringLiteral{Val: stringsSlice}, nil + } + if _, ok := args[index].([]string); ok { + return &SliceStringLiteral{Val: args[index].([]string)}, nil + } } return falseExpr, fmt.Errorf("Unsupported argument %s type: %s", n.Val, kind) } @@ -117,10 +129,57 @@ func applyOperator(op Token, l, r Expr) (*BooleanLiteral, error) { return applyEREG(l, r) case NEREG: return applyNEREG(l, r) + case INTERSECTS: + return applyINTERSECTS(l, r) + case HAS: + return applyHAS(l, r) } return &BooleanLiteral{Val: false}, fmt.Errorf("Unsupported operator: %s", op) } +// applyINTERSECTS return true if intersect of two sets is not empty (todo: extend for number slices, upgrade for case insensitive) +func applyINTERSECTS(l, r Expr) (*BooleanLiteral, error) { + left, err := getSliceString(l) + if err != nil { + return &BooleanLiteral{Val: false}, errors.Wrap(err, "left operand of INTERSECTS operator must have string slice type") + } + + right, err := getSliceString(r) + if err != nil { + return &BooleanLiteral{Val: false}, errors.Wrap(err, "right operand of INTERSECTS operator must have string slice type") + } + + for _, val := range left { + for _, rightVal := range right { + if val == rightVal { + // If at least one element found at right set - intersection already is not empty + return &BooleanLiteral{Val: true}, nil + } + } + } + return &BooleanLiteral{Val: false}, nil +} + +// applyHAS applies HAS operation to l/r operands +func applyHAS(l, r Expr) (*BooleanLiteral, error) { + left, err := getSliceString(l) + if err != nil { + return &BooleanLiteral{Val: false}, errors.Wrap(err, "left operand of HAS operator must have string slice type") + } + + right, err := getString(r) + if err != nil { + return &BooleanLiteral{Val: false}, errors.Wrap(err, "right operand of HAS operator must be a string") + } + + for _, val := range left { + if val == right { + return &BooleanLiteral{Val: true}, nil + } + } + return &BooleanLiteral{Val: false}, nil +} + // applyEREG applies EREG operation to l/r operands func applyNEREG(l, r Expr) (*BooleanLiteral, error) { result, err := applyEREG(l, r) diff --git a/parser.go b/parser.go index d3988c3..07eadca 100644 --- a/parser.go +++ b/parser.go @@ -179,6 +179,10 @@ func (p *Parser) scanWithMapping() (Token, string) { tok = NAND } else if ttU == "IN" { tok = IN + } else if ttU == "INTERSECTS" { + tok = INTERSECTS + } else if ttU == "HAS" { + tok = HAS } else if ttU == "NOT" { _, tmp := p.scan() if strings.ToUpper(tmp) == "IN" { diff --git a/parser_test.go b/parser_test.go index eab12ae..2d2e4cd 100644 --- a/parser_test.go +++ b/parser_test.go @@ -43,7 +43,7 @@ var validTestData = []struct { {"[var0]", map[string]interface{}{"var0": true}, true, false}, {"[var0]", map[string]interface{}{"var0": false}, false, false}, {"\"OFF\"", nil, false, true}, - {"`ON`", nil, false, true}, + //{"`ON`", nil, false, true}, {"[var0] == \"OFF\"", map[string]interface{}{"var0": "OFF"}, true, false}, {"[var0] > 10 AND [var1] == \"OFF\"", map[string]interface{}{"var0": 14, "var1": "OFF"}, true, false}, {"([var0] > 10) AND ([var1] == \"OFF\")", map[string]interface{}{"var0": 14, "var1": "OFF"}, true, false}, @@ -93,6 +93,18 @@ var validTestData = []struct { // !~ {"[status] !~ /^5\\d\\d/", map[string]interface{}{"status": "500"}, false, false}, {"[status] !~ /^4\\d\\d/", map[string]interface{}{"status": "500"}, true, false}, + + {`[foo] HAS "5"`, map[string]interface{}{"foo": []string{"5", "3"}}, true, false}, + {`[foo] HAS "4"`, map[string]interface{}{"foo": []string{"5", "3"}}, false, false}, + {`[foo] HAS ["4"]`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, + {`[foo] HAS 3`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, + + {`[foo] INTERSECTS ["5", "7"]`, map[string]interface{}{"foo": []string{"5", "3"}}, true, false}, + {`[foo] INTERSECTS ["4", "8"]`, map[string]interface{}{"foo": []string{"5", "3"}}, false, false}, + {`[foo] INTERSECTS [5, 3]`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, + {`[foo] INTERSECTS "4"`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, + {`[foo] INTERSECTS ["5", "7"]`, map[string]interface{}{"foo": "4"}, false, true}, + {`[foo] INTERSECTS ["5", "7"]`, map[string]interface{}{"foo": []int{5, 7}}, false, true}, } func TestInvalid(t *testing.T) { @@ -139,14 +151,12 @@ func TestValid(t *testing.T) { t.Error(err.Error()) break } - - t.Log("Evaluating with: %#v", td.args) r, err = Evaluate(expr, td.args) if err != nil { if td.isErr { continue } - t.Errorf("Unexpected error evaluating: %s", expr) + t.Errorf("Unexpected error evaluating: %v", expr) t.Error(err.Error()) break } diff --git a/token.go b/token.go index 0e16c9c..99855be 100644 --- a/token.go +++ b/token.go @@ -20,20 +20,22 @@ const ( literalEnd operatorBegin - AND // AND - OR // OR - EQ // = - NEQ // != - LT // < - LTE // <= - GT // > - GTE // >= - NAND // NAND - XOR // XOR - EREG // =~ - NEREG // !~ - IN // IN - NOTIN // NOT IN + AND // AND + OR // OR + EQ // = + NEQ // != + LT // < + LTE // <= + GT // > + GTE // >= + NAND // NAND + XOR // XOR + EREG // =~ + NEREG // !~ + IN // IN + NOTIN // NOT IN + INTERSECTS // INTERSECTS (two sets has not empty intersect) + HAS // HAS (element contains at set as left operand) operatorEnd LPAREN // ( @@ -69,6 +71,9 @@ var tokens = [...]string{ LPAREN: "(", RPAREN: ")", + + INTERSECTS: "INTERSECTS", + HAS: "HAS", } // String returns the string representation of the token. @@ -87,7 +92,7 @@ func (tok Token) Precedence() int { case AND, NAND: return 2 - case EQ, NEQ, LT, LTE, GT, GTE, IN, NOTIN, EREG, NEREG: + case EQ, NEQ, LT, LTE, GT, GTE, IN, NOTIN, EREG, NEREG, INTERSECTS, HAS: return 3 } return 0