Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#293 regular exp operator #326

Merged
merged 3 commits into from
Jul 9, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
})
}
39 changes: 39 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,38 @@ 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())
}

break
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 +1366,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
}