Skip to content

Commit

Permalink
fix jsonpath parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Stepan Pyzhov committed Mar 22, 2019
1 parent 34091c6 commit c2c45d3
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 55 deletions.
46 changes: 27 additions & 19 deletions buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,52 +275,62 @@ func (b *buffer) step() error {
func (b *buffer) token() (err error) {
var (
c byte
str bool
stack = make([]byte, 0)
start int
)
tokenLoop:
for ; b.index < b.length; b.index++ {
c = b.data[b.index]
switch {
case c == quote:
if !str {
str = true
stack = append(stack, c)
} else if !b.backslash() {
if len(stack) == 0 || stack[len(stack)-1] != quote {
return b.errorSymbol()
}
str = false
stack = stack[:len(stack)-1]
start = b.index
err = b.step()
if err != nil {
return b.errorEOF()
}
case c == bracketL && !str:
err = b.skip(quote)
if err == nil || err == io.EOF {
continue
}
b.index = start
case c == bracketL:
stack = append(stack, c)
case c == bracketR && !str:
case c == bracketR:
if len(stack) == 0 || stack[len(stack)-1] != bracketL {
return b.errorSymbol()
}
stack = stack[:len(stack)-1]
case c == parenthesesL && !str:
case c == parenthesesL:
stack = append(stack, c)
case c == parenthesesR && !str:
case c == parenthesesR:
if len(stack) == 0 || stack[len(stack)-1] != parenthesesL {
return b.errorSymbol()
}
stack = stack[:len(stack)-1]
case str:
continue
case c == dot || c == at || c == dollar || c == question || c == asterisk || (c >= 'A' && c <= 'z') || (c >= '0' && c <= '9'): // standard token name
continue
case len(stack) != 0:
continue
case c == minus || c == plus:
start = b.index
err = b.numeric()
if err == nil || err == io.EOF {
b.index--
continue
}
b.index = start
fallthrough
default:
break tokenLoop
}
}
if len(stack) != 0 {
return b.errorEOF()
}
return io.EOF
if b.index >= b.length {
return io.EOF
}
return nil
}

func (b *buffer) rpn() (result []string, err error) {
Expand Down Expand Up @@ -356,7 +366,6 @@ func (b *buffer) rpn() (result []string, err error) {
err = nil
}

found = false
for len(stack) > 0 {
temp = stack[len(stack)-1]
found = false
Expand Down Expand Up @@ -427,7 +436,6 @@ func (b *buffer) rpn() (result []string, err error) {
stack = append(stack, current)
case c == parenthesesR: // )
variable = true
current = string(c)
found = false
for len(stack) > 0 {
temp = stack[len(stack)-1]
Expand Down
9 changes: 9 additions & 0 deletions buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ func TestBuffer_Token(t *testing.T) {

{name: "part 1", value: "@[email protected]", index: 5, fail: false},
{name: "part 2", value: "@.foo && @.bar", index: 5, fail: false},
{name: "part 3", value: "@.foo,3", index: 5, fail: false},

{name: "number 1", value: "1", index: 1, fail: false},
{name: "number 2", value: "1.3e2", index: 5, fail: false},
{name: "number 3", value: "-1.3e2", index: 6, fail: false},
{name: "number 4", value: "-1.3e-2", index: 7, fail: false},

{name: "string 1", value: "'1'", index: 3, fail: false},
{name: "string 2", value: "'foo \\'bar '", index: 12, fail: false},

{name: "fail 1", value: "@.foo[", fail: true},
{name: "fail 2", value: "@.foo[(]", fail: true},
Expand Down
107 changes: 72 additions & 35 deletions jsonpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func JSONPath(data []byte, path string) (result []*Node, err error) {
temporary []*Node
keys []string
from, to, step int
c byte
key string
)
for i, cmd := range commands {
switch {
Expand Down Expand Up @@ -101,13 +103,47 @@ func JSONPath(data []byte, path string) (result []*Node, err error) {
}
result = temporary
case strings.HasPrefix(cmd, "?("): // applies a filter (script) expression
//$..[?(@.price == 19.95 && @.color == 'red')].color

//case strings.HasPrefix(cmd, "("): // script expression, using the underlying script engine
//todo
//$..[?(@.price == 19.95 && @.color == 'red')].color
case strings.HasPrefix(cmd, "("): // script expression, using the underlying script engine
//todo
default: // try to get by key & Union
keys = strings.Split(cmd, ",")
buf := newBuffer([]byte(cmd))
keys = make([]string, 0)
for {
c, err = buf.first()
if err != nil {
return nil, errorRequest("blank request")
}
if c == coma {
return nil, errorRequest("wrong request: %s", cmd)
}
from = buf.index
err = buf.token()
if err != nil && err != io.EOF {
return nil, errorRequest("wrong request: %s", cmd)
}
key = string(buf.data[from:buf.index])
if len(key) > 2 && key[0] == quote && key[len(key)-1] == quote { // string
key = key[1 : len(key)-1]
}
keys = append(keys, key)
c, err = buf.first()
if err != nil {
err = nil
break
}
if c != coma {
return nil, errorRequest("wrong request: %s", cmd)
}
err = buf.step()
if err != nil {
return nil, errorRequest("wrong request: %s", cmd)
}
}

temporary = make([]*Node, 0)
for _, key := range keys {
for _, key = range keys {
for _, element := range result {
if element.isContainer() {
value, ok := element.children[key]
Expand Down Expand Up @@ -148,34 +184,41 @@ func recursiveChildren(node *Node) (result []*Node) {
return temp
}

//ParseJSONPath will parse current path and return all commands tobe run.
// ParseJSONPath will parse current path and return all commands tobe run.
// Example:
//
// result, _ := ParseJSONPath("$.store.book[?(@.price < 10)].title")
// result == []string{"$", "store", "book", "?(@.price < 10)", "title"}
//
func ParseJSONPath(path string) (result []string, err error) {
buf := newBuffer([]byte(path))
result = make([]string, 0)
var (
b byte
c byte
start, stop int
childEnd = map[byte]bool{dot: true, bracketL: true}
str bool
)
for {
b, err = buf.current()
c, err = buf.current()
if err != nil {
break
}
parseSwitch:
switch true {
case b == dollar:
result = append(result, "$")
case b == dot:
case c == dollar || c == at:
result = append(result, string(c))
case c == dot:
start = buf.index
b, err = buf.next()
c, err = buf.next()
if err == io.EOF {
err = nil
break
}
if err != nil {
break
}
if b == dot {
if c == dot {
result = append(result, "..")
buf.index--
break
Expand All @@ -194,34 +237,28 @@ func ParseJSONPath(path string) (result []string, err error) {
if start+1 < stop {
result = append(result, string(buf.data[start+1:stop]))
}
case b == bracketL:
b, err = buf.next()
case c == bracketL:
_, err = buf.next()
if err != nil {
return nil, buf.errorEOF()
}
start = buf.index
if b == quote {
start++
err = buf.string(quote)
if err != nil {
return nil, buf.errorEOF()
}
stop = buf.index
b, err = buf.next()
if err != nil {
return nil, buf.errorEOF()
}
if b != bracketR {
return nil, buf.errorSymbol()
}
} else {
err = buf.skip(bracketR)
stop = buf.index
if err != nil {
return nil, buf.errorEOF()
for ; buf.index < buf.length; buf.index++ {
c = buf.data[buf.index]
if c == quote {
if str {
str = buf.backslash()
} else {
str = true
}
} else if c == bracketR {
if !str {
result = append(result, string(buf.data[start:buf.index]))
break parseSwitch
}
}
}
result = append(result, string(buf.data[start:stop]))
return nil, buf.errorEOF()
default:
return nil, buf.errorSymbol()
}
Expand Down
3 changes: 2 additions & 1 deletion jsonpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestJsonPath(t *testing.T) {
{name: "all key bracket", path: "$..['price']", expected: "[$['store']['bicycle']['price'], $['store']['book'][0]['price'], $['store']['book'][1]['price'], $['store']['book'][2]['price'], $['store']['book'][3]['price']]"},
{name: "all fields", path: "$['store']['book'][1].*", expected: "[$['store']['book'][1]['author'], $['store']['book'][1]['category'], $['store']['book'][1]['price'], $['store']['book'][1]['title']]"},

{name: "union fields", path: "$['store']['book'][2]['author,price,title']", expected: "[$['store']['book'][2]['author'], $['store']['book'][2]['price'], $['store']['book'][2]['title']]"},
{name: "union fields", path: "$['store']['book'][2]['author','price','title']", expected: "[$['store']['book'][2]['author'], $['store']['book'][2]['price'], $['store']['book'][2]['title']]"},
{name: "union indexes", path: "$['store']['book'][1,2]", expected: "[$['store']['book'][1], $['store']['book'][2]]"},

{name: "slices 1", path: "$..[1:4]", expected: "[$['store']['book'][1], $['store']['book'][2], $['store']['book'][3]]"},
Expand Down Expand Up @@ -122,6 +122,7 @@ func TestParseJSONPath(t *testing.T) {
{name: "path combined:dotted small", path: "$['root'].*.['element']", expected: []string{"$", "root", "*", "element"}},
{name: "phoneNumbers", path: "$.phoneNumbers[*].type", expected: []string{"$", "phoneNumbers", "*", "type"}},
{name: "filtered", path: "$.store.book[?(@.price < 10)].title", expected: []string{"$", "store", "book", "?(@.price < 10)", "title"}},
{name: "formula", path: "$..phoneNumbers..('ty' + 'pe')", expected: []string{"$", "..", "phoneNumbers", "..", "('ty' + 'pe')"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down

0 comments on commit c2c45d3

Please sign in to comment.