-
- Notifications
You must be signed in to change notification settings - Fork 81
Add all the xpath query helpers for IDR #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
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
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
jf-tech marked this conversation as resolved. Show resolved Hide resolved | ||
| return ret, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| 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())) | ||
| } |
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.