Skip to content

Commit

Permalink
Add transformFunctionQuery and its first usage xpath 2.0's reverse()
Browse files Browse the repository at this point in the history
XPath 2.0 introduces a number of functions that returns a node-set for a given input
node-set. That's different from XPath 1.0 functions. Introduce `transformFunctionQuery`
which performs a mapping/transform on a given node-set input (NodeNavigator) and returns
a new node-set. And coming with it is the first application `reverse()`. Plus unit test
coverage.

For more details, check out the discussion in #45.
  • Loading branch information
jinfengnarvar committed Apr 7, 2020
1 parent 4966611 commit a098872
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 2 deletions.
9 changes: 9 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,15 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
args = append(args, q)
}
qyOutput = &functionQuery{Input: b.firstInput, Func: concatFunc(args...)}
case "reverse":
if len(root.Args) == 0 {
return nil, fmt.Errorf("xpath: reverse(node-sets) function must with have parameters node-sets")
}
argQuery, err := b.processNode(root.Args[0])
if err != nil {
return nil, err
}
qyOutput = &transformFunctionQuery{Input: argQuery, Func: reverseFunc}
default:
return nil, fmt.Errorf("not yet support this function %s()", root.FuncName)
}
Expand Down
20 changes: 20 additions & 0 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,23 @@ func functionArgs(q query) query {
}
return q.Clone()
}

func reverseFunc(q query, t iterator) func() NodeNavigator {
var list []NodeNavigator
for {
node := q.Select(t)
if node == nil {
break
}
list = append(list, node.Copy())
}
i := len(list)
return func() NodeNavigator {
if i <= 0 {
return nil
}
i--
node := list[i]
return node
}
}
33 changes: 31 additions & 2 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,8 +590,9 @@ func (f *filterQuery) Clone() query {
return &filterQuery{Input: f.Input.Clone(), Predicate: f.Predicate.Clone()}
}

// functionQuery is an XPath function that call a function to returns
// value of current NodeNavigator node.
// functionQuery is an XPath function that returns a computed value for
// the Evaluate call of the current NodeNavigator node. Select call isn't
// applicable for functionQuery.
type functionQuery struct {
Input query // Node Set
Func func(query, iterator) interface{} // The xpath function.
Expand All @@ -611,6 +612,34 @@ func (f *functionQuery) Clone() query {
return &functionQuery{Input: f.Input.Clone(), Func: f.Func}
}

// transformFunctionQuery diffs from functionQuery where the latter computes a scalar
// value (number,string,boolean) for the current NodeNavigator node while the former
// (transformFunctionQuery) performs a mapping or transform of the current NodeNavigator
// and returns a new NodeNavigator. It is used for non-scalar XPath functions such as
// reverse(), remove(), subsequence(), unordered(), etc.
type transformFunctionQuery struct {
Input query
Func func(query, iterator) func() NodeNavigator
iterator func() NodeNavigator
}

func (f *transformFunctionQuery) Select(t iterator) NodeNavigator {
if f.iterator == nil {
f.iterator = f.Func(f.Input, t)
}
return f.iterator()
}

func (f *transformFunctionQuery) Evaluate(t iterator) interface{} {
f.Input.Evaluate(t)
f.iterator = nil
return f
}

func (f *transformFunctionQuery) Clone() query {
return &transformFunctionQuery{Input: f.Input.Clone(), Func: f.Func}
}

// constantQuery is an XPath constant operand.
type constantQuery struct {
Val interface{}
Expand Down
23 changes: 23 additions & 0 deletions xpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,29 @@ func TestFunction(t *testing.T) {
testXPath3(t, html, "//li/preceding::*[1]", selectNode(html, "//h1"))
}

func TestTransformFunctionReverse(t *testing.T) {
nodes := selectNodes(html, "reverse(//li)")
expectedReversedNodeValues := []string { "", "login", "about", "Home" }
if len(nodes) != len(expectedReversedNodeValues) {
t.Fatalf("reverse(//li) should return %d <li> nodes", len(expectedReversedNodeValues))
}
for i := 0; i < len(expectedReversedNodeValues); i++ {
if nodes[i].Value() != expectedReversedNodeValues[i] {
t.Fatalf("reverse(//li)[%d].Value() should be '%s', instead, got '%s'",
i, expectedReversedNodeValues[i], nodes[i].Value())
}
}

// Although this xpath itself doesn't make much sense, it does exercise the call path to provide coverage
// for transformFunctionQuery.Evaluate()
testXPath2(t, html, "//h1[reverse(.) = reverse(.)]", 1)

// Test reverse() parsing error: missing node-sets argument.
assertPanic(t, func() { testXPath2(t, html, "reverse()", 0) })
// Test reverse() parsing error: invalid node-sets argument.
assertPanic(t, func() { testXPath2(t, html, "reverse(concat())", 0) })
}

func TestPanic(t *testing.T) {
// starts-with
assertPanic(t, func() { testXPath(t, html, "//*[starts-with(0, 0)]", "") })
Expand Down

0 comments on commit a098872

Please sign in to comment.