Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions idr/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package idr

import (
"errors"
"fmt"

"github.com/antchfx/xpath"
"github.com/jf-tech/go-corelib/caches"
)

var (
// ErrNoMatch is returned when not a single matched node can be found.
ErrNoMatch = errors.New("no match")
// ErrMoreThanExpected is returned when more than expected matched nodes are found.
ErrMoreThanExpected = errors.New("more than expected matched")
)

const (
// DisableXPathCache disables caching xpath compilation when MatchAll/MatchSingle
// are called. Useful when caller knows the xpath string isn't cache-able (such as
// containing unique IDs, timestamps, etc) which would otherwise cause the xpath
// compilation cache grow unbounded.
DisableXPathCache = uint(1) << iota
)

func loadXPathExpr(exprStr string, flags []uint) (*xpath.Expr, error) {
var flagsActual uint
switch len(flags) {
case 0:
flagsActual = 0
case 1:
flagsActual = flags[0]
default:
return nil, fmt.Errorf("only one flag is allowed, instead got: %d", len(flags))
}
var expr interface{}
var err error
if flagsActual&DisableXPathCache != 0 {
expr, err = xpath.Compile(exprStr)
} else {
expr, err = caches.GetXPathExpr(exprStr)
}
if err != nil {
return nil, fmt.Errorf("xpath '%s' compilation failed: %s", exprStr, err.Error())
}
return expr.(*xpath.Expr), nil
}

// QueryIter initiates an xpath query specified by 'expr' against an IDR tree rooted at 'n'.
func QueryIter(n *Node, expr *xpath.Expr) *xpath.NodeIterator {
return expr.Select(createNavigator(n))
}

// AnyMatch returns true if the xpath query 'expr' against an IDR tree rooted at 'n' yields any result.
func AnyMatch(n *Node, expr *xpath.Expr) bool {
return QueryIter(n, expr).MoveNext()
}

// MatchAll returns all the matched nodes by an xpath query 'exprStr' against an IDR tree rooted at 'n'.
func MatchAll(n *Node, exprStr string, flags ...uint) ([]*Node, error) {
if exprStr == "." {
return []*Node{n}, nil
}
exp, err := loadXPathExpr(exprStr, flags)
if err != nil {
return nil, err
}
iter := QueryIter(n, exp)
var ret []*Node
for iter.MoveNext() {
ret = append(ret, nodeFromIter(iter))
}
return ret, nil
}

// MatchSingle returns one and only one matched node by an xpath query 'exprStr' against an IDR tree rooted
// at 'n'. If no matching node is found, ErrNoMatch is returned; if more than one matching nodes are found,
// ErrMoreThanExpected is returned.
func MatchSingle(n *Node, exprStr string, flags ...uint) (*Node, error) {
if exprStr == "." {
return n, nil
}
expr, err := loadXPathExpr(exprStr, flags)
if err != nil {
return nil, err
}
iter := QueryIter(n, expr)
if !iter.MoveNext() {
return nil, ErrNoMatch
}
ret := nodeFromIter(iter)
if iter.MoveNext() {
return nil, ErrMoreThanExpected
}
return ret, nil
}
198 changes: 198 additions & 0 deletions idr/query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package idr

import (
"fmt"
"testing"

"github.com/jf-tech/go-corelib/caches"
"github.com/stretchr/testify/assert"
)

func TestLoadXPathExpr(t *testing.T) {
for _, test := range []struct {
name string
exprStr string
flags []uint
existsInCache bool
err string
}{
{
name: "valid expr; not in cache before; added to cache",
exprStr: ".",
flags: nil,
existsInCache: false,
err: "",
},
{
name: "valid expr; not in cache before; not added to cache",
exprStr: ".",
flags: []uint{DisableXPathCache},
existsInCache: false,
err: "",
},
{
name: "valid expr; in cache before; not added to cache again",
exprStr: ".",
flags: nil,
existsInCache: true,
err: "",
},
{
name: "not valid expr",
exprStr: "]",
flags: nil,
existsInCache: false,
err: "xpath ']' compilation failed: expression must evaluate to a node-set",
},
{
name: "more than one flag",
exprStr: ".",
flags: []uint{DisableXPathCache, DisableXPathCache},
existsInCache: false,
err: "only one flag is allowed, instead got: 2",
},
} {
t.Run(test.name, func(t *testing.T) {
addedToCache := true
for _, f := range test.flags {
if f&DisableXPathCache != 0 {
addedToCache = false
}
}
caches.XPathExprCache = caches.NewLoadingCache()
if test.existsInCache {
_, err := caches.GetXPathExpr(test.exprStr)
assert.NoError(t, err)
assert.Equal(t, 1, len(caches.XPathExprCache.DumpForTest()))
}
expr, err := loadXPathExpr(test.exprStr, test.flags)
if test.err != "" {
assert.Error(t, err)
assert.Equal(t, test.err, err.Error())
assert.Nil(t, expr)
if test.existsInCache {
assert.Equal(t, 1, len(caches.XPathExprCache.DumpForTest()))
} else {
assert.Equal(t, 0, len(caches.XPathExprCache.DumpForTest()))
}
} else {
assert.NoError(t, err)
assert.NotNil(t, expr)
if addedToCache {
exprInCache, err := caches.XPathExprCache.Get(test.exprStr, func(interface{}) (interface{}, error) {
return nil, fmt.Errorf("expr '%s' should've already existed in cache, but not", test.exprStr)
})
assert.NoError(t, err)
assert.True(t, expr == exprInCache)
}
}
})
}
}

func TestQueryIter(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
expr, err := caches.GetXPathExpr(".")
assert.NoError(t, err)
iter := QueryIter(tt.elemB, expr)
assert.True(t, tt.elemB == iter.Current().(*navigator).root)
assert.True(t, tt.elemB == iter.Current().(*navigator).cur)
}

func TestAnyMatch(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
expr, err := caches.GetXPathExpr(".")
assert.NoError(t, err)
assert.True(t, AnyMatch(tt.elemB, expr))
expr, err = caches.GetXPathExpr("not_existing")
assert.NoError(t, err)
assert.False(t, AnyMatch(tt.elemB, expr))
}

func TestMatchAll_Dot(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
nodes, err := MatchAll(tt.elemB, ".")
assert.NoError(t, err)
assert.Equal(t, 1, len(nodes))
assert.True(t, tt.elemB == nodes[0])
assert.Equal(t, 0, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchAll_InvalidExpr(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
nodes, err := MatchAll(tt.elemB, "]")
assert.Error(t, err)
assert.Equal(t, "xpath ']' compilation failed: expression must evaluate to a node-set", err.Error())
assert.Equal(t, 0, len(nodes))
assert.Equal(t, 0, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchAll_NoMatch(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
nodes, err := MatchAll(tt.elemC, "non_existing")
assert.NoError(t, err)
assert.Equal(t, 0, len(nodes))
assert.Equal(t, 1, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchAll_MultipleMatches(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
nodes, err := MatchAll(tt.elemC, "*")
assert.NoError(t, err)
assert.Equal(t, 2, len(nodes))
assert.True(t, tt.elemC3 == nodes[0])
assert.True(t, tt.elemC4 == nodes[1])
assert.Equal(t, 1, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchSingle_Dot(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
n, err := MatchSingle(tt.elemB, ".")
assert.NoError(t, err)
assert.True(t, tt.elemB == n)
assert.Equal(t, 0, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchSingle_InvalidExpr(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
n, err := MatchSingle(tt.elemB, "]")
assert.Error(t, err)
assert.Equal(t, "xpath ']' compilation failed: expression must evaluate to a node-set", err.Error())
assert.Nil(t, n)
assert.Equal(t, 0, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchSingle_NoMatch(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
n, err := MatchSingle(tt.elemC, "non_existing")
assert.Equal(t, ErrNoMatch, err)
assert.Nil(t, n)
assert.Equal(t, 1, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchSingle_SingleMatch(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
n, err := MatchSingle(tt.elemC, "elemC4")
assert.NoError(t, err)
assert.True(t, tt.elemC4 == n)
assert.Equal(t, 1, len(caches.XPathExprCache.DumpForTest()))
}

func TestMatchSingle_MoreThanOneMatch(t *testing.T) {
tt, _, _ := navTestSetup(t)
caches.XPathExprCache = caches.NewLoadingCache()
n, err := MatchSingle(tt.elemA, "*")
assert.Equal(t, ErrMoreThanExpected, err)
assert.Nil(t, n)
assert.Equal(t, 1, len(caches.XPathExprCache.DumpForTest()))
}