From e80f53ef15b7cb4072bbea9fe406678e71971655 Mon Sep 17 00:00:00 2001 From: Pavel Ivanisenko Date: Mon, 2 Dec 2019 16:51:41 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=B1=D0=B0=D0=B3=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20reflect.Slice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ast.go | 2 +- evaluator.go | 12 ++++++++++++ parser_test.go | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) 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..638f9b5 100644 --- a/evaluator.go +++ b/evaluator.go @@ -79,6 +79,18 @@ func evaluateSubtree(expr Expr, args map[string]interface{}) (Expr, error) { 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) } diff --git a/parser_test.go b/parser_test.go index eab12ae..970ecc0 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}, @@ -95,6 +95,7 @@ var validTestData = []struct { {"[status] !~ /^4\\d\\d/", map[string]interface{}{"status": "500"}, true, false}, } + func TestInvalid(t *testing.T) { var ( @@ -140,7 +141,6 @@ func TestValid(t *testing.T) { break } - t.Log("Evaluating with: %#v", td.args) r, err = Evaluate(expr, td.args) if err != nil { if td.isErr { From 7a29fd92c5011a2677c3a917928bce000954ca41 Mon Sep 17 00:00:00 2001 From: Pavel Ivanisenko Date: Mon, 2 Dec 2019 16:53:59 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=B1=D0=B0=D0=B3=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20reflect.Slice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- evaluator.go | 1 - 1 file changed, 1 deletion(-) diff --git a/evaluator.go b/evaluator.go index 638f9b5..275b216 100644 --- a/evaluator.go +++ b/evaluator.go @@ -78,7 +78,6 @@ 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 { From 6cb21611f36d8afc59a78fa6f6cbebed510861c0 Mon Sep 17 00:00:00 2001 From: Pavel Ivanisenko Date: Mon, 2 Dec 2019 22:47:08 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=20=D0=B5=D1=89=D0=B5=20=D0=B4=D0=B2=D0=B5=20?= =?UTF-8?q?=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BC?= =?UTF-8?q?=D0=BD=D0=BE=D0=B6=D0=B5=D1=81=D1=82=D0=B2=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=20-=20JOINT=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BA=D0=B0=D1=82=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=B5=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=B5=D1=87=D0=B5=D0=BD=D0=B8=D0=B5,=20HAS?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B0=D0=B4=D0=BB=D0=B5=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D0=BD=D0=B4=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=B5=D0=B2=D0=BE=D0=BC=D1=83=20=D0=BC=D0=BD=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D1=83.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- evaluator.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 4 ++++ parser_test.go | 16 +++++++++++++--- token.go | 7 ++++++- 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/evaluator.go b/evaluator.go index 275b216..a3f5445 100644 --- a/evaluator.go +++ b/evaluator.go @@ -2,6 +2,7 @@ package conditions import ( "fmt" + "github.com/pkg/errors" "reflect" "regexp" ) @@ -128,10 +129,57 @@ func applyOperator(op Token, l, r Expr) (*BooleanLiteral, error) { return applyEREG(l, r) case NEREG: return applyNEREG(l, r) + case JOINT: + return applyJOINT(l, r) + case HAS: + return applyHAS(l, r) } return &BooleanLiteral{Val: false}, fmt.Errorf("Unsupported operator: %s", op) } +// applyJOINT return true if intersect of two sets is not empty (todo: extend for number slices, upgrade for case insensitive) +func applyJOINT(l, r Expr) (*BooleanLiteral, error) { + left, err := getSliceString(l) + if err != nil { + return &BooleanLiteral{Val: false}, errors.Wrap(err, "left operand of JOINT operator must have string slice type") + } + + right, err := getSliceString(r) + if err != nil { + return &BooleanLiteral{Val: false}, errors.Wrap(err, "right operand of JOINT 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..7c64d78 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 == "JOINT" { + tok = JOINT + } 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 970ecc0..d0fb2e6 100644 --- a/parser_test.go +++ b/parser_test.go @@ -93,8 +93,19 @@ 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] JOINT ["5", "7"]`, map[string]interface{}{"foo": []string{"5", "3"}}, true, false}, + {`[foo] JOINT ["4", "8"]`, map[string]interface{}{"foo": []string{"5", "3"}}, false, false}, + {`[foo] JOINT [5, 3]`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, + {`[foo] JOINT "4"`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, + {`[foo] JOINT ["5", "7"]`, map[string]interface{}{"foo": "4"}, false, true}, + {`[foo] JOINT ["5", "7"]`, map[string]interface{}{"foo": []int{5, 7}}, false, true}, +} func TestInvalid(t *testing.T) { @@ -140,13 +151,12 @@ func TestValid(t *testing.T) { t.Error(err.Error()) break } - 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..031b7b2 100644 --- a/token.go +++ b/token.go @@ -34,6 +34,8 @@ const ( NEREG // !~ IN // IN NOTIN // NOT IN + JOINT // JOINT (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: ")", + + JOINT: "JOINT", + 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, JOINT, HAS: return 3 } return 0 From 2dd4eee302aecc9c482f770f3bf6ec5707944d35 Mon Sep 17 00:00:00 2001 From: Pavel Ivanisenko Date: Tue, 3 Dec 2019 11:34:36 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=D0=BB=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20JOINT=20=D0=BD=D0=B0=20INTERSECTS.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- evaluator.go | 12 ++++++------ parser.go | 4 ++-- parser_test.go | 12 ++++++------ token.go | 38 +++++++++++++++++++------------------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/evaluator.go b/evaluator.go index a3f5445..d460a89 100644 --- a/evaluator.go +++ b/evaluator.go @@ -129,24 +129,24 @@ func applyOperator(op Token, l, r Expr) (*BooleanLiteral, error) { return applyEREG(l, r) case NEREG: return applyNEREG(l, r) - case JOINT: - return applyJOINT(l, r) + case INTERSECTS: + return applyINTERSECTS(l, r) case HAS: return applyHAS(l, r) } return &BooleanLiteral{Val: false}, fmt.Errorf("Unsupported operator: %s", op) } -// applyJOINT return true if intersect of two sets is not empty (todo: extend for number slices, upgrade for case insensitive) -func applyJOINT(l, r Expr) (*BooleanLiteral, error) { +// 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 JOINT operator must have string slice type") + 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 JOINT operator must have string slice type") + return &BooleanLiteral{Val: false}, errors.Wrap(err, "right operand of INTERSECTS operator must have string slice type") } for _, val := range left { diff --git a/parser.go b/parser.go index 7c64d78..07eadca 100644 --- a/parser.go +++ b/parser.go @@ -179,8 +179,8 @@ func (p *Parser) scanWithMapping() (Token, string) { tok = NAND } else if ttU == "IN" { tok = IN - } else if ttU == "JOINT" { - tok = JOINT + } else if ttU == "INTERSECTS" { + tok = INTERSECTS } else if ttU == "HAS" { tok = HAS } else if ttU == "NOT" { diff --git a/parser_test.go b/parser_test.go index d0fb2e6..2d2e4cd 100644 --- a/parser_test.go +++ b/parser_test.go @@ -99,12 +99,12 @@ var validTestData = []struct { {`[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] JOINT ["5", "7"]`, map[string]interface{}{"foo": []string{"5", "3"}}, true, false}, - {`[foo] JOINT ["4", "8"]`, map[string]interface{}{"foo": []string{"5", "3"}}, false, false}, - {`[foo] JOINT [5, 3]`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, - {`[foo] JOINT "4"`, map[string]interface{}{"foo": []string{"5", "3"}}, false, true}, - {`[foo] JOINT ["5", "7"]`, map[string]interface{}{"foo": "4"}, false, true}, - {`[foo] JOINT ["5", "7"]`, map[string]interface{}{"foo": []int{5, 7}}, 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) { diff --git a/token.go b/token.go index 031b7b2..99855be 100644 --- a/token.go +++ b/token.go @@ -20,22 +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 - JOINT // JOINT (two sets has not empty intersect) - HAS // HAS (element contains at set as left operand) + 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 // ( @@ -72,8 +72,8 @@ var tokens = [...]string{ LPAREN: "(", RPAREN: ")", - JOINT: "JOINT", - HAS: "HAS", + INTERSECTS: "INTERSECTS", + HAS: "HAS", } // String returns the string representation of the token. @@ -92,7 +92,7 @@ func (tok Token) Precedence() int { case AND, NAND: return 2 - case EQ, NEQ, LT, LTE, GT, GTE, IN, NOTIN, EREG, NEREG, JOINT, HAS: + case EQ, NEQ, LT, LTE, GT, GTE, IN, NOTIN, EREG, NEREG, INTERSECTS, HAS: return 3 } return 0