1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-11-27 22:08: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
16 changed files with 1211 additions and 893 deletions

View File

@@ -80,6 +80,21 @@ func TestFunctionNSCall(t *testing.T) {
So(err, ShouldNotBeNil) 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() { Convey("Should use keywords", t, func() {
c := compiler.New() c := compiler.New()

View File

@@ -5,6 +5,7 @@ import (
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/pkg/errors"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"testing" "testing"
) )
@@ -83,6 +84,44 @@ func TestFunctionCall(t *testing.T) {
So(string(out), ShouldEqual, `[2,4,6,8]`) 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) { func BenchmarkFunctionCallArg1(b *testing.B) {

View File

@@ -2,8 +2,10 @@ package compiler_test
import ( import (
"context" "context"
"errors"
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime" "github.com/MontFerret/ferret/pkg/runtime"
"github.com/MontFerret/ferret/pkg/runtime/core"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"testing" "testing"
) )
@@ -71,7 +73,43 @@ func TestLogicalOperators(t *testing.T) {
So(string(out), ShouldEqual, `"foo"`) 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() c := compiler.New()
p, err := c.Compile(` p, err := c.Compile(`

View File

@@ -3,6 +3,7 @@ package compiler_test
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/MontFerret/ferret/pkg/runtime/core"
"testing" "testing"
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
@@ -495,6 +496,25 @@ RETURN o1.first["second"][o2.prop].fourth["fifth"]["bottom"]
So(string(out), ShouldEqual, `"bar"`) 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) return v.doVisitParamContext(param.(*fql.ParamContext), scope)
} }
if fnCall := ctx.FunctionCallExpression(); fnCall != nil { if fnCall := ctx.FunctionCall(); fnCall != nil {
return v.doVisitFunctionCallExpression(fnCall.(*fql.FunctionCallExpressionContext), scope) return v.doVisitFunctionCall(fnCall.(*fql.FunctionCallContext), false, scope)
} }
if objectLiteral := ctx.ObjectLiteral(); objectLiteral != nil { 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) args := make([]core.Expression, 0, 5)
argsCtx := context.Arguments() argsCtx := context.Arguments()
@@ -1170,10 +1170,19 @@ func (v *visitor) doVisitFunctionCallExpression(context *fql.FunctionCallExpress
return expressions.NewFunctionCallExpression( return expressions.NewFunctionCallExpression(
v.getSourceMap(context), v.getSourceMap(context),
fun, fun,
ignoreErrors,
args..., 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) { func (v *visitor) doVisitParamContext(context *fql.ParamContext, s *scope) (core.Expression, error) {
name := context.Identifier().GetText() name := context.Identifier().GetText()

View File

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

View File

@@ -264,9 +264,17 @@ memberExpression
memberExpressionSource memberExpressionSource
: variable : variable
| param | param
| functionCallExpression
| arrayLiteral | arrayLiteral
| objectLiteral | objectLiteral
| functionCall
;
functionCall
: namespace functionIdentifier arguments
;
functionCallExpression
: functionCall QuestionMark?
; ;
memberExpressionPath memberExpressionPath
@@ -274,10 +282,6 @@ memberExpressionPath
| (QuestionMark Dot)? computedPropertyName | (QuestionMark Dot)? computedPropertyName
; ;
functionCallExpression
: namespace functionIdentifier arguments
;
functionIdentifier functionIdentifier
: Identifier : Identifier
| And | 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. // ExitMemberExpressionSource is called when production memberExpressionSource is exited.
func (s *BaseFqlParserListener) ExitMemberExpressionSource(ctx *MemberExpressionSourceContext) {} func (s *BaseFqlParserListener) ExitMemberExpressionSource(ctx *MemberExpressionSourceContext) {}
// EnterMemberExpressionPath is called when production memberExpressionPath is entered. // EnterFunctionCall is called when production functionCall is entered.
func (s *BaseFqlParserListener) EnterMemberExpressionPath(ctx *MemberExpressionPathContext) {} func (s *BaseFqlParserListener) EnterFunctionCall(ctx *FunctionCallContext) {}
// ExitMemberExpressionPath is called when production memberExpressionPath is exited. // ExitFunctionCall is called when production functionCall is exited.
func (s *BaseFqlParserListener) ExitMemberExpressionPath(ctx *MemberExpressionPathContext) {} func (s *BaseFqlParserListener) ExitFunctionCall(ctx *FunctionCallContext) {}
// EnterFunctionCallExpression is called when production functionCallExpression is entered. // EnterFunctionCallExpression is called when production functionCallExpression is entered.
func (s *BaseFqlParserListener) EnterFunctionCallExpression(ctx *FunctionCallExpressionContext) {} func (s *BaseFqlParserListener) EnterFunctionCallExpression(ctx *FunctionCallExpressionContext) {}
@@ -320,6 +320,12 @@ func (s *BaseFqlParserListener) EnterFunctionCallExpression(ctx *FunctionCallExp
// ExitFunctionCallExpression is called when production functionCallExpression is exited. // ExitFunctionCallExpression is called when production functionCallExpression is exited.
func (s *BaseFqlParserListener) ExitFunctionCallExpression(ctx *FunctionCallExpressionContext) {} 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. // EnterFunctionIdentifier is called when production functionIdentifier is entered.
func (s *BaseFqlParserListener) EnterFunctionIdentifier(ctx *FunctionIdentifierContext) {} func (s *BaseFqlParserListener) EnterFunctionIdentifier(ctx *FunctionIdentifierContext) {}

View File

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

View File

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

View File

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

View File

@@ -7,21 +7,23 @@ import (
) )
type FunctionCallExpression struct { type FunctionCallExpression struct {
src core.SourceMap src core.SourceMap
fun core.Function fun core.Function
args []core.Expression ignoreErrors bool
args []core.Expression
} }
func NewFunctionCallExpression( func NewFunctionCallExpression(
src core.SourceMap, src core.SourceMap,
fun core.Function, fun core.Function,
ignoreErrors bool,
args ...core.Expression, args ...core.Expression,
) (*FunctionCallExpression, error) { ) (*FunctionCallExpression, error) {
if fun == nil { if fun == nil {
return nil, core.Error(core.ErrMissedArgument, "function") 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 { func (e *FunctionCallExpression) Arguments() []core.Expression {
@@ -29,6 +31,18 @@ func (e *FunctionCallExpression) Arguments() []core.Expression {
} }
func (e *FunctionCallExpression) Function() core.Function { 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 return e.fun
} }
@@ -49,7 +63,7 @@ func (e *FunctionCallExpression) Exec(ctx context.Context, scope *core.Scope) (c
out, err := arg.Exec(ctx, scope) out, err := arg.Exec(ctx, scope)
if err != nil { if err != nil {
return values.None, core.SourceError(e.src, err) return values.None, e.maybeError(core.SourceError(e.src, err))
} }
args[idx] = out args[idx] = out
@@ -59,9 +73,17 @@ func (e *FunctionCallExpression) Exec(ctx context.Context, scope *core.Scope) (c
} }
if err != nil { if err != nil {
return values.None, core.SourceError(e.src, err) return values.None, e.maybeError(core.SourceError(e.src, err))
} }
return out, nil 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"
"github.com/MontFerret/ferret/pkg/runtime/expressions/literals" "github.com/MontFerret/ferret/pkg/runtime/expressions/literals"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
"testing" "testing"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
@@ -21,6 +22,7 @@ func TestFunctionCallExpression(t *testing.T) {
return values.True, nil return values.True, nil
}, },
false,
) )
So(err, ShouldBeNil) So(err, ShouldBeNil)
@@ -46,6 +48,7 @@ func TestFunctionCallExpression(t *testing.T) {
return values.True, nil return values.True, nil
}, },
false,
args..., args...,
) )
@@ -72,6 +75,7 @@ func TestFunctionCallExpression(t *testing.T) {
return values.True, nil return values.True, nil
}, },
false,
args..., args...,
) )
@@ -85,5 +89,21 @@ func TestFunctionCallExpression(t *testing.T) {
So(err, ShouldEqual, core.ErrTerminated) 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) val, err := e.source.Exec(ctx, scope)
if err != nil { if err != nil {
if e.path[0].optional {
return values.None, nil
}
return values.None, core.SourceError( return values.None, core.SourceError(
e.src, e.src,
err, err,