Add an efficient expr path traverser
This commit is contained in:
parent
860e0c585c
commit
1f415be2db
97
server/pkg/expr/path.go
Normal file
97
server/pkg/expr/path.go
Normal file
@ -0,0 +1,97 @@
|
||||
package expr
|
||||
|
||||
type (
|
||||
exprPath struct {
|
||||
path string
|
||||
i int
|
||||
isLast bool
|
||||
|
||||
start, end int
|
||||
}
|
||||
)
|
||||
|
||||
// Path initializes a new exprPath helper to efficiently traverse the path
|
||||
func Path(p string) (out exprPath) {
|
||||
return exprPath{path: p}
|
||||
}
|
||||
|
||||
func (p exprPath) More() bool {
|
||||
return p.start < len(p.path)
|
||||
}
|
||||
|
||||
func (p exprPath) Get() string {
|
||||
return p.path[p.start:p.end]
|
||||
}
|
||||
|
||||
func (p exprPath) Rest() string {
|
||||
var rest string
|
||||
if p.end+1 < len(p.path) {
|
||||
rest = p.path[p.end:]
|
||||
}
|
||||
|
||||
// @todo this is fugly but it'll do the trick for now
|
||||
// Clean it up please :)
|
||||
if len(rest) > 0 && (rest[0] == '.' || rest[0] == ']') {
|
||||
rest = rest[1:]
|
||||
}
|
||||
if len(rest) > 0 && (rest[0] == '.' || rest[0] == ']') {
|
||||
rest = rest[1:]
|
||||
}
|
||||
|
||||
return rest
|
||||
}
|
||||
|
||||
func (p exprPath) Next() (out exprPath, err error) {
|
||||
if !p.More() {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
if p.end > 0 {
|
||||
p.start = p.end + 1
|
||||
}
|
||||
|
||||
var ()
|
||||
|
||||
p.start, p.end, p.isLast, err = nxtRange(p.path, p.start)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
p.i++
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func nxtRange(path string, start int) (startOut, end int, isLast bool, err error) {
|
||||
startOut = start
|
||||
for i := start; i < len(path); i++ {
|
||||
switch path[i] {
|
||||
// This thing concludes the prev ident
|
||||
case '.', '[':
|
||||
if i == len(path)-1 {
|
||||
return startOut, -1, false, invalidPathErr
|
||||
}
|
||||
if i > 0 {
|
||||
if path[i-1] == ']' {
|
||||
startOut++
|
||||
continue
|
||||
}
|
||||
}
|
||||
return startOut, i, false, nil
|
||||
|
||||
case ']':
|
||||
// If we're at the end, that's that
|
||||
if i == len(path)-1 {
|
||||
return startOut, i, true, nil
|
||||
}
|
||||
|
||||
if path[i+1] != '.' && path[i+1] != '[' {
|
||||
return startOut, -1, false, invalidPathErr
|
||||
} else {
|
||||
return startOut, i, false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return startOut, len(path), true, nil
|
||||
}
|
||||
143
server/pkg/expr/path_test.go
Normal file
143
server/pkg/expr/path_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNxtRange(t *testing.T) {
|
||||
tcc := []struct {
|
||||
path string
|
||||
|
||||
start int
|
||||
expEnd int
|
||||
expErr bool
|
||||
}{{
|
||||
path: "a",
|
||||
|
||||
start: 0,
|
||||
expEnd: 1,
|
||||
}, {
|
||||
path: "bcd",
|
||||
|
||||
start: 0,
|
||||
expEnd: 3,
|
||||
}, {
|
||||
path: "bcd[e]",
|
||||
|
||||
start: 0,
|
||||
expEnd: 3,
|
||||
}, {
|
||||
path: "bcd.e",
|
||||
|
||||
start: 0,
|
||||
expEnd: 3,
|
||||
}, {
|
||||
path: "bcd.",
|
||||
|
||||
start: 0,
|
||||
expErr: true,
|
||||
}, {
|
||||
path: "bcd[",
|
||||
|
||||
start: 0,
|
||||
expErr: true,
|
||||
}, {
|
||||
path: "bcd.e",
|
||||
|
||||
start: 4,
|
||||
expEnd: 5,
|
||||
}}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.path[c.start:], func(t *testing.T) {
|
||||
_, o, _, err := nxtRange(c.path, c.start)
|
||||
if c.expErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, c.expEnd, o)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
tcc := []struct {
|
||||
path string
|
||||
|
||||
expBits []string
|
||||
expRests []string
|
||||
expErr bool
|
||||
}{{
|
||||
path: "a",
|
||||
|
||||
expBits: []string{"a"},
|
||||
expRests: []string{""},
|
||||
}, {
|
||||
path: "a.b",
|
||||
|
||||
expBits: []string{"a", "b"},
|
||||
expRests: []string{"b", ""},
|
||||
}, {
|
||||
path: "a[b][c]",
|
||||
|
||||
expBits: []string{"a", "b", "c"},
|
||||
expRests: []string{"[b][c]", "[c]", ""},
|
||||
}, {
|
||||
path: "aa.b[c][d].e.f[g][0]",
|
||||
|
||||
expBits: []string{"aa", "b", "c", "d", "e", "f", "g", "0"},
|
||||
expRests: []string{"b[c][d].e.f[g][0]", "[c][d].e.f[g][0]", "[d].e.f[g][0]", "e.f[g][0]", "f[g][0]", "[g][0]", "[0]", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.path, func(t *testing.T) {
|
||||
pp := Path(c.path)
|
||||
var err error
|
||||
|
||||
i := -1
|
||||
for {
|
||||
i++
|
||||
|
||||
pp, err = pp.Next()
|
||||
require.NoError(t, err)
|
||||
|
||||
if !pp.More() {
|
||||
break
|
||||
}
|
||||
|
||||
require.Equal(t, c.expBits[i], pp.Get())
|
||||
require.Equal(t, c.expRests[i], pp.Rest())
|
||||
}
|
||||
require.Equal(t, len(c.expBits), i)
|
||||
|
||||
// for _, b := range c.expBits {
|
||||
|
||||
// require.Equal(t, b, pp.Get())
|
||||
// }
|
||||
// pp, err = pp.Next()
|
||||
// require.NoError(t, err)
|
||||
|
||||
// require.False(t, pp.More())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPath(b *testing.B) {
|
||||
path := "aa.b[c][d].e.f[g][0]"
|
||||
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
pp := Path(path)
|
||||
|
||||
for {
|
||||
pp, _ = pp.Next()
|
||||
if !pp.More() {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user