From c2c45d37e25447b7b32b9f2100e3179a2dab89f1 Mon Sep 17 00:00:00 2001 From: Stepan Pyzhov Date: Fri, 22 Mar 2019 19:33:52 +0300 Subject: [PATCH] fix jsonpath parser --- buffer.go | 46 +++++++++++--------- buffer_test.go | 9 ++++ jsonpath.go | 107 +++++++++++++++++++++++++++++++---------------- jsonpath_test.go | 3 +- 4 files changed, 110 insertions(+), 55 deletions(-) diff --git a/buffer.go b/buffer.go index c8bb3fb..ef3295b 100644 --- a/buffer.go +++ b/buffer.go @@ -275,44 +275,51 @@ 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 } @@ -320,7 +327,10 @@ 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) { @@ -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 @@ -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] diff --git a/buffer_test.go b/buffer_test.go index 46f51fe..7b04e7f 100644 --- a/buffer_test.go +++ b/buffer_test.go @@ -20,6 +20,15 @@ func TestBuffer_Token(t *testing.T) { {name: "part 1", value: "@.foo+@.bar", 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}, diff --git a/jsonpath.go b/jsonpath.go index 21d9acd..20be143 100644 --- a/jsonpath.go +++ b/jsonpath.go @@ -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 { @@ -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] @@ -148,26 +184,33 @@ 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 @@ -175,7 +218,7 @@ func ParseJSONPath(path string) (result []string, err error) { if err != nil { break } - if b == dot { + if c == dot { result = append(result, "..") buf.index-- break @@ -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() } diff --git a/jsonpath_test.go b/jsonpath_test.go index a685f80..52aa121 100644 --- a/jsonpath_test.go +++ b/jsonpath_test.go @@ -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]]"}, @@ -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) {