1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-01-08 03:13:15 +02:00

Added support of optional ignoring of errors to function calls (#652)

* Added support of optional ignoring of errors to function calls

* Added support of handling of source failure to optional chaining

* Updated unit tests
This commit is contained in:
Tim Voronov 2021-09-06 22:21:20 -04:00 committed by GitHub
parent 0cb7623a7f
commit 5f361e9e1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1211 additions and 893 deletions

View File

@ -80,6 +80,21 @@ func TestFunctionNSCall(t *testing.T) {
So(err, ShouldNotBeNil)
})
Convey("T::FAIL()? should return NONE", t, func() {
c := compiler.New()
p, err := c.Compile(`
RETURN T::FAIL()?
`)
So(err, ShouldBeNil)
out, err := p.Run(context.Background())
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `null`)
})
Convey("Should use keywords", t, func() {
c := compiler.New()

View File

@ -5,6 +5,7 @@ import (
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/pkg/errors"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
@ -83,6 +84,44 @@ func TestFunctionCall(t *testing.T) {
So(string(out), ShouldEqual, `[2,4,6,8]`)
})
Convey("Should handle errors when ? is used", t, func() {
c := compiler.New()
c.RegisterFunction("ERROR", func(ctx context.Context, args ...core.Value) (core.Value, error) {
return values.None, errors.New("test error")
})
p, err := c.Compile(`
RETURN ERROR()?
`)
So(err, ShouldBeNil)
out, err := p.Run(context.Background())
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `null`)
})
Convey("Should return NONE when error is handled", t, func() {
c := compiler.New()
c.RegisterFunction("ERROR", func(ctx context.Context, args ...core.Value) (core.Value, error) {
return values.NewString("booo"), errors.New("test error")
})
p, err := c.Compile(`
RETURN ERROR()?
`)
So(err, ShouldBeNil)
out, err := p.Run(context.Background())
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `null`)
})
}
func BenchmarkFunctionCallArg1(b *testing.B) {

View File

@ -2,8 +2,10 @@ package compiler_test
import (
"context"
"errors"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime"
"github.com/MontFerret/ferret/pkg/runtime/core"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
@ -71,7 +73,43 @@ func TestLogicalOperators(t *testing.T) {
So(string(out), ShouldEqual, `"foo"`)
})
Convey("NONE && true should return null", t, func() {
Convey("ERROR()? || 'boo' should return 'boo'", t, func() {
c := compiler.New()
c.RegisterFunction("ERROR", func(ctx context.Context, args ...core.Value) (core.Value, error) {
return nil, errors.New("test")
})
p, err := c.Compile(`
RETURN ERROR()? || 'boo'
`)
So(err, ShouldBeNil)
out, err := p.Run(context.Background())
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `"boo"`)
})
Convey("!ERROR()? && TRUE should return false", t, func() {
c := compiler.New()
c.RegisterFunction("ERROR", func(ctx context.Context, args ...core.Value) (core.Value, error) {
return nil, errors.New("test")
})
p, err := c.Compile(`
RETURN !ERROR()? && TRUE
`)
So(err, ShouldBeNil)
out, err := p.Run(context.Background())
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `true`)
})
Convey("NONE && true should return null", t, func() {
c := compiler.New()
p, err := c.Compile(`

View File

@ -3,6 +3,7 @@ package compiler_test
import (
"context"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/core"
"testing"
"github.com/MontFerret/ferret/pkg/compiler"
@ -495,6 +496,25 @@ RETURN o1.first["second"][o2.prop].fourth["fifth"]["bottom"]
So(string(out), ShouldEqual, `"bar"`)
})
Convey("When function returns error", func() {
c := compiler.New()
c.RegisterFunction("ERROR", func(ctx context.Context, args ...core.Value) (core.Value, error) {
return nil, core.ErrNotImplemented
})
p, err := c.Compile(`
RETURN ERROR()?.foo
`)
So(err, ShouldBeNil)
out, err := p.Run(context.Background())
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `null`)
})
})
})
}

View File

@ -928,8 +928,8 @@ func (v *visitor) doVisitMemberExpressionSource(ctx *fql.MemberExpressionSourceC
return v.doVisitParamContext(param.(*fql.ParamContext), scope)
}
if fnCall := ctx.FunctionCallExpression(); fnCall != nil {
return v.doVisitFunctionCallExpression(fnCall.(*fql.FunctionCallExpressionContext), scope)
if fnCall := ctx.FunctionCall(); fnCall != nil {
return v.doVisitFunctionCall(fnCall.(*fql.FunctionCallContext), false, scope)
}
if objectLiteral := ctx.ObjectLiteral(); objectLiteral != nil {
@ -1134,7 +1134,7 @@ func (v *visitor) doVisitRangeOperator(ctx *fql.RangeOperatorContext, scope *sco
)
}
func (v *visitor) doVisitFunctionCallExpression(context *fql.FunctionCallExpressionContext, scope *scope) (core.Expression, error) {
func (v *visitor) doVisitFunctionCall(context *fql.FunctionCallContext, ignoreErrors bool, scope *scope) (core.Expression, error) {
args := make([]core.Expression, 0, 5)
argsCtx := context.Arguments()
@ -1170,10 +1170,19 @@ func (v *visitor) doVisitFunctionCallExpression(context *fql.FunctionCallExpress
return expressions.NewFunctionCallExpression(
v.getSourceMap(context),
fun,
ignoreErrors,
args...,
)
}
func (v *visitor) doVisitFunctionCallExpression(context *fql.FunctionCallExpressionContext, scope *scope) (core.Expression, error) {
return v.doVisitFunctionCall(
context.FunctionCall().(*fql.FunctionCallContext),
context.QuestionMark() != nil,
scope,
)
}
func (v *visitor) doVisitParamContext(context *fql.ParamContext, s *scope) (core.Expression, error) {
name := context.Identifier().GetText()

View File

@ -57,6 +57,7 @@ func NewBufferedTestEventStream(buffer int) *TestEventStream {
es := new(TestEventStream)
es.ready = make(chan struct{}, buffer)
es.message = make(chan interface{}, buffer)
return es
}
@ -394,7 +395,7 @@ func TestLoop(t *testing.T) {
// Stop the loop
cancel()
time.Sleep(time.Duration(100) * time.Millisecond)
time.Sleep(time.Duration(500) * time.Millisecond)
onLoad.Emit(&page.LoadEventFiredReply{})
@ -404,6 +405,8 @@ func TestLoop(t *testing.T) {
onLoad.Emit(&page.LoadEventFiredReply{})
}
time.Sleep(time.Duration(500) * time.Millisecond)
So(counter.Value(), ShouldEqual, eventsToFire)
})
}

View File

@ -264,9 +264,17 @@ memberExpression
memberExpressionSource
: variable
| param
| functionCallExpression
| arrayLiteral
| objectLiteral
| functionCall
;
functionCall
: namespace functionIdentifier arguments
;
functionCallExpression
: functionCall QuestionMark?
;
memberExpressionPath
@ -274,10 +282,6 @@ memberExpressionPath
| (QuestionMark Dot)? computedPropertyName
;
functionCallExpression
: namespace functionIdentifier arguments
;
functionIdentifier
: Identifier
| And

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -308,11 +308,11 @@ func (s *BaseFqlParserListener) EnterMemberExpressionSource(ctx *MemberExpressio
// ExitMemberExpressionSource is called when production memberExpressionSource is exited.
func (s *BaseFqlParserListener) ExitMemberExpressionSource(ctx *MemberExpressionSourceContext) {}
// EnterMemberExpressionPath is called when production memberExpressionPath is entered.
func (s *BaseFqlParserListener) EnterMemberExpressionPath(ctx *MemberExpressionPathContext) {}
// EnterFunctionCall is called when production functionCall is entered.
func (s *BaseFqlParserListener) EnterFunctionCall(ctx *FunctionCallContext) {}
// ExitMemberExpressionPath is called when production memberExpressionPath is exited.
func (s *BaseFqlParserListener) ExitMemberExpressionPath(ctx *MemberExpressionPathContext) {}
// ExitFunctionCall is called when production functionCall is exited.
func (s *BaseFqlParserListener) ExitFunctionCall(ctx *FunctionCallContext) {}
// EnterFunctionCallExpression is called when production functionCallExpression is entered.
func (s *BaseFqlParserListener) EnterFunctionCallExpression(ctx *FunctionCallExpressionContext) {}
@ -320,6 +320,12 @@ func (s *BaseFqlParserListener) EnterFunctionCallExpression(ctx *FunctionCallExp
// ExitFunctionCallExpression is called when production functionCallExpression is exited.
func (s *BaseFqlParserListener) ExitFunctionCallExpression(ctx *FunctionCallExpressionContext) {}
// EnterMemberExpressionPath is called when production memberExpressionPath is entered.
func (s *BaseFqlParserListener) EnterMemberExpressionPath(ctx *MemberExpressionPathContext) {}
// ExitMemberExpressionPath is called when production memberExpressionPath is exited.
func (s *BaseFqlParserListener) ExitMemberExpressionPath(ctx *MemberExpressionPathContext) {}
// EnterFunctionIdentifier is called when production functionIdentifier is entered.
func (s *BaseFqlParserListener) EnterFunctionIdentifier(ctx *FunctionIdentifierContext) {}

View File

@ -199,7 +199,7 @@ func (v *BaseFqlParserVisitor) VisitMemberExpressionSource(ctx *MemberExpression
return v.VisitChildren(ctx)
}
func (v *BaseFqlParserVisitor) VisitMemberExpressionPath(ctx *MemberExpressionPathContext) interface{} {
func (v *BaseFqlParserVisitor) VisitFunctionCall(ctx *FunctionCallContext) interface{} {
return v.VisitChildren(ctx)
}
@ -207,6 +207,10 @@ func (v *BaseFqlParserVisitor) VisitFunctionCallExpression(ctx *FunctionCallExpr
return v.VisitChildren(ctx)
}
func (v *BaseFqlParserVisitor) VisitMemberExpressionPath(ctx *MemberExpressionPathContext) interface{} {
return v.VisitChildren(ctx)
}
func (v *BaseFqlParserVisitor) VisitFunctionIdentifier(ctx *FunctionIdentifierContext) interface{} {
return v.VisitChildren(ctx)
}

View File

@ -151,12 +151,15 @@ type FqlParserListener interface {
// EnterMemberExpressionSource is called when entering the memberExpressionSource production.
EnterMemberExpressionSource(c *MemberExpressionSourceContext)
// EnterMemberExpressionPath is called when entering the memberExpressionPath production.
EnterMemberExpressionPath(c *MemberExpressionPathContext)
// EnterFunctionCall is called when entering the functionCall production.
EnterFunctionCall(c *FunctionCallContext)
// EnterFunctionCallExpression is called when entering the functionCallExpression production.
EnterFunctionCallExpression(c *FunctionCallExpressionContext)
// EnterMemberExpressionPath is called when entering the memberExpressionPath production.
EnterMemberExpressionPath(c *MemberExpressionPathContext)
// EnterFunctionIdentifier is called when entering the functionIdentifier production.
EnterFunctionIdentifier(c *FunctionIdentifierContext)
@ -346,12 +349,15 @@ type FqlParserListener interface {
// ExitMemberExpressionSource is called when exiting the memberExpressionSource production.
ExitMemberExpressionSource(c *MemberExpressionSourceContext)
// ExitMemberExpressionPath is called when exiting the memberExpressionPath production.
ExitMemberExpressionPath(c *MemberExpressionPathContext)
// ExitFunctionCall is called when exiting the functionCall production.
ExitFunctionCall(c *FunctionCallContext)
// ExitFunctionCallExpression is called when exiting the functionCallExpression production.
ExitFunctionCallExpression(c *FunctionCallExpressionContext)
// ExitMemberExpressionPath is called when exiting the memberExpressionPath production.
ExitMemberExpressionPath(c *MemberExpressionPathContext)
// ExitFunctionIdentifier is called when exiting the functionIdentifier production.
ExitFunctionIdentifier(c *FunctionIdentifierContext)

View File

@ -151,12 +151,15 @@ type FqlParserVisitor interface {
// Visit a parse tree produced by FqlParser#memberExpressionSource.
VisitMemberExpressionSource(ctx *MemberExpressionSourceContext) interface{}
// Visit a parse tree produced by FqlParser#memberExpressionPath.
VisitMemberExpressionPath(ctx *MemberExpressionPathContext) interface{}
// Visit a parse tree produced by FqlParser#functionCall.
VisitFunctionCall(ctx *FunctionCallContext) interface{}
// Visit a parse tree produced by FqlParser#functionCallExpression.
VisitFunctionCallExpression(ctx *FunctionCallExpressionContext) interface{}
// Visit a parse tree produced by FqlParser#memberExpressionPath.
VisitMemberExpressionPath(ctx *MemberExpressionPathContext) interface{}
// Visit a parse tree produced by FqlParser#functionIdentifier.
VisitFunctionIdentifier(ctx *FunctionIdentifierContext) interface{}

View File

@ -7,21 +7,23 @@ import (
)
type FunctionCallExpression struct {
src core.SourceMap
fun core.Function
args []core.Expression
src core.SourceMap
fun core.Function
ignoreErrors bool
args []core.Expression
}
func NewFunctionCallExpression(
src core.SourceMap,
fun core.Function,
ignoreErrors bool,
args ...core.Expression,
) (*FunctionCallExpression, error) {
if fun == nil {
return nil, core.Error(core.ErrMissedArgument, "function")
}
return &FunctionCallExpression{src, fun, args}, nil
return &FunctionCallExpression{src, fun, ignoreErrors, args}, nil
}
func (e *FunctionCallExpression) Arguments() []core.Expression {
@ -29,6 +31,18 @@ func (e *FunctionCallExpression) Arguments() []core.Expression {
}
func (e *FunctionCallExpression) Function() core.Function {
if e.ignoreErrors {
return func(ctx context.Context, args ...core.Value) (core.Value, error) {
out, err := e.fun(ctx, args...)
if err != nil {
return values.None, nil
}
return out, nil
}
}
return e.fun
}
@ -49,7 +63,7 @@ func (e *FunctionCallExpression) Exec(ctx context.Context, scope *core.Scope) (c
out, err := arg.Exec(ctx, scope)
if err != nil {
return values.None, core.SourceError(e.src, err)
return values.None, e.maybeError(core.SourceError(e.src, err))
}
args[idx] = out
@ -59,9 +73,17 @@ func (e *FunctionCallExpression) Exec(ctx context.Context, scope *core.Scope) (c
}
if err != nil {
return values.None, core.SourceError(e.src, err)
return values.None, e.maybeError(core.SourceError(e.src, err))
}
return out, nil
}
}
func (e *FunctionCallExpression) maybeError(err error) error {
if !e.ignoreErrors {
return err
}
return nil
}

View File

@ -6,6 +6,7 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/expressions"
"github.com/MontFerret/ferret/pkg/runtime/expressions/literals"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
"testing"
. "github.com/smartystreets/goconvey/convey"
@ -21,6 +22,7 @@ func TestFunctionCallExpression(t *testing.T) {
return values.True, nil
},
false,
)
So(err, ShouldBeNil)
@ -46,6 +48,7 @@ func TestFunctionCallExpression(t *testing.T) {
return values.True, nil
},
false,
args...,
)
@ -72,6 +75,7 @@ func TestFunctionCallExpression(t *testing.T) {
return values.True, nil
},
false,
args...,
)
@ -85,5 +89,21 @@ func TestFunctionCallExpression(t *testing.T) {
So(err, ShouldEqual, core.ErrTerminated)
})
Convey("Should ignore errors and return NONE", func() {
f, err := expressions.NewFunctionCallExpression(
core.SourceMap{},
func(ctx context.Context, args ...core.Value) (value core.Value, e error) {
return values.NewString("booo"), core.ErrNotImplemented
},
true,
)
So(err, ShouldBeNil)
out, err := f.Exec(context.Background(), rootScope.Fork())
So(err, ShouldBeNil)
So(out.Type().String(), ShouldEqual, types.None.String())
})
})
}

View File

@ -29,6 +29,10 @@ func (e *MemberExpression) Exec(ctx context.Context, scope *core.Scope) (core.Va
val, err := e.source.Exec(ctx, scope)
if err != nil {
if e.path[0].optional {
return values.None, nil
}
return values.None, core.SourceError(
e.src,
err,