mirror of
https://github.com/MontFerret/ferret.git
synced 2025-02-09 13:38:35 +02:00
Feature/optimized member expression (#653)
* Added new member path resolution logic * Updated Getter and Setter interfaces * Added ssupport of pre-compiled static member path * Improved error handling
This commit is contained in:
parent
e4e98830a0
commit
8f2957e6ca
@ -557,9 +557,21 @@ func BenchmarkMemberObject(b *testing.B) {
|
||||
|
||||
func BenchmarkMemberObjectComputed(b *testing.B) {
|
||||
p := compiler.New().MustCompile(`
|
||||
LET obj = { "foo": "bar"}
|
||||
LET obj = {
|
||||
first: {
|
||||
second: {
|
||||
third: {
|
||||
fourth: {
|
||||
fifth: {
|
||||
bottom: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RETURN obj["foo"]
|
||||
RETURN obj["first"]["second"]["third"]["fourth"]["fifth"]["bottom"]
|
||||
`)
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/MontFerret/ferret/pkg/runtime/expressions/clauses"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/expressions/literals"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/expressions/operators"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
"github.com/antlr/antlr4/runtime/Go/antlr"
|
||||
"github.com/pkg/errors"
|
||||
"regexp"
|
||||
@ -877,6 +878,8 @@ func (v *visitor) doVisitMemberExpression(ctx *fql.MemberExpressionContext, scop
|
||||
|
||||
children := ctx.AllMemberExpressionPath()
|
||||
path := make([]*expressions.MemberPathSegment, 0, len(children))
|
||||
preCompiledPath := make([]core.Value, 0, len(children))
|
||||
skipOptimization := false
|
||||
|
||||
for _, memberPath := range children {
|
||||
var exp core.Expression
|
||||
@ -903,6 +906,18 @@ func (v *visitor) doVisitMemberExpression(ctx *fql.MemberExpressionContext, scop
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipOptimization {
|
||||
switch t := exp.(type) {
|
||||
case literals.StringLiteral:
|
||||
preCompiledPath = append(preCompiledPath, values.NewString(string(t)))
|
||||
case literals.IntLiteral:
|
||||
preCompiledPath = append(preCompiledPath, values.NewInt(int(t)))
|
||||
default:
|
||||
skipOptimization = true
|
||||
preCompiledPath = nil
|
||||
}
|
||||
}
|
||||
|
||||
path = append(path, segment)
|
||||
}
|
||||
|
||||
@ -910,6 +925,7 @@ func (v *visitor) doVisitMemberExpression(ctx *fql.MemberExpressionContext, scop
|
||||
v.getSourceMap(ctx),
|
||||
source,
|
||||
path,
|
||||
preCompiledPath,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -184,12 +184,12 @@ func (doc *HTMLDocument) Iterate(ctx context.Context) (core.Iterator, error) {
|
||||
return doc.element.Iterate(ctx)
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
return common.GetInDocument(ctx, doc, path)
|
||||
func (doc *HTMLDocument) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
return common.GetInDocument(ctx, path, doc)
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
||||
return common.SetInDocument(ctx, doc, path, value)
|
||||
func (doc *HTMLDocument) SetIn(ctx context.Context, path []core.Value, value core.Value) core.PathError {
|
||||
return common.SetInDocument(ctx, path, doc, value)
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) Close() error {
|
||||
|
@ -185,12 +185,12 @@ func (el *HTMLElement) Iterate(_ context.Context) (core.Iterator, error) {
|
||||
return common.NewIterator(el)
|
||||
}
|
||||
|
||||
func (el *HTMLElement) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
return common.GetInElement(ctx, el, path)
|
||||
func (el *HTMLElement) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
return common.GetInElement(ctx, path, el)
|
||||
}
|
||||
|
||||
func (el *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
||||
return common.SetInElement(ctx, el, path, value)
|
||||
func (el *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core.Value) core.PathError {
|
||||
return common.SetInElement(ctx, path, el, value)
|
||||
}
|
||||
|
||||
func (el *HTMLElement) GetValue(ctx context.Context) (core.Value, error) {
|
||||
|
@ -257,12 +257,12 @@ func (p *HTMLPage) Copy() core.Value {
|
||||
return values.None
|
||||
}
|
||||
|
||||
func (p *HTMLPage) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
return common.GetInPage(ctx, p, path)
|
||||
func (p *HTMLPage) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
return common.GetInPage(ctx, path, p)
|
||||
}
|
||||
|
||||
func (p *HTMLPage) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
||||
return common.SetInPage(ctx, p, path, value)
|
||||
func (p *HTMLPage) SetIn(ctx context.Context, path []core.Value, value core.Value) core.PathError {
|
||||
return common.SetInPage(ctx, path, p, value)
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Iterate(ctx context.Context) (core.Iterator, error) {
|
||||
|
@ -11,12 +11,13 @@ import (
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values/types"
|
||||
)
|
||||
|
||||
func GetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value) (core.Value, error) {
|
||||
func GetInPage(ctx context.Context, path []core.Value, page drivers.HTMLPage) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return page, nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
segmentIdx := 0
|
||||
segment := path[segmentIdx]
|
||||
|
||||
if segment.Type() == types.String {
|
||||
segment := segment.(values.String)
|
||||
@ -26,27 +27,55 @@ func GetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value) (c
|
||||
resp, err := page.GetResponse(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get response")
|
||||
return nil, core.NewPathError(
|
||||
errors.Wrap(err, "get response"),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
return resp.GetIn(ctx, path[1:])
|
||||
out, pathErr := resp.GetIn(ctx, path[segmentIdx+1:])
|
||||
|
||||
if pathErr != nil {
|
||||
return values.None, core.NewPathErrorFrom(pathErr, segmentIdx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case "mainFrame", "document":
|
||||
return GetInDocument(ctx, page.GetMainFrame(), path[1:])
|
||||
out, pathErr := GetInDocument(ctx, path[segmentIdx+1:], page.GetMainFrame())
|
||||
|
||||
if pathErr != nil {
|
||||
return values.None, core.NewPathErrorFrom(pathErr, segmentIdx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case "frames":
|
||||
if len(path) == 1 {
|
||||
return page.GetFrames(ctx)
|
||||
out, err := page.GetFrames(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, core.NewPathError(
|
||||
errors.Wrap(err, "get response"),
|
||||
segmentIdx,
|
||||
)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
idx := path[1]
|
||||
segmentIdx = +1
|
||||
idx := path[segmentIdx]
|
||||
|
||||
if !values.IsNumber(idx) {
|
||||
return values.None, core.TypeError(idx.Type(), types.Int, types.Float)
|
||||
return values.None, core.NewPathError(
|
||||
core.TypeError(idx.Type(), types.Int, types.Float),
|
||||
segmentIdx,
|
||||
)
|
||||
}
|
||||
|
||||
value, err := page.GetFrame(ctx, values.ToInt(idx))
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
return values.None, core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
if len(path) == 2 {
|
||||
@ -56,42 +85,57 @@ func GetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value) (c
|
||||
frame, err := drivers.ToDocument(value)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
return values.None, core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
return GetInDocument(ctx, frame, path[2:])
|
||||
out, pathErr := GetInDocument(ctx, path[segmentIdx+1:], frame)
|
||||
|
||||
if err != nil {
|
||||
return values.None, core.NewPathErrorFrom(pathErr, segmentIdx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case "url", "URL":
|
||||
return page.GetURL(), nil
|
||||
case "cookies":
|
||||
cookies, err := page.GetCookies(ctx)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
return values.None, core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
return cookies.GetIn(ctx, path[1:])
|
||||
out, pathErr := cookies.GetIn(ctx, path[segmentIdx+1:])
|
||||
|
||||
if err != nil {
|
||||
return values.None, core.NewPathErrorFrom(pathErr, segmentIdx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case "title":
|
||||
return page.GetMainFrame().GetTitle(), nil
|
||||
case "isClosed":
|
||||
return page.IsClosed(), nil
|
||||
default:
|
||||
return GetInDocument(ctx, page.GetMainFrame(), path)
|
||||
return GetInDocument(ctx, path, page.GetMainFrame())
|
||||
}
|
||||
}
|
||||
|
||||
return GetInDocument(ctx, page.GetMainFrame(), path)
|
||||
return GetInDocument(ctx, path, page.GetMainFrame())
|
||||
}
|
||||
|
||||
func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Value) (core.Value, error) {
|
||||
func GetInDocument(ctx context.Context, path []core.Value, doc drivers.HTMLDocument) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
var out core.Value
|
||||
var err error
|
||||
segmentIdx := 0
|
||||
segment := path[segmentIdx]
|
||||
|
||||
if segment.Type() == types.String {
|
||||
segment := segment.(values.String)
|
||||
@ -107,7 +151,7 @@ func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Va
|
||||
parent, err := doc.GetParentDocument(ctx)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
return values.None, core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
if parent == nil {
|
||||
@ -118,12 +162,18 @@ func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Va
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
return GetInDocument(ctx, parent, path[1:])
|
||||
out, pathErr := GetInDocument(ctx, path[segmentIdx+1:], parent)
|
||||
|
||||
if pathErr != nil {
|
||||
return values.None, core.NewPathErrorFrom(pathErr, segmentIdx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case "body", "head":
|
||||
out, err := doc.QuerySelector(ctx, segment)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
return values.None, core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
if out == values.None {
|
||||
@ -137,121 +187,127 @@ func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Va
|
||||
el, err := drivers.ToElement(out)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
return values.None, core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
return GetInElement(ctx, el, path[1:])
|
||||
out, pathErr := GetInElement(ctx, path[segmentIdx+1:], el)
|
||||
|
||||
if pathErr != nil {
|
||||
return values.None, core.NewPathErrorFrom(pathErr, segmentIdx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case "innerHTML":
|
||||
return doc.GetElement().GetInnerHTML(ctx)
|
||||
out, err = doc.GetElement().GetInnerHTML(ctx)
|
||||
case "innerText":
|
||||
return doc.GetElement().GetInnerText(ctx)
|
||||
out, err = doc.GetElement().GetInnerText(ctx)
|
||||
default:
|
||||
return GetInNode(ctx, doc.GetElement(), path)
|
||||
return GetInNode(ctx, path, doc.GetElement())
|
||||
}
|
||||
|
||||
return values.ReturnOrNext(ctx, path, segmentIdx, out, err)
|
||||
}
|
||||
|
||||
return GetInNode(ctx, doc.GetElement(), path)
|
||||
return GetInNode(ctx, path, doc.GetElement())
|
||||
}
|
||||
|
||||
func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value) (core.Value, error) {
|
||||
func GetInElement(ctx context.Context, path []core.Value, el drivers.HTMLElement) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return el, nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
segmentIdx := 0
|
||||
segment := path[segmentIdx]
|
||||
|
||||
if segment.Type() == types.String {
|
||||
var out core.Value
|
||||
var err error
|
||||
|
||||
segment := segment.(values.String)
|
||||
|
||||
switch segment {
|
||||
case "innerText":
|
||||
return el.GetInnerText(ctx)
|
||||
out, err = el.GetInnerText(ctx)
|
||||
case "innerHTML":
|
||||
return el.GetInnerHTML(ctx)
|
||||
out, err = el.GetInnerHTML(ctx)
|
||||
case "value":
|
||||
return el.GetValue(ctx)
|
||||
out, err = el.GetValue(ctx)
|
||||
case "attributes":
|
||||
attrs, err := el.GetAttributes(ctx)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
return attrs, nil
|
||||
}
|
||||
out, err = el.GetAttributes(ctx)
|
||||
} else {
|
||||
// e.g. attributes.href
|
||||
segmentIdx++
|
||||
attrName := path[segmentIdx]
|
||||
|
||||
return values.GetIn(ctx, attrs, path[1:])
|
||||
out, err = el.GetAttribute(ctx, values.ToString(attrName))
|
||||
}
|
||||
case "style":
|
||||
styles, err := el.GetStyles(ctx)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
return styles, nil
|
||||
}
|
||||
out, err = el.GetStyles(ctx)
|
||||
} else {
|
||||
// e.g. style.color
|
||||
segmentIdx++
|
||||
styleName := path[segmentIdx]
|
||||
|
||||
return values.GetIn(ctx, styles, path[1:])
|
||||
out, err = el.GetStyle(ctx, values.ToString(styleName))
|
||||
}
|
||||
case "previousElementSibling":
|
||||
return el.GetPreviousElementSibling(ctx)
|
||||
out, err = el.GetPreviousElementSibling(ctx)
|
||||
case "nextElementSibling":
|
||||
return el.GetNextElementSibling(ctx)
|
||||
out, err = el.GetNextElementSibling(ctx)
|
||||
case "parentElement":
|
||||
return el.GetParentElement(ctx)
|
||||
out, err = el.GetParentElement(ctx)
|
||||
default:
|
||||
return GetInNode(ctx, el, path)
|
||||
return GetInNode(ctx, path, el)
|
||||
}
|
||||
|
||||
return values.ReturnOrNext(ctx, path, segmentIdx, out, err)
|
||||
}
|
||||
|
||||
return GetInNode(ctx, el, path)
|
||||
return GetInNode(ctx, path, el)
|
||||
}
|
||||
|
||||
func GetInNode(ctx context.Context, node drivers.HTMLNode, path []core.Value) (core.Value, error) {
|
||||
func GetInNode(ctx context.Context, path []core.Value, node drivers.HTMLNode) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return node, nil
|
||||
}
|
||||
|
||||
nt := node.Type()
|
||||
segment := path[0]
|
||||
st := segment.Type()
|
||||
segmentIdx := 0
|
||||
segment := path[segmentIdx]
|
||||
|
||||
switch st {
|
||||
var out core.Value
|
||||
var err error
|
||||
|
||||
switch segment.Type() {
|
||||
case types.Int:
|
||||
if nt == drivers.HTMLElementType || nt == drivers.HTMLDocumentType {
|
||||
re := node.(drivers.HTMLNode)
|
||||
|
||||
return re.GetChildNode(ctx, values.ToInt(segment))
|
||||
}
|
||||
|
||||
return values.GetIn(ctx, node, path[1:])
|
||||
out, err = node.GetChildNode(ctx, values.ToInt(segment))
|
||||
case types.String:
|
||||
segment := segment.(values.String)
|
||||
|
||||
switch segment {
|
||||
case "nodeType":
|
||||
return node.GetNodeType(ctx)
|
||||
out, err = node.GetNodeType(ctx)
|
||||
case "nodeName":
|
||||
return node.GetNodeName(ctx)
|
||||
out, err = node.GetNodeName(ctx)
|
||||
case "children":
|
||||
children, err := node.GetChildNodes(ctx)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
if len(path) == 1 {
|
||||
return children, nil
|
||||
out, err = node.GetChildNodes(ctx)
|
||||
} else {
|
||||
segmentIdx++
|
||||
out, err = node.GetChildNode(ctx, values.ToInt(path[segmentIdx]))
|
||||
}
|
||||
|
||||
return values.GetIn(ctx, children, path[1:])
|
||||
case "length":
|
||||
return node.Length(), nil
|
||||
default:
|
||||
return values.None, nil
|
||||
}
|
||||
default:
|
||||
return values.None, core.TypeError(st, types.Int, types.String)
|
||||
return values.None, core.NewPathError(
|
||||
core.TypeError(segment.Type(), types.Int, types.String),
|
||||
segmentIdx,
|
||||
)
|
||||
}
|
||||
|
||||
return values.ReturnOrNext(ctx, path, segmentIdx, out, err)
|
||||
}
|
||||
|
@ -9,28 +9,29 @@ import (
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values/types"
|
||||
)
|
||||
|
||||
func SetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value, value core.Value) error {
|
||||
func SetInPage(ctx context.Context, path []core.Value, page drivers.HTMLPage, value core.Value) core.PathError {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return SetInDocument(ctx, page.GetMainFrame(), path, value)
|
||||
return SetInDocument(ctx, path, page.GetMainFrame(), value)
|
||||
}
|
||||
|
||||
func SetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Value, value core.Value) error {
|
||||
func SetInDocument(ctx context.Context, path []core.Value, doc drivers.HTMLDocument, value core.Value) core.PathError {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return SetInNode(ctx, doc, path, value)
|
||||
return SetInNode(ctx, path, doc, value)
|
||||
}
|
||||
|
||||
func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value, value core.Value) error {
|
||||
func SetInElement(ctx context.Context, path []core.Value, el drivers.HTMLElement, value core.Value) core.PathError {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
segmentIdx := 0
|
||||
segment := path[segmentIdx]
|
||||
|
||||
if segment.Type() == types.String {
|
||||
segment := segment.(values.String)
|
||||
@ -39,27 +40,32 @@ func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value
|
||||
case "attributes":
|
||||
if len(path) > 1 {
|
||||
attrName := path[1]
|
||||
err := el.SetAttribute(ctx, values.NewString(attrName.String()), values.NewString(value.String()))
|
||||
|
||||
return el.SetAttribute(ctx, values.NewString(attrName.String()), values.NewString(value.String()))
|
||||
if err != nil {
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := core.ValidateType(value, types.Object)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
curr, err := el.GetAttributes(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
// remove all previous attributes
|
||||
err = el.RemoveAttribute(ctx, curr.Keys()...)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
obj := value.(*values.Object)
|
||||
@ -69,24 +75,34 @@ func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value
|
||||
return err == nil
|
||||
})
|
||||
|
||||
return err
|
||||
if err != nil {
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
return nil
|
||||
case "style":
|
||||
if len(path) > 1 {
|
||||
attrName := path[1]
|
||||
|
||||
return el.SetStyle(ctx, values.NewString(attrName.String()), values.NewString(value.String()))
|
||||
err := el.SetStyle(ctx, values.NewString(attrName.String()), values.NewString(value.String()))
|
||||
|
||||
if err != nil {
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := core.ValidateType(value, types.Object)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
styles, err := el.GetStyles(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
err = el.RemoveStyle(ctx, styles.Keys()...)
|
||||
@ -98,30 +114,33 @@ func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value
|
||||
return err == nil
|
||||
})
|
||||
|
||||
return err
|
||||
case "value":
|
||||
if len(path) > 1 {
|
||||
return core.Error(ErrInvalidPath, PathToString(path[1:]))
|
||||
if err != nil {
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
return el.SetValue(ctx, value)
|
||||
return nil
|
||||
case "value":
|
||||
if len(path) > 1 {
|
||||
return core.NewPathError(ErrInvalidPath, segmentIdx+1)
|
||||
}
|
||||
|
||||
err := el.SetValue(ctx, value)
|
||||
|
||||
if err != nil {
|
||||
return core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return SetInNode(ctx, el, path, value)
|
||||
return SetInNode(ctx, path, el, value)
|
||||
}
|
||||
|
||||
func SetInNode(_ context.Context, _ drivers.HTMLNode, path []core.Value, _ core.Value) error {
|
||||
func SetInNode(_ context.Context, path []core.Value, _ drivers.HTMLNode, _ core.Value) core.PathError {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
st := segment.Type()
|
||||
|
||||
if st == types.Int {
|
||||
return core.Error(core.ErrInvalidOperation, "children are read-only")
|
||||
}
|
||||
|
||||
return core.Error(ErrReadOnly, PathToString(path))
|
||||
return core.NewPathError(ErrReadOnly, 0)
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values/types"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -162,20 +161,14 @@ func (c HTTPCookie) MarshalJSON() ([]byte, error) {
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (c HTTPCookie) GetIn(_ context.Context, path []core.Value) (core.Value, error) {
|
||||
func (c HTTPCookie) GetIn(_ context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return values.None, nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
|
||||
err := core.ValidateType(segment, types.String)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
switch segment.(values.String) {
|
||||
switch values.ToString(segment) {
|
||||
case "name":
|
||||
return values.NewString(c.Name), nil
|
||||
case "value":
|
||||
|
@ -175,17 +175,18 @@ func (c *HTTPCookies) Set(cookie HTTPCookie) {
|
||||
c.values[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
func (c *HTTPCookies) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
func (c *HTTPCookies) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return values.None, nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
segmentIdx := 0
|
||||
segment := path[segmentIdx]
|
||||
|
||||
err := core.ValidateType(segment, types.String)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
return values.None, core.NewPathError(err, segmentIdx)
|
||||
}
|
||||
|
||||
cookie, found := c.values[segment.String()]
|
||||
@ -195,7 +196,7 @@ func (c *HTTPCookies) GetIn(ctx context.Context, path []core.Value) (core.Value,
|
||||
return cookie, nil
|
||||
}
|
||||
|
||||
return values.GetIn(ctx, cookie, path[1:])
|
||||
return values.GetIn(ctx, cookie, path[segmentIdx+1:])
|
||||
}
|
||||
|
||||
return values.None, nil
|
||||
|
@ -11,8 +11,6 @@ import (
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values/types"
|
||||
|
||||
"github.com/wI2L/jettison"
|
||||
)
|
||||
|
||||
@ -159,20 +157,15 @@ func (h *HTTPHeaders) Get(key string) string {
|
||||
return textproto.MIMEHeader(h.values).Get(key)
|
||||
}
|
||||
|
||||
func (h *HTTPHeaders) GetIn(_ context.Context, path []core.Value) (core.Value, error) {
|
||||
func (h *HTTPHeaders) GetIn(_ context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return values.None, nil
|
||||
}
|
||||
|
||||
segment := path[0]
|
||||
segmentIx := 0
|
||||
segment := path[segmentIx]
|
||||
|
||||
err := core.ValidateType(segment, types.String)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
return values.NewString(h.Get(segment.String())), nil
|
||||
return values.NewString(h.Get(string(values.ToString(segment)))), nil
|
||||
}
|
||||
|
||||
func (h *HTTPHeaders) ForEach(predicate func(value []string, key string) bool) {
|
||||
|
@ -134,12 +134,12 @@ func (doc *HTMLDocument) Iterate(_ context.Context) (core.Iterator, error) {
|
||||
return common.NewIterator(doc.element)
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
return common.GetInDocument(ctx, doc, path)
|
||||
func (doc *HTMLDocument) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
return common.GetInDocument(ctx, path, doc)
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
||||
return common.SetInDocument(ctx, doc, path, value)
|
||||
func (doc *HTMLDocument) SetIn(ctx context.Context, path []core.Value, value core.Value) core.PathError {
|
||||
return common.SetInDocument(ctx, path, doc, value)
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) GetNodeType(_ context.Context) (values.Int, error) {
|
||||
|
@ -481,12 +481,12 @@ func (el *HTMLElement) ExistsBySelector(_ context.Context, selector values.Strin
|
||||
return values.True, nil
|
||||
}
|
||||
|
||||
func (el *HTMLElement) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
return common.GetInElement(ctx, el, path)
|
||||
func (el *HTMLElement) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
return common.GetInElement(ctx, path, el)
|
||||
}
|
||||
|
||||
func (el *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
||||
return common.SetInElement(ctx, el, path, value)
|
||||
func (el *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core.Value) core.PathError {
|
||||
return common.SetInElement(ctx, path, el, value)
|
||||
}
|
||||
|
||||
func (el *HTMLElement) Iterate(_ context.Context) (core.Iterator, error) {
|
||||
|
@ -108,12 +108,12 @@ func (p *HTMLPage) Iterate(ctx context.Context) (core.Iterator, error) {
|
||||
return p.document.Iterate(ctx)
|
||||
}
|
||||
|
||||
func (p *HTMLPage) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
return common.GetInPage(ctx, p, path)
|
||||
func (p *HTMLPage) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
return common.GetInPage(ctx, path, p)
|
||||
}
|
||||
|
||||
func (p *HTMLPage) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
||||
return common.SetInPage(ctx, p, path, value)
|
||||
func (p *HTMLPage) SetIn(ctx context.Context, path []core.Value, value core.Value) core.PathError {
|
||||
return common.SetInPage(ctx, path, p, value)
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Length() values.Int {
|
||||
|
@ -77,16 +77,19 @@ func (resp *HTTPResponse) MarshalJSON() ([]byte, error) {
|
||||
return jettison.MarshalOpts(responseMarshal(*resp), jettison.NoHTMLEscaping())
|
||||
}
|
||||
|
||||
func (resp *HTTPResponse) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
func (resp *HTTPResponse) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if typ := path[0].Type(); typ != types.String {
|
||||
return values.None, core.TypeError(typ, types.String)
|
||||
segmentIdx := 0
|
||||
segment := path[segmentIdx]
|
||||
|
||||
if typ := segment.Type(); typ != types.String {
|
||||
return values.None, core.NewPathError(core.TypeError(typ, types.String), segmentIdx)
|
||||
}
|
||||
|
||||
field := path[0].(values.String).String()
|
||||
field := segment.String()
|
||||
|
||||
switch field {
|
||||
case "url", "URL":
|
||||
@ -100,7 +103,13 @@ func (resp *HTTPResponse) GetIn(ctx context.Context, path []core.Value) (core.Va
|
||||
return resp.Headers, nil
|
||||
}
|
||||
|
||||
return resp.Headers.GetIn(ctx, path[1:])
|
||||
out, pathErr := resp.Headers.GetIn(ctx, path[1:])
|
||||
|
||||
if pathErr != nil {
|
||||
return values.None, core.NewPathErrorFrom(pathErr, segmentIdx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
case "responseTime":
|
||||
return values.NewFloat(resp.ResponseTime), nil
|
||||
|
||||
|
@ -7,9 +7,65 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SourceErrorDetail struct {
|
||||
BaseError error
|
||||
ComputeError error
|
||||
type (
|
||||
SourceErrorDetail struct {
|
||||
error
|
||||
BaseError error
|
||||
ComputeError error
|
||||
}
|
||||
|
||||
// PathError represents an interface of
|
||||
// error type which returned when an error occurs during an execution of Getter.GetIn or Setter.SetIn functions
|
||||
// and contains segment of a given path that caused the error.
|
||||
PathError interface {
|
||||
error
|
||||
Cause() error
|
||||
Segment() int
|
||||
Format(path []Value) string
|
||||
}
|
||||
|
||||
// NativePathError represents a default implementation of GetterError interface.
|
||||
NativePathError struct {
|
||||
cause error
|
||||
segment int
|
||||
}
|
||||
)
|
||||
|
||||
// NewPathError is a constructor function of NativePathError struct.
|
||||
func NewPathError(err error, segment int) PathError {
|
||||
return &NativePathError{
|
||||
cause: err,
|
||||
segment: segment,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPathErrorFrom is a constructor function of NativePathError struct
|
||||
// that accepts nested PathError and original segment index.
|
||||
// It sums indexes to get the correct one that points to original path.
|
||||
func NewPathErrorFrom(err PathError, segment int) PathError {
|
||||
return NewPathError(err.Cause(), err.Segment()+segment)
|
||||
}
|
||||
|
||||
func (e *NativePathError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e *NativePathError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e *NativePathError) Segment() int {
|
||||
return e.segment
|
||||
}
|
||||
|
||||
func (e *NativePathError) Format(path []Value) string {
|
||||
err := e.cause
|
||||
|
||||
if err == ErrInvalidPath && len(path) > e.segment {
|
||||
return err.Error() + " '" + path[e.segment].String() + "'"
|
||||
}
|
||||
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func (e *SourceErrorDetail) Error() string {
|
||||
@ -30,6 +86,7 @@ var (
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
ErrNotSupported = errors.New("not supported")
|
||||
ErrNoMoreData = errors.New("no more data")
|
||||
ErrInvalidPath = errors.New("cannot read property")
|
||||
)
|
||||
|
||||
const typeErrorTemplate = "expected %s, but got %s"
|
||||
|
@ -2,6 +2,20 @@ package core
|
||||
|
||||
import "context"
|
||||
|
||||
type Expression interface {
|
||||
Exec(ctx context.Context, scope *Scope) (Value, error)
|
||||
type (
|
||||
Expression interface {
|
||||
Exec(ctx context.Context, scope *Scope) (Value, error)
|
||||
}
|
||||
|
||||
ExpressionFn struct {
|
||||
fn func(ctx context.Context, scope *Scope) (Value, error)
|
||||
}
|
||||
)
|
||||
|
||||
func NewExpressionFn(fn func(ctx context.Context, scope *Scope) (Value, error)) Expression {
|
||||
return &ExpressionFn{fn}
|
||||
}
|
||||
|
||||
func (f *ExpressionFn) Exec(ctx context.Context, scope *Scope) (Value, error) {
|
||||
return f.fn(ctx, scope)
|
||||
}
|
||||
|
21
pkg/runtime/core/getter.go
Normal file
21
pkg/runtime/core/getter.go
Normal file
@ -0,0 +1,21 @@
|
||||
package core
|
||||
|
||||
import "context"
|
||||
|
||||
type (
|
||||
GetterPathIterator interface {
|
||||
Path() []Value
|
||||
Current() Value
|
||||
CurrentIndex() int
|
||||
}
|
||||
|
||||
// Getter represents an interface of
|
||||
// complex types that needs to be used to read values by path.
|
||||
// The interface is created to let user-defined types be used in dot notation data access.
|
||||
Getter interface {
|
||||
GetIn(ctx context.Context, path []Value) (Value, PathError)
|
||||
}
|
||||
|
||||
// GetterFn represents a type of helper functions that implement complex path resolutions.
|
||||
GetterFn func(ctx context.Context, path []Value, src Getter) (Value, PathError)
|
||||
)
|
12
pkg/runtime/core/setter.go
Normal file
12
pkg/runtime/core/setter.go
Normal file
@ -0,0 +1,12 @@
|
||||
package core
|
||||
|
||||
import "context"
|
||||
|
||||
type (
|
||||
// Setter represents an interface of
|
||||
// complex types that needs to be used to write values by path.
|
||||
// The interface is created to let user-defined types be used in dot notation assignment.
|
||||
Setter interface {
|
||||
SetIn(ctx context.Context, path []Value, value Value) PathError
|
||||
}
|
||||
)
|
@ -28,18 +28,4 @@ type (
|
||||
Iterator interface {
|
||||
Next(ctx context.Context) (value Value, key Value, err error)
|
||||
}
|
||||
|
||||
// Getter represents an interface of
|
||||
// complex types that needs to be used to read values by path.
|
||||
// The interface is created to let user-defined types be used in dot notation data access.
|
||||
Getter interface {
|
||||
GetIn(ctx context.Context, path []Value) (Value, error)
|
||||
}
|
||||
|
||||
// Setter represents an interface of
|
||||
// complex types that needs to be used to write values by path.
|
||||
// The interface is created to let user-defined types be used in dot notation assignment.
|
||||
Setter interface {
|
||||
SetIn(ctx context.Context, path []Value, value Value) error
|
||||
}
|
||||
)
|
||||
|
@ -2,18 +2,20 @@ package expressions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
)
|
||||
|
||||
type MemberExpression struct {
|
||||
src core.SourceMap
|
||||
source core.Expression
|
||||
path []*MemberPathSegment
|
||||
src core.SourceMap
|
||||
source core.Expression
|
||||
path []*MemberPathSegment
|
||||
preCompiledPath []core.Value
|
||||
}
|
||||
|
||||
func NewMemberExpression(src core.SourceMap, source core.Expression, path []*MemberPathSegment) (*MemberExpression, error) {
|
||||
func NewMemberExpression(src core.SourceMap, source core.Expression, path []*MemberPathSegment, preCompiledPath []core.Value) (*MemberExpression, error) {
|
||||
if source == nil {
|
||||
return nil, core.Error(core.ErrMissedArgument, "source")
|
||||
}
|
||||
@ -22,11 +24,11 @@ func NewMemberExpression(src core.SourceMap, source core.Expression, path []*Mem
|
||||
return nil, core.Error(core.ErrMissedArgument, "path expressions")
|
||||
}
|
||||
|
||||
return &MemberExpression{src, source, path}, nil
|
||||
return &MemberExpression{src, source, path, preCompiledPath}, nil
|
||||
}
|
||||
|
||||
func (e *MemberExpression) Exec(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
val, err := e.source.Exec(ctx, scope)
|
||||
member, err := e.source.Exec(ctx, scope)
|
||||
|
||||
if err != nil {
|
||||
if e.path[0].optional {
|
||||
@ -39,28 +41,49 @@ func (e *MemberExpression) Exec(ctx context.Context, scope *core.Scope) (core.Va
|
||||
)
|
||||
}
|
||||
|
||||
out := val
|
||||
path := make([]core.Value, 1)
|
||||
var segments = e.preCompiledPath
|
||||
|
||||
for _, seg := range e.path {
|
||||
segment, err := seg.exp.Exec(ctx, scope)
|
||||
if e.preCompiledPath == nil {
|
||||
segments = make([]core.Value, len(e.path))
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
// unfold the path
|
||||
for i, seg := range e.path {
|
||||
segment, err := seg.exp.Exec(ctx, scope)
|
||||
|
||||
path[0] = segment
|
||||
c, err := values.GetIn(ctx, out, path)
|
||||
|
||||
if err != nil {
|
||||
if !seg.optional {
|
||||
return values.None, core.SourceError(e.src, err)
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
return values.None, nil
|
||||
segments[i] = segment
|
||||
}
|
||||
}
|
||||
|
||||
var pathErr core.PathError
|
||||
var out core.Value = values.None
|
||||
|
||||
getter, ok := member.(core.Getter)
|
||||
|
||||
if ok {
|
||||
out, pathErr = getter.GetIn(ctx, segments)
|
||||
} else {
|
||||
out, pathErr = values.GetIn(ctx, member, segments)
|
||||
}
|
||||
|
||||
if pathErr != nil {
|
||||
segmentIdx := pathErr.Segment()
|
||||
// if invalid index is returned, we ignore the optionality check
|
||||
// and return the pathErr
|
||||
if segmentIdx >= len(e.path) {
|
||||
return values.None, errors.New(pathErr.Format(segments))
|
||||
}
|
||||
|
||||
out = c
|
||||
segment := e.path[segmentIdx]
|
||||
|
||||
if !segment.optional {
|
||||
return values.None, errors.New(pathErr.Format(segments))
|
||||
}
|
||||
|
||||
return values.None, nil
|
||||
}
|
||||
|
||||
return out, nil
|
||||
|
295
pkg/runtime/expressions/member_test.go
Normal file
295
pkg/runtime/expressions/member_test.go
Normal file
@ -0,0 +1,295 @@
|
||||
package expressions_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/expressions"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values/types"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type (
|
||||
TestObject struct {
|
||||
mock.Mock
|
||||
*values.Object
|
||||
failAt string
|
||||
}
|
||||
)
|
||||
|
||||
func NewTestObject() *TestObject {
|
||||
o := new(TestObject)
|
||||
o.Object = values.NewObject()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (to *TestObject) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
to.Mock.Called(path)
|
||||
|
||||
var current core.Value = to.Object
|
||||
|
||||
for i, segment := range path {
|
||||
if segment.String() == to.failAt {
|
||||
return values.None, core.NewPathError(core.ErrInvalidPath, i)
|
||||
}
|
||||
|
||||
next, err := values.GetIn(ctx, current, []core.Value{segment})
|
||||
|
||||
if err != nil {
|
||||
return values.None, core.NewPathError(err, i)
|
||||
}
|
||||
|
||||
current = next
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func TestMemberExpression(t *testing.T) {
|
||||
Convey(".Exec", t, func() {
|
||||
Convey("Should use .Getter interface if a source implements it", func() {
|
||||
o := NewTestObject()
|
||||
o.Set("foo", values.NewObjectWith(
|
||||
values.NewObjectProperty("bar", values.NewObjectWith(
|
||||
values.NewObjectProperty("baz", values.NewObject()),
|
||||
)),
|
||||
))
|
||||
|
||||
args := []core.Value{
|
||||
values.NewString("foo"),
|
||||
values.NewString("bar"),
|
||||
values.NewString("baz"),
|
||||
}
|
||||
|
||||
o.On("GetIn", args)
|
||||
|
||||
s1, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[0], nil
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
s2, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[1], nil
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
s3, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[2], nil
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
segments := []*expressions.MemberPathSegment{s1, s2, s3}
|
||||
|
||||
exp, err := expressions.NewMemberExpression(
|
||||
core.SourceMap{},
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return o, nil
|
||||
}),
|
||||
segments,
|
||||
nil,
|
||||
)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
root, cancel := core.NewRootScope()
|
||||
|
||||
defer func() {
|
||||
if err := cancel(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := exp.Exec(context.Background(), root.Fork())
|
||||
So(err, ShouldBeNil)
|
||||
So(out.Type().String(), ShouldNotEqual, types.None.String())
|
||||
|
||||
o.AssertExpectations(t)
|
||||
})
|
||||
|
||||
Convey("Should use generic traverse logic if a source does not implement Getter interface", func() {
|
||||
o := values.NewString("abcdefg")
|
||||
|
||||
args := []core.Value{
|
||||
values.NewInt(0),
|
||||
}
|
||||
|
||||
s1, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[0], nil
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
segments := []*expressions.MemberPathSegment{s1}
|
||||
|
||||
exp, err := expressions.NewMemberExpression(
|
||||
core.SourceMap{},
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return o, nil
|
||||
}),
|
||||
segments,
|
||||
nil,
|
||||
)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
root, cancel := core.NewRootScope()
|
||||
|
||||
defer func() {
|
||||
if err := cancel(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := exp.Exec(context.Background(), root.Fork())
|
||||
So(err, ShouldBeNil)
|
||||
So(out.String(), ShouldEqual, "a")
|
||||
})
|
||||
|
||||
Convey("When path is not optional", func() {
|
||||
Convey("Should return an error if it occurs during path resolution", func() {
|
||||
o := NewTestObject()
|
||||
o.failAt = "bar"
|
||||
o.Set("foo", values.NewObjectWith(
|
||||
values.NewObjectProperty("bar", values.NewObjectWith(
|
||||
values.NewObjectProperty("baz", values.NewObject()),
|
||||
)),
|
||||
))
|
||||
|
||||
args := []core.Value{
|
||||
values.NewString("foo"),
|
||||
values.NewString("bar"),
|
||||
values.NewString("baz"),
|
||||
}
|
||||
|
||||
o.On("GetIn", mock.Anything)
|
||||
|
||||
s1, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[0], nil
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
s2, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[1], nil
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
s3, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[2], nil
|
||||
}),
|
||||
false,
|
||||
)
|
||||
|
||||
segments := []*expressions.MemberPathSegment{s1, s2, s3}
|
||||
|
||||
exp, err := expressions.NewMemberExpression(
|
||||
core.SourceMap{},
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return o, nil
|
||||
}),
|
||||
segments,
|
||||
nil,
|
||||
)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
root, cancel := core.NewRootScope()
|
||||
|
||||
defer func() {
|
||||
if err := cancel(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = exp.Exec(context.Background(), root.Fork())
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldEqual, core.NewPathError(core.ErrInvalidPath, 1).Format(args))
|
||||
|
||||
o.AssertExpectations(t)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When path is optional", func() {
|
||||
Convey("Should return None if it occurs during path resolution", func() {
|
||||
o := NewTestObject()
|
||||
o.failAt = "bar"
|
||||
o.Set("foo", values.NewObjectWith(
|
||||
values.NewObjectProperty("bar", values.NewObjectWith(
|
||||
values.NewObjectProperty("baz", values.NewObject()),
|
||||
)),
|
||||
))
|
||||
|
||||
args := []core.Value{
|
||||
values.NewString("foo"),
|
||||
values.NewString("bar"),
|
||||
values.NewString("baz"),
|
||||
}
|
||||
|
||||
o.On("GetIn", mock.Anything)
|
||||
|
||||
s1, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[0], nil
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
s2, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[1], nil
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
s3, _ := expressions.NewMemberPathSegment(
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return args[2], nil
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
segments := []*expressions.MemberPathSegment{s1, s2, s3}
|
||||
|
||||
exp, err := expressions.NewMemberExpression(
|
||||
core.SourceMap{},
|
||||
core.NewExpressionFn(func(ctx context.Context, scope *core.Scope) (core.Value, error) {
|
||||
return o, nil
|
||||
}),
|
||||
segments,
|
||||
nil,
|
||||
)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
root, cancel := core.NewRootScope()
|
||||
|
||||
defer func() {
|
||||
if err := cancel(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := exp.Exec(context.Background(), root.Fork())
|
||||
So(err, ShouldBeNil)
|
||||
So(out.Type().String(), ShouldEqual, values.None.Type().String())
|
||||
|
||||
o.AssertExpectations(t)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -277,28 +277,34 @@ func (t *Array) SortWith(sorter ArraySorter) *Array {
|
||||
return res
|
||||
}
|
||||
|
||||
func (t *Array) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
func (t *Array) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return None, nil
|
||||
}
|
||||
|
||||
if typ := path[0].Type(); typ != types.Int {
|
||||
return None, core.TypeError(typ, types.Int)
|
||||
segmentIdx := 0
|
||||
|
||||
if typ := path[segmentIdx].Type(); typ != types.Int {
|
||||
return None, core.NewPathError(core.TypeError(typ, types.Int), segmentIdx)
|
||||
}
|
||||
|
||||
first := t.Get(path[0].(Int))
|
||||
first := t.Get(path[segmentIdx].(Int))
|
||||
|
||||
if len(path) == 1 {
|
||||
return first, nil
|
||||
}
|
||||
|
||||
getter, ok := first.(core.Getter)
|
||||
if !ok {
|
||||
return None, core.TypeError(
|
||||
first.Type(),
|
||||
core.NewType("Getter"),
|
||||
)
|
||||
segmentIdx++
|
||||
|
||||
if first == None || first == nil {
|
||||
return None, core.NewPathError(core.ErrInvalidPath, segmentIdx)
|
||||
}
|
||||
|
||||
return getter.GetIn(ctx, path[1:])
|
||||
getter, ok := first.(core.Getter)
|
||||
|
||||
if !ok {
|
||||
return GetIn(ctx, first, path[segmentIdx:])
|
||||
}
|
||||
|
||||
return getter.GetIn(ctx, path[segmentIdx:])
|
||||
}
|
||||
|
@ -15,21 +15,53 @@ import (
|
||||
)
|
||||
|
||||
// GetIn checks that from implements core.Getter interface. If it implements,
|
||||
// GetIn call from.GetIn method, otherwise return error.
|
||||
func GetIn(ctx context.Context, from core.Value, byPath []core.Value) (core.Value, error) {
|
||||
getter, ok := from.(core.Getter)
|
||||
|
||||
if !ok {
|
||||
return None, core.TypeError(
|
||||
from.Type(),
|
||||
core.NewType("Getter"),
|
||||
)
|
||||
// GetIn call from.GetIn method, otherwise iterates over values and tries to resolve a given path.
|
||||
func GetIn(ctx context.Context, from core.Value, byPath []core.Value) (core.Value, core.PathError) {
|
||||
if len(byPath) == 0 {
|
||||
return None, nil
|
||||
}
|
||||
|
||||
return getter.GetIn(ctx, byPath)
|
||||
var result = from
|
||||
|
||||
for i, segment := range byPath {
|
||||
if result == None || result == nil {
|
||||
break
|
||||
}
|
||||
|
||||
segType := segment.Type()
|
||||
|
||||
switch curVal := result.(type) {
|
||||
case *Object:
|
||||
result, _ = curVal.Get(ToString(segment))
|
||||
case *Array:
|
||||
if segType != types.Int {
|
||||
return nil, core.NewPathError(
|
||||
core.TypeError(segType, types.Int),
|
||||
i,
|
||||
)
|
||||
}
|
||||
|
||||
result = curVal.Get(segment.(Int))
|
||||
case String:
|
||||
if segType != types.Int {
|
||||
return nil, core.NewPathError(
|
||||
core.TypeError(segType, types.Int),
|
||||
i,
|
||||
)
|
||||
}
|
||||
|
||||
result = curVal.At(ToInt(segment))
|
||||
case core.Getter:
|
||||
return curVal.GetIn(ctx, byPath[i:])
|
||||
default:
|
||||
return None, core.NewPathError(core.ErrInvalidPath, i)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func SetIn(ctx context.Context, to core.Value, byPath []core.Value, value core.Value) error {
|
||||
func SetIn(ctx context.Context, to core.Value, byPath []core.Value, value core.Value) core.PathError {
|
||||
if len(byPath) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -46,7 +78,10 @@ func SetIn(ctx context.Context, to core.Value, byPath []core.Value, value core.V
|
||||
switch parVal := parent.(type) {
|
||||
case *Object:
|
||||
if segmentType != types.String {
|
||||
return core.TypeError(segmentType, types.String)
|
||||
return core.NewPathError(
|
||||
core.TypeError(segmentType, types.String),
|
||||
idx,
|
||||
)
|
||||
}
|
||||
|
||||
if !isTarget {
|
||||
@ -56,14 +91,17 @@ func SetIn(ctx context.Context, to core.Value, byPath []core.Value, value core.V
|
||||
}
|
||||
case *Array:
|
||||
if segmentType != types.Int {
|
||||
return core.TypeError(segmentType, types.Int)
|
||||
return core.NewPathError(
|
||||
core.TypeError(segmentType, types.Int),
|
||||
idx,
|
||||
)
|
||||
}
|
||||
|
||||
if !isTarget {
|
||||
current = parVal.Get(segment.(Int))
|
||||
} else {
|
||||
if err := parVal.Set(segment.(Int), value); err != nil {
|
||||
return err
|
||||
return core.NewPathError(err, idx)
|
||||
}
|
||||
}
|
||||
case core.Setter:
|
||||
@ -78,7 +116,7 @@ func SetIn(ctx context.Context, to core.Value, byPath []core.Value, value core.V
|
||||
parent = obj
|
||||
|
||||
if segmentType != types.String {
|
||||
return core.TypeError(segmentType, types.String)
|
||||
return core.NewPathError(core.TypeError(segmentType, types.String), idx)
|
||||
}
|
||||
|
||||
if isTarget {
|
||||
@ -90,13 +128,21 @@ func SetIn(ctx context.Context, to core.Value, byPath []core.Value, value core.V
|
||||
|
||||
if isTarget {
|
||||
if err := arr.Set(segment.(Int), value); err != nil {
|
||||
return err
|
||||
return core.NewPathError(err, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set new parent
|
||||
if err := SetIn(ctx, to, byPath[0:idx-1], parent); err != nil {
|
||||
nextPath := byPath
|
||||
|
||||
if idx > 0 {
|
||||
nextPath = byPath[0 : idx-1]
|
||||
} else {
|
||||
nextPath = byPath[0:]
|
||||
}
|
||||
|
||||
if err := SetIn(ctx, to, nextPath, parent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -109,6 +155,30 @@ func SetIn(ctx context.Context, to core.Value, byPath []core.Value, value core.V
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReturnOrNext(ctx context.Context, path []core.Value, idx int, out core.Value, err error) (core.Value, core.PathError) {
|
||||
if err != nil {
|
||||
pathErr, ok := err.(core.PathError)
|
||||
|
||||
if ok {
|
||||
return None, core.NewPathErrorFrom(pathErr, idx)
|
||||
}
|
||||
|
||||
return None, core.NewPathError(err, idx)
|
||||
}
|
||||
|
||||
if len(path) > (idx + 1) {
|
||||
out, pathErr := GetIn(ctx, out, path[idx+1:])
|
||||
|
||||
if pathErr != nil {
|
||||
return None, core.NewPathErrorFrom(pathErr, idx)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func Parse(input interface{}) core.Value {
|
||||
switch value := input.(type) {
|
||||
case bool:
|
||||
|
@ -46,7 +46,7 @@ func (t *CustomValue) Copy() core.Value {
|
||||
return values.None
|
||||
}
|
||||
|
||||
func (t *CustomValue) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
func (t *CustomValue) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return values.None, nil
|
||||
}
|
||||
@ -65,7 +65,7 @@ func (t *CustomValue) GetIn(ctx context.Context, path []core.Value) (core.Value,
|
||||
return values.GetIn(ctx, propValue, path[1:])
|
||||
}
|
||||
|
||||
func (t *CustomValue) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
||||
func (t *CustomValue) SetIn(ctx context.Context, path []core.Value, value core.Value) core.PathError {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -296,28 +296,29 @@ func (t *Object) Clone() core.Cloneable {
|
||||
return cloned
|
||||
}
|
||||
|
||||
func (t *Object) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
||||
func (t *Object) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
|
||||
if len(path) == 0 {
|
||||
return None, nil
|
||||
}
|
||||
|
||||
if typ := path[0].Type(); typ != types.String {
|
||||
return None, core.TypeError(typ, types.String)
|
||||
}
|
||||
|
||||
first, _ := t.Get(path[0].(String))
|
||||
segmentIdx := 0
|
||||
first, _ := t.Get(ToString(path[segmentIdx]))
|
||||
|
||||
if len(path) == 1 {
|
||||
return first, nil
|
||||
}
|
||||
|
||||
getter, ok := first.(core.Getter)
|
||||
if !ok {
|
||||
return None, core.TypeError(
|
||||
first.Type(),
|
||||
core.NewType("Getter"),
|
||||
)
|
||||
segmentIdx++
|
||||
|
||||
if first == None || first == nil {
|
||||
return None, core.NewPathError(core.ErrInvalidPath, segmentIdx)
|
||||
}
|
||||
|
||||
return getter.GetIn(ctx, path[1:])
|
||||
getter, ok := first.(core.Getter)
|
||||
|
||||
if !ok {
|
||||
return GetIn(ctx, first, path[segmentIdx:])
|
||||
}
|
||||
|
||||
return getter.GetIn(ctx, path[segmentIdx:])
|
||||
}
|
||||
|
@ -444,14 +444,15 @@ func TestObject(t *testing.T) {
|
||||
|
||||
Convey("Should error when input is not correct", func() {
|
||||
|
||||
Convey("Should error when path[0] is not a string", func() {
|
||||
Convey("Should return None when path[0] is not a string", func() {
|
||||
obj := values.NewObject()
|
||||
path := []core.Value{values.NewInt(0)}
|
||||
|
||||
el, err := obj.GetIn(ctx, path)
|
||||
|
||||
So(err, ShouldBeError)
|
||||
So(el.Compare(values.None), ShouldEqual, 0)
|
||||
So(err, ShouldBeNil)
|
||||
So(el, ShouldNotBeNil)
|
||||
So(el.Type().String(), ShouldEqual, types.None.String())
|
||||
})
|
||||
|
||||
Convey("Should error when first received item is not a Getter and len(path) > 1", func() {
|
||||
|
@ -121,3 +121,7 @@ func (t String) IndexOf(other String) Int {
|
||||
func (t String) Concat(other core.Value) String {
|
||||
return String(string(t) + other.String())
|
||||
}
|
||||
|
||||
func (t String) At(index Int) String {
|
||||
return String([]rune(t)[index])
|
||||
}
|
||||
|
@ -63,5 +63,12 @@ func TestString(t *testing.T) {
|
||||
So(string(json2), ShouldEqual, fmt.Sprintf(`"%s"`, value))
|
||||
})
|
||||
})
|
||||
Convey(".At", t, func() {
|
||||
Convey("It should return a character", func() {
|
||||
v := values.NewString("abcdefg")
|
||||
c := v.At(2)
|
||||
|
||||
So(string(c), ShouldEqual, "c")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user