Skip to content

Commit

Permalink
Feature/#293 regular exp operator (#326)
Browse files Browse the repository at this point in the history
* Added Regexp operator

* Added simple type check

* Fixed linting issue
  • Loading branch information
ziflex authored Jul 9, 2019
1 parent cf8c55a commit cf43c7f
Show file tree
Hide file tree
Showing 10 changed files with 965 additions and 593 deletions.
77 changes: 77 additions & 0 deletions pkg/compiler/compiler_regexp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package compiler_test

import (
"context"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"testing"

"github.com/MontFerret/ferret/pkg/compiler"
. "github.com/smartystreets/goconvey/convey"
)

func TestRegexpOperator(t *testing.T) {
Convey("Should be possible to use positive regular expression operator", t, func() {
out := compiler.New().
MustCompile(`
RETURN "foo" =~ "^f[o].$"
`).
MustRun(context.Background())

So(string(out), ShouldEqual, `true`)
})

Convey("Should be possible to use negative regular expression operator", t, func() {
out := compiler.New().
MustCompile(`
RETURN "foo" !~ "[a-z]+bar$"
`).
MustRun(context.Background())

So(string(out), ShouldEqual, `true`)
})

Convey("Should be possible to use negative regular expression operator", t, func() {
c := compiler.New()
c.RegisterFunction("T::REGEXP", func(_ context.Context, _ ...core.Value) (value core.Value, e error) {
return values.NewString("[a-z]+bar$"), nil
})

out := c.
MustCompile(`
RETURN "foo" !~ T::REGEXP()
`).
MustRun(context.Background())

So(string(out), ShouldEqual, `true`)
})

Convey("Should return an error during compilation when a regexp string invalid", t, func() {
_, err := compiler.New().
Compile(`
RETURN "foo" !~ "[ ]\K(?<!\d )(?=(?: ?\d){8})(?!(?: ?\d){9})\d[ \d]+\d"
`)

So(err, ShouldBeError)
})

Convey("Should return an error during compilation when a regexp is not a string", t, func() {
right := []string{
"[]",
"{}",
"1",
"1.1",
"TRUE",
}

for _, r := range right {
_, err := compiler.New().
Compile(fmt.Sprintf(`
RETURN "foo" !~ %s
`, r))

So(err, ShouldBeError)
}
})
}
37 changes: 37 additions & 0 deletions pkg/compiler/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package compiler

import (
"fmt"
"regexp"
"strconv"
"strings"

Expand Down Expand Up @@ -1187,6 +1188,36 @@ func (v *visitor) doVisitEqualityOperator(ctx *fql.ExpressionContext, scope *sco
return operators.NewEqualityOperator(v.getSourceMap(equalityOp), left, right, equalityOp.GetText())
}

func (v *visitor) doVisitRegexpOperator(ctx *fql.ExpressionContext, scope *scope) (core.Expression, error) {
regexpOp := ctx.RegexpOperator().(*fql.RegexpOperatorContext)
rawExps := ctx.AllExpression()
exps, err := v.doVisitAllExpressions(rawExps, scope)

if err != nil {
return nil, err
}

left := exps[0]
right := exps[1]

switch lit := right.(type) {
case literals.StringLiteral:
_, err := regexp.Compile(string(lit))

if err != nil {
src := v.getSourceMap(rawExps[1])

return nil, errors.Wrap(err, src.String())
}
case *literals.ArrayLiteral, *literals.ObjectLiteral, literals.BooleanLiteral, literals.FloatLiteral, literals.IntLiteral:
src := v.getSourceMap(rawExps[1])

return nil, errors.Wrap(errors.New("expected a string literal or a function call"), src.String())
}

return operators.NewRegexpOperator(v.getSourceMap(regexpOp), left, right, regexpOp.GetText())
}

func (v *visitor) doVisitInOperator(ctx *fql.ExpressionContext, scope *scope) (core.OperatorExpression, error) {
exps, err := v.doVisitAllExpressions(ctx.AllExpression(), scope)

Expand Down Expand Up @@ -1333,6 +1364,12 @@ func (v *visitor) doVisitExpression(ctx *fql.ExpressionContext, scope *scope) (c
return v.doVisitLogicalOperator(ctx, scope)
}

regexpOp := ctx.RegexpOperator()

if regexpOp != nil {
return v.doVisitRegexpOperator(ctx, scope)
}

variable := ctx.Variable()

if variable != nil {
Expand Down
6 changes: 6 additions & 0 deletions pkg/parser/antlr/FqlParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ expression
| expression arrayOperator (inOperator | equalityOperator) expression
| expression inOperator expression
| expression equalityOperator expression
| expression regexpOperator expression
| expression logicalAndOperator expression
| expression logicalOrOperator expression
| expression QuestionMark expression? Colon expression
Expand Down Expand Up @@ -270,6 +271,11 @@ equalityOperator
| Neq
;

regexpOperator
: RegexMatch
| RegexNotMatch
;

logicalAndOperator
: And
;
Expand Down
3 changes: 2 additions & 1 deletion pkg/parser/fql/FqlParser.interp

Large diffs are not rendered by default.

1,331 changes: 739 additions & 592 deletions pkg/parser/fql/fql_parser.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pkg/parser/fql/fqlparser_base_listener.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pkg/parser/fql/fqlparser_base_visitor.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pkg/parser/fql/fqlparser_listener.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pkg/parser/fql/fqlparser_visitor.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions pkg/runtime/expressions/operators/regexp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package operators

import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"regexp"
)

type (
RegexpOperatorType int
RegexpOperator struct {
*baseOperator
opType RegexpOperatorType
}
)

const (
RegexpOperatorTypeNegative RegexpOperatorType = 0
RegexpOperatorTypePositive RegexpOperatorType = 1
)

var regexpOperators = map[string]RegexpOperatorType{
"!~": RegexpOperatorTypeNegative,
"=~": RegexpOperatorTypePositive,
}

func NewRegexpOperator(
src core.SourceMap,
left core.Expression,
right core.Expression,
operator string,
) (*RegexpOperator, error) {
op, exists := regexpOperators[operator]

if !exists {
return nil, core.Error(core.ErrInvalidArgument, "operator")
}

return &RegexpOperator{
&baseOperator{
src,
left,
right,
},
op,
}, nil
}

func (operator *RegexpOperator) Type() RegexpOperatorType {
return operator.opType
}

func (operator *RegexpOperator) Exec(ctx context.Context, scope *core.Scope) (core.Value, error) {
left, err := operator.left.Exec(ctx, scope)

if err != nil {
return nil, err
}

right, err := operator.right.Exec(ctx, scope)

if err != nil {
return nil, err
}

return operator.Eval(ctx, left, right)
}

func (operator *RegexpOperator) Eval(_ context.Context, left, right core.Value) (core.Value, error) {
leftStr := left.String()
rightStr := right.String()

r, err := regexp.Compile(rightStr)

if err != nil {
return values.None, err
}

if operator.opType == RegexpOperatorTypePositive {
return values.NewBoolean(r.MatchString(leftStr)), nil
}

return values.NewBoolean(!r.MatchString(leftStr)), nil
}

0 comments on commit cf43c7f

Please sign in to comment.