diff --git a/eval.go b/eval.go index 8012de41a..48a0d1871 100644 --- a/eval.go +++ b/eval.go @@ -158,16 +158,6 @@ func (n binaryNode) eval(env interface{}) (interface{}, error) { } return !ok, nil - case "matches": - if isText(left) && isText(right) { - matched, err := regexp.MatchString(toText(right), toText(left)) - if err != nil { - return nil, err - } - return matched, nil - } - return nil, fmt.Errorf("operator matches not defined on (%T, %T)", left, right) - case "~": if isText(left) && isText(right) { return toText(left) + toText(right), nil @@ -255,6 +245,34 @@ func makeRange(min, max int64) ([]float64, error) { return a, nil } +func (n matchesNode) eval(env interface{}) (interface{}, error) { + left, err := Run(n.left, env) + if err != nil { + return nil, err + } + + if n.r != nil { + if isText(left) { + return n.r.MatchString(toText(left)), nil + } + } + + right, err := Run(n.right, env) + if err != nil { + return nil, err + } + + if isText(left) && isText(right) { + matched, err := regexp.MatchString(toText(right), toText(left)) + if err != nil { + return nil, err + } + return matched, nil + } + + return nil, fmt.Errorf("operator matches doesn't defined on (%T, %T): %v", left, right, n) +} + func (n propertyNode) eval(env interface{}) (interface{}, error) { v, err := Run(n.node, env) if err != nil { diff --git a/eval_test.go b/eval_test.go index f7b83fca0..4e379142d 100644 --- a/eval_test.go +++ b/eval_test.go @@ -265,6 +265,11 @@ var evalTests = []evalTest{ nil, true, }, + { + `"seafood" matches "sea" ~ "food"`, + nil, + true, + }, { `not ("seafood" matches "[0-9]+") ? "a" : "b"`, nil, @@ -317,7 +322,27 @@ var evalErrorTests = []evalErrorTest{ { `"seafood" matches "a(b"`, nil, - `error parsing regexp:`, + "error parsing regexp: missing closing ): `a(b`", + }, + { + `"seafood" matches "a" ~ ")b"`, + nil, + "error parsing regexp: unexpected ): `a)b`", + }, + { + `1 matches "1" ~ "2"`, + nil, + "operator matches doesn't defined on (float64, string): (1 matches (\"1\" ~ \"2\"))", + }, + { + `1 matches "1"`, + nil, + "operator matches doesn't defined on (float64, string): (1 matches \"1\")", + }, + { + `"1" matches 1`, + nil, + "operator matches doesn't defined on (string, float64): (\"1\" matches 1)", }, { `0 ? 1 : 2`, diff --git a/node.go b/node.go index 5abef3da2..1ab7789dc 100644 --- a/node.go +++ b/node.go @@ -1,5 +1,7 @@ package expr +import "regexp" + // Node represents items of abstract syntax tree. type Node interface{} @@ -36,6 +38,12 @@ type binaryNode struct { right Node } +type matchesNode struct { + r *regexp.Regexp + left Node + right Node +} + type propertyNode struct { node Node property Node diff --git a/parser.go b/parser.go index b3c72da5d..27e1b6db1 100644 --- a/parser.go +++ b/parser.go @@ -2,6 +2,7 @@ package expr import ( "fmt" + "regexp" "strconv" "unicode/utf8" ) @@ -179,7 +180,18 @@ func (p *parser) parseExpression(precedence int) (Node, error) { } } - node = binaryNode{operator: token.value, left: node, right: expr} + if token.is(operator, "matches") { + var r *regexp.Regexp + if s, ok := expr.(textNode); ok { + r, err = regexp.Compile(s.value) + if err != nil { + return nil, p.errorf("%v", err) + } + } + node = matchesNode{r: r, left: node, right: expr} + } else { + node = binaryNode{operator: token.value, left: node, right: expr} + } token = p.current continue } diff --git a/parser_test.go b/parser_test.go index ecaccf0ce..090fe2872 100644 --- a/parser_test.go +++ b/parser_test.go @@ -101,10 +101,6 @@ var parseTests = []parseTest{ "a ?: b", conditionalNode{nameNode{"a"}, nameNode{"a"}, nameNode{"b"}}, }, - { - `"foo" matches "/foo/"`, - binaryNode{"matches", textNode{"foo"}, textNode{"/foo/"}}, - }, { "foo.bar().foo().baz[33]", propertyNode{propertyNode{methodNode{methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{}}, identifierNode{"foo"}, []Node{}}, identifierNode{"baz"}}, numberNode{33}}, @@ -169,6 +165,10 @@ var parseErrorTests = []parseErrorTest{ "{-}", "a map key must be a", }, + { + "a matches 'a)(b'", + "error parsing regexp: unexpected )", + }, } func TestParse(t *testing.T) { @@ -196,14 +196,42 @@ func TestParseError(t *testing.T) { } } -func TestParseErrorPosition(t *testing.T) { - _, err := Parse("foo() + bar(**)") - if err == nil { - err = fmt.Errorf("") +func TestParse_matches(t *testing.T) { + node, err := Parse(`foo matches "foo"`) + if err != nil { + t.Fatal(err) + } + + m, ok := node.(matchesNode) + if !ok { + t.Fatalf("expected to me matchesNode, got %T", node) + } + + if !reflect.DeepEqual(m.left, nameNode{"foo"}) || !reflect.DeepEqual(m.right, textNode{"foo"}) { + t.Fatalf("left or right side of matches operator invalid: %#v", m) + } + + if m.r == nil { + t.Fatal("regexp should be compiled") + } +} + +func TestParse_matches_dynamic(t *testing.T) { + node, err := Parse(`foo matches regex`) + if err != nil { + t.Fatal(err) + } + + m, ok := node.(matchesNode) + if !ok { + t.Fatalf("expected to me matchesNode, got %T", node) + } + + if !reflect.DeepEqual(m.left, nameNode{"foo"}) || !reflect.DeepEqual(m.right, nameNode{"regex"}) { + t.Fatalf("left or right side of matches operator invalid: %#v", m) } - expected := "unexpected token operator(**)\nfoo() + bar(**)\n------------^" - if err.Error() != expected { - t.Errorf("\ngot\n\t%+v\nexpected\n\t%v", err.Error(), expected) + if m.r != nil { + t.Fatal("regexp should not be compiled") } } diff --git a/print.go b/print.go index 60d86c1d4..492805af4 100644 --- a/print.go +++ b/print.go @@ -44,6 +44,10 @@ func (n binaryNode) String() string { return fmt.Sprintf("(%v %v %v)", n.left, n.operator, n.right) } +func (n matchesNode) String() string { + return fmt.Sprintf("(%v matches %v)", n.left, n.right) +} + func (n propertyNode) String() string { switch n.property.(type) { case identifierNode: diff --git a/print_test.go b/print_test.go index 8de71cdec..4cb3fe6f3 100644 --- a/print_test.go +++ b/print_test.go @@ -43,6 +43,14 @@ var printTests = []printTest{ binaryNode{"and", binaryNode{"or", nameNode{"a"}, nameNode{"b"}}, nameNode{"c"}}, "((a or b) and c)", }, + { + matchesNode{left: nameNode{"foo"}, right: textNode{"foobar"}}, + "(foo matches \"foobar\")", + }, + { + conditionalNode{nameNode{"a"}, nameNode{"a"}, nameNode{"b"}}, + "a ? a : b", + }, } func TestPrint(t *testing.T) {