diff --git a/server/pkg/expr/path.go b/server/pkg/expr/path.go new file mode 100644 index 000000000..eecbc554a --- /dev/null +++ b/server/pkg/expr/path.go @@ -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 +} diff --git a/server/pkg/expr/path_test.go b/server/pkg/expr/path_test.go new file mode 100644 index 000000000..77c6d5b55 --- /dev/null +++ b/server/pkg/expr/path_test.go @@ -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 + } + } + } +}