diff --git a/README.md b/README.md index 54d77e8..28dfb21 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,22 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/spyzhov/ajson)](https://goreportcard.com/report/github.com/spyzhov/ajson) [![GoDoc](https://godoc.org/github.com/spyzhov/ajson?status.svg)](https://godoc.org/github.com/spyzhov/ajson) [![Coverage Status](https://coveralls.io/repos/github/spyzhov/ajson/badge.svg?branch=master)](https://coveralls.io/github/spyzhov/ajson?branch=master) +[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go#json) -Abstract [JSON](https://www.json.org/) is a small golang package that provide a parser for JSON with support of JSONPath, in case when you are not sure in it's structure. +Abstract [JSON](https://www.json.org/) is a small golang package provides a parser for JSON with support of JSONPath, in case when you are not sure in its structure. -Method `Unmarshal` will scan all the byte slice to create a root node of JSON structure, with all it behaviors. +Method `Unmarshal` will scan all the byte slice to create a root node of JSON structure, with all its behaviors. Method `Marshal` will serialize current `Node` object to JSON structure. -Each `Node` has it's own type and calculated value, which will be calculated on demand. +Each `Node` has its own type and calculated value, which will be calculated on demand. Calculated value saves in `atomic.Value`, so it's thread safe. -Method `JSONPath` will returns slice of founded elements in current JSON data, by it's JSONPath. +Method `JSONPath` will returns slice of found elements in current JSON data, by [JSONPath](http://goessner.net/articles/JsonPath/) request. + +## Compare with other solutions + +Check the [cburgmer/json-path-comparison](https://cburgmer.github.io/json-path-comparison/) project. ## Example @@ -307,7 +312,8 @@ func main() { Current package supports JSONPath selection described at [http://goessner.net/articles/JsonPath/](http://goessner.net/articles/JsonPath/). -JSONPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination with an XML document. Since a JSON structure is usually anonymous and doesn't necessarily have a "root member object" JSONPath assumes the abstract name $ assigned to the outer level object. +JSONPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination with an XML document. +Since a JSON structure is usually anonymous and doesn't necessarily have a "root member object" JSONPath assumes the abstract name $ assigned to the outer level object. JSONPath expressions can use the dot–notation @@ -317,9 +323,10 @@ or the bracket–notation `$['store']['book'][0]['title']` -for input pathes. Internal or output pathes will always be converted to the more general bracket–notation. +for input paths. Internal or output paths will always be converted to the more general bracket–notation. -JSONPath allows the wildcard symbol `*` for member names and array indices. It borrows the descendant operator `..` from E4X and the array slice syntax proposal `[start:end:step]` from ECMASCRIPT 4. +JSONPath allows the wildcard symbol `*` for member names and array indices. +It borrows the descendant operator `..` from E4X and the array slice syntax proposal `[start:end:step]` from ECMASCRIPT 4. Expressions of the underlying scripting language `()` can be used as an alternative to explicit names or indices as in @@ -348,7 +355,7 @@ Here is a complete overview and a side by side comparison of the JSONPath syntax ### Predefined constant -Package has several predefined constants. You are free to add new one with `AddConstant` +Package has several predefined constants. e math.E float64 pi math.Pi float64 @@ -367,10 +374,16 @@ Package has several predefined constants. You are free to add new one with `AddC true true bool false false bool null nil interface{} + +You are free to add new one with function `AddConstant`: + +```go + AddConstant("c", NumericNode("speed of light in vacuum", 299_792_458)) +``` ### Supported operations -Package has several predefined operators. You are free to add new one with `AddOperator` +Package has several predefined operators. [Operator precedence](https://golang.org/ref/spec#Operator_precedence) @@ -407,9 +420,21 @@ Package has several predefined operators. You are free to add new one with `AddO >= larger or equals any =~ equals regex string strings +You are free to add new one with function `AddOperation`: + +```go + AddOperation("<>", 3, false, func(left *ajson.Node, right *ajson.Node) (node *ajson.Node, err error) { + result, err := left.Eq(right) + if err != nil { + return nil, err + } + return BoolNode("neq", !result), nil + }) +``` + ### Supported functions -Package has several predefined functions. You are free to add new one with `AddFunction` +Package has several predefined functions. abs math.Abs integers, floats acos math.Acos integers, floats @@ -454,6 +479,17 @@ Package has several predefined functions. You are free to add new one with `AddF y0 math.Y0 integers, floats y1 math.Y1 integers, floats +You are free to add new one with function `AddFunction`: + +```go + AddFunction("trim", func(node *ajson.Node) (result *Node, err error) { + if node.IsString() { + return StringNode("trim", strings.TrimSpace(node.MustString())), nil + } + return + }) +``` + # Benchmarks Current package is comparable with `encoding/json` package. @@ -499,9 +535,9 @@ $ go test -bench=. -cpu=1 -benchmem goos: linux goarch: amd64 pkg: github.com/spyzhov/ajson -BenchmarkUnmarshal_AJSON 91104 13756 ns/op 5344 B/op 95 allocs/op -BenchmarkUnmarshal_JSON 67794 16851 ns/op 968 B/op 31 allocs/op -BenchmarkJSONPath_all_prices 49650 25073 ns/op 7368 B/op 161 allocs/op +BenchmarkUnmarshal_AJSON 138032 8762 ns/op 5344 B/op 95 allocs/op +BenchmarkUnmarshal_JSON 117423 10502 ns/op 968 B/op 31 allocs/op +BenchmarkJSONPath_all_prices 80908 14394 ns/op 7128 B/op 153 allocs/op ``` # License diff --git a/buffer.go b/buffer.go index bda4d23..40d5177 100644 --- a/buffer.go +++ b/buffer.go @@ -499,6 +499,10 @@ func (b *buffer) rpn() (result rpn, err error) { stack = stack[:len(stack)-1] } + if len(result) == 0 { + return nil, b.errorEOF() + } + return } diff --git a/buffer_test.go b/buffer_test.go index 4ced36c..92b5ef3 100644 --- a/buffer_test.go +++ b/buffer_test.go @@ -92,7 +92,6 @@ func TestBuffer_RPN(t *testing.T) { {name: "example_8", value: "@.length-1", expected: []string{"@.length", "1", "-"}}, {name: "example_9", value: "@.length+-1", expected: []string{"@.length", "-1", "+"}}, {name: "example_10", value: "@.length/e", expected: []string{"@.length", "e", "/"}}, - {name: "example_11", value: "", expected: []string{}}, {name: "example_12", value: "123.456", expected: []string{"123.456"}}, {name: "example_13", value: " 123.456 ", expected: []string{"123.456"}}, @@ -140,6 +139,7 @@ func TestBuffer_RPNError(t *testing.T) { {value: "e + q"}, {value: "foo(e)"}, {value: "++2"}, + {value: ""}, } for _, test := range tests { t.Run(test.value, func(t *testing.T) { diff --git a/jsonpath.go b/jsonpath.go index fad4629..23b1bf9 100644 --- a/jsonpath.go +++ b/jsonpath.go @@ -394,6 +394,13 @@ func deReference(node *Node, commands []string) (result []*Node, err error) { } if ikeys[2] > 0 { + if ikeys[0] < 0 { + ikeys[0] = 0 + } + if ikeys[1] > element.Size() { + ikeys[1] = element.Size() + } + for i := ikeys[0]; i < ikeys[1]; i += ikeys[2] { value, ok := element.children[strconv.Itoa(i)] if ok { @@ -401,6 +408,13 @@ func deReference(node *Node, commands []string) (result []*Node, err error) { } } } else if ikeys[2] < 0 { + if ikeys[0] > element.Size() { + ikeys[0] = element.Size() + } + if ikeys[1] < -1 { + ikeys[1] = -1 + } + for i := ikeys[0]; i > ikeys[1]; i += ikeys[2] { value, ok := element.children[strconv.Itoa(i)] if ok { diff --git a/jsonpath_test.go b/jsonpath_test.go index ad76b30..9020f85 100644 --- a/jsonpath_test.go +++ b/jsonpath_test.go @@ -339,6 +339,42 @@ func TestJSONPath_suite(t *testing.T) { path: `$['"']`, expected: []interface{}{"value"}, // ["value"] }, + { + name: "$[2:113667776004]", + input: `["first", "second", "third", "forth", "fifth"]`, + path: `$[2:113667776004]`, + expected: []interface{}{"third", "forth", "fifth"}, // ["third", "forth", "fifth"] + }, + { + name: "$[2:-113667776004:-1]", + input: `["first", "second", "third", "forth", "fifth"]`, + path: `$[2:-113667776004:-1]`, + expected: []interface{}{"third", "second", "first"}, // ["third", "second", "first"] + }, + { + name: "$[-113667776004:2]", + input: `["first", "second", "third", "forth", "fifth"]`, + path: `$[-113667776004:2]`, + expected: []interface{}{"first", "second"}, // ["first", "second"] + }, + { + name: "$[113667776004:2:-1]", + input: `["first", "second", "third", "forth", "fifth"]`, + path: `$[113667776004:2:-1]`, + expected: []interface{}{"fifth", "forth"}, // ["fifth", "forth"] + }, + { + name: "$.length", + input: `[4, 5, 6]`, + path: `$.length`, + expected: []interface{}{float64(3)}, // [3] + }, + { + name: "$[?()]", + input: `[1, {"key": 42}, "value", null]`, + path: `$[?()]`, + wantErr: true, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/node.go b/node.go index c7a05e1..37a9716 100644 --- a/node.go +++ b/node.go @@ -205,7 +205,7 @@ func (n *Node) Source() []byte { // String is implementation of Stringer interface, returns string based on source part func (n *Node) String() string { - if n.ready() { + if n.ready() && !n.dirty { return string(n.Source()) } val := n.value.Load() @@ -481,19 +481,20 @@ func (n *Node) Unpack() (value interface{}, err error) { case Null: return nil, nil case Numeric: - value, err = strconv.ParseFloat(string(n.Source()), 64) - if err != nil { - return + value, err = n.Value() + if _, ok := value.(float64); !ok { + return nil, errorType() } case String: - var ok bool - value, ok = unquote(n.Source(), quotes) - if !ok { - return "", errorAt(n.borders[0], (*n.data)[n.borders[0]]) + value, err = n.Value() + if _, ok := value.(string); !ok { + return nil, errorType() } case Bool: - b := n.Source()[0] - value = b == 't' || b == 'T' + value, err = n.Value() + if _, ok := value.(bool); !ok { + return nil, errorType() + } case Array: children := make([]interface{}, len(n.children)) for _, child := range n.children {