Skip to content

Commit 4cd31b6

Browse files
committed
Implement matches XPath function.
See details in #57. Also created a number of `assert????` helpers in `assert_test.go` and moved `assertPanic` into it.
1 parent a5d9242 commit 4cd31b6

8 files changed

+350
-20
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Supported Features
138138
`lang()`| ✗ |
139139
`last()`| ✓ |
140140
`local-name()`| ✓ |
141+
`matches()`| ✓ |
141142
`name()`| ✓ |
142143
`namespace-uri()`| ✓ |
143144
`normalize-space()`| ✓ |

assert_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package xpath
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func assertEqual(tb testing.TB, v1, v2 interface{}) {
9+
if !reflect.DeepEqual(v1, v2) {
10+
tb.Fatalf("'%+v' and '%+v' are not equal", v1, v2)
11+
}
12+
}
13+
14+
func assertNoErr(tb testing.TB, err error) {
15+
if err != nil {
16+
tb.Fatalf("expected no err, but got: %s", err.Error())
17+
}
18+
}
19+
20+
func assertErr(tb testing.TB, err error) {
21+
if err == nil {
22+
tb.Fatal("expected err, but got nil")
23+
}
24+
}
25+
26+
func assertTrue(tb testing.TB, v bool) {
27+
if !v {
28+
tb.Fatal("expected true, but got false")
29+
}
30+
}
31+
32+
func assertFalse(tb testing.TB, v bool) {
33+
if v {
34+
tb.Fatal("expected false, but got true")
35+
}
36+
}
37+
38+
func assertNil(tb testing.TB, v interface{}) {
39+
if v != nil && !reflect.ValueOf(v).IsNil() {
40+
tb.Fatalf("expected nil, but got: %+v", v)
41+
}
42+
}
43+
44+
func assertPanic(t *testing.T, f func()) {
45+
defer func() {
46+
if r := recover(); r == nil {
47+
t.Errorf("The code did not panic")
48+
}
49+
}()
50+
f()
51+
}

build.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,23 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
193193
if err != nil {
194194
return nil, err
195195
}
196-
197196
qyOutput = &functionQuery{Input: b.firstInput, Func: containsFunc(arg1, arg2)}
197+
case "matches":
198+
//matches(string , pattern)
199+
if len(root.Args) != 2 {
200+
return nil, errors.New("xpath: matches function must have two parameters")
201+
}
202+
var (
203+
arg1, arg2 query
204+
err error
205+
)
206+
if arg1, err = b.processNode(root.Args[0]); err != nil {
207+
return nil, err
208+
}
209+
if arg2, err = b.processNode(root.Args[1]); err != nil {
210+
return nil, err
211+
}
212+
qyOutput = &functionQuery{Input: b.firstInput, Func: matchesFunc(arg1, arg2)}
198213
case "substring":
199214
//substring( string , start [, length] )
200215
if len(root.Args) < 2 {

cache.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package xpath
2+
3+
import (
4+
"regexp"
5+
"sync"
6+
)
7+
8+
type loadFunc func(key interface{}) (interface{}, error)
9+
10+
const (
11+
defaultCap = 65536
12+
)
13+
14+
type loadingCache struct {
15+
sync.RWMutex
16+
cap int
17+
load loadFunc
18+
m map[interface{}]interface{}
19+
reset int
20+
}
21+
22+
// NewLoadingCache creates a new instance of a loading cache with capacity. Capacity must be >= 0, or
23+
// it will panic. Capacity == 0 means the cache growth is unbounded.
24+
func NewLoadingCache(load loadFunc, capacity int) *loadingCache {
25+
if capacity < 0 {
26+
panic("capacity must be >= 0")
27+
}
28+
return &loadingCache{cap: capacity, load: load, m: make(map[interface{}]interface{})}
29+
}
30+
31+
func (c *loadingCache) get(key interface{}) (interface{}, error) {
32+
c.RLock()
33+
v, found := c.m[key]
34+
c.RUnlock()
35+
if found {
36+
return v, nil
37+
}
38+
v, err := c.load(key)
39+
if err != nil {
40+
return nil, err
41+
}
42+
c.Lock()
43+
if c.cap > 0 && len(c.m) >= c.cap {
44+
c.m = map[interface{}]interface{}{key: v}
45+
c.reset++
46+
} else {
47+
c.m[key] = v
48+
}
49+
c.Unlock()
50+
return v, nil
51+
}
52+
53+
var (
54+
// RegexpCache is a loading cache for string -> *regexp.Regexp mapping. It is exported so that in rare cases
55+
// client can customize load func and/or capacity.
56+
RegexpCache = defaultRegexpCache()
57+
)
58+
59+
func defaultRegexpCache() *loadingCache {
60+
return NewLoadingCache(
61+
func(key interface{}) (interface{}, error) {
62+
return regexp.Compile(key.(string))
63+
}, defaultCap)
64+
}
65+
66+
func getRegexp(pattern string) (*regexp.Regexp, error) {
67+
exp, err := RegexpCache.get(pattern)
68+
if err != nil {
69+
return nil, err
70+
}
71+
return exp.(*regexp.Regexp), nil
72+
}

cache_test.go

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package xpath
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"math/rand"
7+
"strconv"
8+
"sync"
9+
"testing"
10+
)
11+
12+
func TestLoadingCache(t *testing.T) {
13+
c := NewLoadingCache(
14+
func(key interface{}) (interface{}, error) {
15+
switch v := key.(type) {
16+
case int:
17+
return strconv.Itoa(v), nil
18+
default:
19+
return nil, errors.New("invalid type")
20+
}
21+
},
22+
2) // cap = 2
23+
assertEqual(t, 0, len(c.m))
24+
v, err := c.get(1)
25+
assertNoErr(t, err)
26+
assertEqual(t, "1", v)
27+
assertEqual(t, 1, len(c.m))
28+
29+
v, err = c.get(1)
30+
assertNoErr(t, err)
31+
assertEqual(t, "1", v)
32+
assertEqual(t, 1, len(c.m))
33+
34+
v, err = c.get(2)
35+
assertNoErr(t, err)
36+
assertEqual(t, "2", v)
37+
assertEqual(t, 2, len(c.m))
38+
39+
// over capacity, m is reset
40+
v, err = c.get(3)
41+
assertNoErr(t, err)
42+
assertEqual(t, "3", v)
43+
assertEqual(t, 1, len(c.m))
44+
45+
// Invalid capacity
46+
assertPanic(t, func() {
47+
NewLoadingCache(func(key interface{}) (interface{}, error) { return key, nil }, -1)
48+
})
49+
50+
// Loading failure
51+
c = NewLoadingCache(
52+
func(key interface{}) (interface{}, error) {
53+
if key.(int)%2 == 0 {
54+
return key, nil
55+
} else {
56+
return nil, fmt.Errorf("artificial error: %d", key.(int))
57+
}
58+
}, 0)
59+
v, err = c.get(12)
60+
assertNoErr(t, err)
61+
assertEqual(t, 12, v)
62+
_, err = c.get(21)
63+
assertErr(t, err)
64+
assertEqual(t, "artificial error: 21", err.Error())
65+
}
66+
67+
const (
68+
benchLoadingCacheRandSeed = 12345
69+
benchLoadingCacheConcurrency = 5
70+
benchLoadingCacheKeyRange = 2000
71+
benchLoadingCacheCap = 1000
72+
)
73+
74+
func BenchmarkLoadingCacheCapped_SingleThread(b *testing.B) {
75+
rand.Seed(benchLoadingCacheRandSeed)
76+
c := NewLoadingCache(
77+
func(key interface{}) (interface{}, error) {
78+
return key, nil
79+
}, benchLoadingCacheCap)
80+
for i := 0; i < b.N; i++ {
81+
k := rand.Intn(benchLoadingCacheKeyRange)
82+
v, _ := c.get(k)
83+
if k != v {
84+
b.FailNow()
85+
}
86+
}
87+
b.Logf("N=%d, reset=%d", b.N, c.reset)
88+
}
89+
90+
func BenchmarkLoadingCacheCapped_MultiThread(b *testing.B) {
91+
rand.Seed(benchLoadingCacheRandSeed)
92+
c := NewLoadingCache(
93+
func(key interface{}) (interface{}, error) {
94+
return key, nil
95+
}, benchLoadingCacheCap)
96+
wg := sync.WaitGroup{}
97+
wg.Add(benchLoadingCacheConcurrency)
98+
for i := 0; i < benchLoadingCacheConcurrency; i++ {
99+
go func() {
100+
for j := 0; j < b.N; j++ {
101+
k := rand.Intn(benchLoadingCacheKeyRange)
102+
v, _ := c.get(k)
103+
if k != v {
104+
b.FailNow()
105+
}
106+
}
107+
defer wg.Done()
108+
}()
109+
}
110+
wg.Wait()
111+
b.Logf("N=%d, concurrency=%d, reset=%d", b.N, benchLoadingCacheConcurrency, c.reset)
112+
}
113+
114+
func BenchmarkLoadingCacheNoCap_SingleThread(b *testing.B) {
115+
rand.Seed(benchLoadingCacheRandSeed)
116+
c := NewLoadingCache(
117+
func(key interface{}) (interface{}, error) {
118+
return key, nil
119+
}, 0) // 0 => no cap
120+
for i := 0; i < b.N; i++ {
121+
k := rand.Intn(benchLoadingCacheKeyRange)
122+
v, _ := c.get(k)
123+
if k != v {
124+
b.FailNow()
125+
}
126+
}
127+
b.Logf("N=%d, reset=%d", b.N, c.reset)
128+
}
129+
130+
func BenchmarkLoadingCacheNoCap_MultiThread(b *testing.B) {
131+
rand.Seed(benchLoadingCacheRandSeed)
132+
c := NewLoadingCache(
133+
func(key interface{}) (interface{}, error) {
134+
return key, nil
135+
}, 0) // 0 => no cap
136+
wg := sync.WaitGroup{}
137+
wg.Add(benchLoadingCacheConcurrency)
138+
for i := 0; i < benchLoadingCacheConcurrency; i++ {
139+
go func() {
140+
for j := 0; j < b.N; j++ {
141+
k := rand.Intn(benchLoadingCacheKeyRange)
142+
v, _ := c.get(k)
143+
if k != v {
144+
b.FailNow()
145+
}
146+
}
147+
defer wg.Done()
148+
}()
149+
}
150+
wg.Wait()
151+
b.Logf("N=%d, concurrency=%d, reset=%d", b.N, benchLoadingCacheConcurrency, c.reset)
152+
}
153+
154+
func TestGetRegexp(t *testing.T) {
155+
RegexpCache = defaultRegexpCache()
156+
assertEqual(t, 0, len(RegexpCache.m))
157+
assertEqual(t, defaultCap, RegexpCache.cap)
158+
exp, err := getRegexp("^[0-9]{3,5}$")
159+
assertNoErr(t, err)
160+
assertTrue(t, exp.MatchString("3141"))
161+
assertFalse(t, exp.MatchString("3"))
162+
exp, err = getRegexp("[invalid")
163+
assertErr(t, err)
164+
assertEqual(t, "error parsing regexp: missing closing ]: `[invalid`", err.Error())
165+
assertNil(t, exp)
166+
}

doc_test.go

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
1-
package xpath_test
1+
package xpath
22

33
import (
44
"fmt"
5-
6-
"github.com/antchfx/xpath"
75
)
86

97
// XPath package example.
108
func Example() {
11-
expr, err := xpath.Compile("count(//book)")
9+
expr, err := Compile("count(//book)")
1210
if err != nil {
1311
panic(err)
1412
}
15-
var root xpath.NodeNavigator
13+
var root NodeNavigator
1614
// using Evaluate() method
1715
val := expr.Evaluate(root) // it returns float64 type
1816
fmt.Println(val.(float64))
1917

2018
// using Evaluate() method
21-
expr = xpath.MustCompile("//book")
19+
expr = MustCompile("//book")
2220
val = expr.Evaluate(root) // it returns NodeIterator type.
23-
iter := val.(*xpath.NodeIterator)
21+
iter := val.(*NodeIterator)
2422
for iter.MoveNext() {
2523
fmt.Println(iter.Current().Value())
2624
}

0 commit comments

Comments
 (0)