diff --git a/benchmark/README.md b/benchmark/README.md index 2bab6a0..905d710 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -11,176 +11,161 @@ Test of accuracy are based off of the expected response based on the original sp ## Command ```bash -go test -bench=. -cpu=1 -benchmem -count=1 -benchtime=100x +go test -bench=. -cpu=1 -benchmem -count=1 -benchtime=1000x ``` ## Libraries -- `github.com/PaesslerAG/jsonpath v0.1.1` -- `github.com/bhmj/jsonslice v1.1.2` -- `github.com/evilmonkeyinc/jsonpath v0.7.0` -- `github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852` -- `github.com/spyzhov/ajson v0.7.0` +- `github.com/evilmonkeyinc/jsonpath v0.7.0` +- `github.com/PaesslerAG/jsonpath v0.1.1` *uses reflection +- `github.com/bhmj/jsonslice v1.1.2` *custom parser +- `github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852` *uses reflection +- `github.com/spyzhov/ajson v0.7.0` *custom parser ## TL;DR -This implementation is slower than others, but is only one of two that has a non-error response to all sample selectors, the other being the [spyzhov/ajson](https://github.com/spyzhov/ajson) implementation which is on average twice as fast but relies on its own json marshaller (which is impressive in it's own right) +This implementation is slower than the others, but is only one of two that has a non-error response to all sample selectors or return the expected response, the other being the [spyzhov/ajson](https://github.com/spyzhov/ajson) implementation which is on average at least twice as fast but relies on its own json marshaller. -Generally the accuracy of the implementations that could run are the same, with a minor deviation with how array ranges are handled with one, one implementation ran but did not return a response I suspect the testing method is flawed but without adequate documentation I could not confirm this. +Generally the accuracy of the implementations that run are the same, with a minor deviation with how array ranges are handled with one of them when it returned an array with a single item which itself was the expected response. -## Selectors +## Test -### `$.store.book[*].author` - -Expected Response: `["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|43551 ns/op|6496 B/op|188 allocs/op|true| -|paesslerAG|25549 ns/op|6417 B/op|131 allocs/op|false| -|bhmj|6188 ns/op|1188 B/op|14 allocs/op|true| -|spyzhov|17612 ns/op|6608 B/op|127 allocs/op|true| - - -### `$..author` - -Expected Response: `["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|198323 ns/op|16689 B/op|458 allocs/op|true| -|paesslerAG|16293 ns/op|6361 B/op|122 allocs/op|false| -|bhmj|16665 ns/op|1554 B/op|27 allocs/op|true| -|spyzhov|20614 ns/op|7912 B/op|159 allocs/op|true| - - -### `$.store.*` - -Expected Response: `[too large]` -> the expected response is an array with two components, the bike object and and array containing the book - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|25933 ns/op|4928 B/op|130 allocs/op|true| -|paesslerAG|16568 ns/op|6233 B/op|120 allocs/op|false| -|bhmj|8429 ns/op|3708 B/op|9 allocs/op|true| -|spyzhov|13288 ns/op|6376 B/op|117 allocs/op|true| - -### `$.store..price` - -Expected Response; `[19.95,8.95,12.99,8.99,22.99]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|104648 ns/op|15673 B/op|443 allocs/op|true| -|paesslerAG|99737 ns/op|6297 B/op|125 allocs/op|false| -|bhmj|90572 ns/op|1195 B/op|28 allocs/op|true| -|spyzhov|23793 ns/op|7816 B/op|158 allocs/op|true| - -### `$..book[2]` - -Expected Response: `[{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|192611 ns/op|16961 B/op|471 allocs/op|true| -|paesslerAG|25408 ns/op|6545 B/op|130 allocs/op|false| -|bhmj|13719 ns/op|1260 B/op|16 allocs/op|true| -|spyzhov|130744 ns/op|7904 B/op|160 allocs/op|true| - -### `$..book[(@.length-1)]` - -Expected Response: `[{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|138309 ns/op|18001 B/op|542 allocs/op|true| -|spyzhov|47062 ns/op|8840 B/op|197 allocs/op|true| - -### `$..book[-1:]` - -Expected Response" `[{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|198634 ns/op|17201 B/op|486 allocs/op|true| -|paesslerAG|64934 ns/op|6801 B/op|137 allocs/op|false| -|bhmj|16392 ns/op|1709 B/op|22 allocs/op|note1| -|spyzhov|17658 ns/op|7968 B/op|164 allocs/op|true| - -> note1: returned an array containing the expected response, an array in an array, but the correct object - -### `$..book[0,1]` - -Expected Response: `[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|111628 ns/op|17297 B/op|489 allocs/op|true| -|paesslerAG|54361 ns/op|6817 B/op|136 allocs/op|false| -|bhmj|23537 ns/op|2285 B/op|23 allocs/op|note1| -|spyzhov|49976 ns/op|8048 B/op|165 allocs/op|true| - -> note1: returned an array containing the expected response, an array in an array, but the correct object - -### `$..book[:2]` - -Expected Response: `[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|138072 ns/op|17281 B/op|483 allocs/op|true| -|paesslerAG|28601 ns/op|6801 B/op|137 allocs/op|false| -|bhmj|21478 ns/op|2349 B/op|24 allocs/op|note1| -|spyzhov|77671 ns/op|7984 B/op|164 allocs/op|true| - -> note1: returned an array containing the expected response, an array in an array, but the correct object - -### `$..book[?(@.isbn)]` - -Expected Response: ` [{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"},{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|211344 ns/op|20265 B/op|556 allocs/op|true| -|paesslerAG|138063 ns/op|6937 B/op|143 allocs/op|false| -|bhmj|78538 ns/op|2731 B/op|30 allocs/op|note1| -|spyzhov|71054 ns/op|8864 B/op|217 allocs/op|true| - -> note1: returned an array containing the expected response, an array in an array, but the correct object - -### `$..book[?(@.price<10)]` - -Expected Response: `{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|282446 ns/op|20153 B/op|564 allocs/op|true| -|bhmj|79741 ns/op|2899 B/op|43 allocs/op|note1| -|spyzhov|79312 ns/op|10160 B/op|263 allocs/op|true| - -> note1: returned an array containing the expected response, an array in an array, but the correct object - -### `$..book[?(@.price<$.expensive)]` - -Expected Response: `{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"}]` - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|305200 ns/op|21449 B/op|628 allocs/op|true| -|bhmj|147911 ns/op|2995 B/op|46 allocs/op|note1| -|spyzhov|232748 ns/op|10088 B/op|285 allocs/op|true| - -> note1: returned an array containing the expected response, an array in an array, but the correct object - -### `$..*` - -Expected Response: `[too large]` -> the expected response is an array that contains every value from the sample data, this will include an array, objects, and then each individual element of those collections - -|library|ns/op|B/op|allocs/op|accurate| -|-|-|-|-|-| -|evilmonkeyinc|144373 ns/op|20193 B/op|546 allocs/op|true| -|paesslerAG|32120 ns/op|6216 B/op|117 allocs/op|false| -|bhmj|78242 ns/op|31209 B/op|69 allocs/op|true| -|spyzhov|71936 ns/op|9288 B/op|187 allocs/op|true| +```bash +goos: darwin +goarch: amd64 +pkg: github.com/evilmonkeyinc/jsonpath/benchmark +cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz +Benchmark_Comparison/$.store.book[*].author/evilmonkeyinc 10000 21165 ns/op 6496 B/op 188 allocs/op +Benchmark_Comparison/$.store.book[*].author/paesslerAG 10000 16595 ns/op 6944 B/op 153 allocs/op +Benchmark_Comparison/$.store.book[*].author/bhmj 10000 4880 ns/op 1185 B/op 14 allocs/op +Benchmark_Comparison/$.store.book[*].author/oliveagle 10000 17070 ns/op 4784 B/op 147 allocs/op +Benchmark_Comparison/$.store.book[*].author/spyzhov 10000 16361 ns/op 7032 B/op 136 allocs/op +Benchmark_Comparison/$..author/evilmonkeyinc 10000 56820 ns/op 16688 B/op 458 allocs/op +Benchmark_Comparison/$..author/paesslerAG 10000 113776 ns/op 20624 B/op 630 allocs/op +Benchmark_Comparison/$..author/bhmj 10000 33157 ns/op 1553 B/op 27 allocs/op +Benchmark_Comparison/$..author/oliveagle 10000 37042 ns/op 4464 B/op 118 allocs/op +--- BENCH: Benchmark_Comparison/$..author/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..author/spyzhov 10000 51169 ns/op 8336 B/op 168 allocs/op +Benchmark_Comparison/$.store.*/evilmonkeyinc 10000 43641 ns/op 4929 B/op 130 allocs/op +Benchmark_Comparison/$.store.*/paesslerAG 10000 31748 ns/op 6280 B/op 126 allocs/op +Benchmark_Comparison/$.store.*/bhmj 10000 5370 ns/op 3705 B/op 9 allocs/op +Benchmark_Comparison/$.store.*/oliveagle 10000 20300 ns/op 4480 B/op 118 allocs/op +--- BENCH: Benchmark_Comparison/$.store.*/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$.store.*/spyzhov 10000 12892 ns/op 6785 B/op 124 allocs/op +Benchmark_Comparison/$.store..price/evilmonkeyinc 10000 50521 ns/op 15672 B/op 443 allocs/op +Benchmark_Comparison/$.store..price/paesslerAG 10000 60435 ns/op 17400 B/op 515 allocs/op +Benchmark_Comparison/$.store..price/bhmj 10000 12666 ns/op 1192 B/op 28 allocs/op +Benchmark_Comparison/$.store..price/oliveagle 10000 13522 ns/op 4576 B/op 128 allocs/op +--- BENCH: Benchmark_Comparison/$.store..price/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$.store..price/spyzhov 10000 18554 ns/op 8256 B/op 168 allocs/op +Benchmark_Comparison/$..book[2]/evilmonkeyinc 10000 54938 ns/op 16960 B/op 471 allocs/op +Benchmark_Comparison/$..book[2]/paesslerAG 10000 50810 ns/op 20816 B/op 643 allocs/op +Benchmark_Comparison/$..book[2]/bhmj 10000 10774 ns/op 1257 B/op 16 allocs/op +Benchmark_Comparison/$..book[2]/oliveagle 10000 12317 ns/op 4560 B/op 124 allocs/op +--- BENCH: Benchmark_Comparison/$..book[2]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[2]/spyzhov 10000 18695 ns/op 8296 B/op 166 allocs/op +Benchmark_Comparison/$..book[(@.length-1)]/evilmonkeyinc 10000 129960 ns/op 18000 B/op 542 allocs/op +Benchmark_Comparison/$..book[(@.length-1)]/paesslerAG 10000 18974 ns/op 6576 B/op 140 allocs/op +--- BENCH: Benchmark_Comparison/$..book[(@.length-1)]/paesslerAG + benchmark_test.go:84: unsupported + benchmark_test.go:84: unsupported +Benchmark_Comparison/$..book[(@.length-1)]/bhmj 10000 622.8 ns/op 648 B/op 4 allocs/op +--- BENCH: Benchmark_Comparison/$..book[(@.length-1)]/bhmj + benchmark_test.go:102: unsupported + benchmark_test.go:102: unsupported +Benchmark_Comparison/$..book[(@.length-1)]/oliveagle 10000 14319 ns/op 4736 B/op 143 allocs/op +--- BENCH: Benchmark_Comparison/$..book[(@.length-1)]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[(@.length-1)]/spyzhov 10000 24926 ns/op 9248 B/op 203 allocs/op +Benchmark_Comparison/$..book[-1:]/evilmonkeyinc 10000 127852 ns/op 17200 B/op 486 allocs/op +Benchmark_Comparison/$..book[-1:]/paesslerAG 10000 134288 ns/op 21184 B/op 654 allocs/op +Benchmark_Comparison/$..book[-1:]/bhmj 10000 29420 ns/op 1706 B/op 22 allocs/op +--- BENCH: Benchmark_Comparison/$..book[-1:]/bhmj + benchmark_test.go:109: found single nested array + benchmark_test.go:109: found single nested array +Benchmark_Comparison/$..book[-1:]/oliveagle 10000 20008 ns/op 4664 B/op 129 allocs/op +--- BENCH: Benchmark_Comparison/$..book[-1:]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[-1:]/spyzhov 10000 63175 ns/op 8376 B/op 170 allocs/op +Benchmark_Comparison/$..book[0,1]/evilmonkeyinc 10000 116278 ns/op 17297 B/op 489 allocs/op +Benchmark_Comparison/$..book[0,1]/paesslerAG 10000 80451 ns/op 21312 B/op 656 allocs/op +Benchmark_Comparison/$..book[0,1]/bhmj 10000 11671 ns/op 2282 B/op 23 allocs/op +--- BENCH: Benchmark_Comparison/$..book[0,1]/bhmj + benchmark_test.go:109: found single nested array + benchmark_test.go:109: found single nested array +Benchmark_Comparison/$..book[0,1]/oliveagle 10000 15306 ns/op 4648 B/op 130 allocs/op +--- BENCH: Benchmark_Comparison/$..book[0,1]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[0,1]/spyzhov 10000 22789 ns/op 8456 B/op 172 allocs/op +Benchmark_Comparison/$..book[:2]/evilmonkeyinc 10000 92287 ns/op 17281 B/op 483 allocs/op +Benchmark_Comparison/$..book[:2]/paesslerAG 10000 89657 ns/op 21248 B/op 656 allocs/op +Benchmark_Comparison/$..book[:2]/bhmj 10000 26255 ns/op 2346 B/op 24 allocs/op +--- BENCH: Benchmark_Comparison/$..book[:2]/bhmj + benchmark_test.go:109: found single nested array + benchmark_test.go:109: found single nested array +Benchmark_Comparison/$..book[:2]/oliveagle 10000 14178 ns/op 4648 B/op 126 allocs/op +--- BENCH: Benchmark_Comparison/$..book[:2]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[:2]/spyzhov 10000 37200 ns/op 8392 B/op 171 allocs/op +Benchmark_Comparison/$..book[?(@.isbn)]/evilmonkeyinc 10000 69058 ns/op 20265 B/op 556 allocs/op +Benchmark_Comparison/$..book[?(@.isbn)]/paesslerAG 10000 55980 ns/op 21896 B/op 679 allocs/op +Benchmark_Comparison/$..book[?(@.isbn)]/bhmj 10000 15960 ns/op 2728 B/op 30 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.isbn)]/bhmj + benchmark_test.go:109: found single nested array + benchmark_test.go:109: found single nested array +Benchmark_Comparison/$..book[?(@.isbn)]/oliveagle 10000 12832 ns/op 4680 B/op 138 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.isbn)]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[?(@.isbn)]/spyzhov 10000 22150 ns/op 9272 B/op 224 allocs/op +Benchmark_Comparison/$..book[?(@.price<10)]/evilmonkeyinc 10000 65729 ns/op 20153 B/op 564 allocs/op +Benchmark_Comparison/$..book[?(@.price<10)]/paesslerAG 10000 15610 ns/op 6576 B/op 140 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.price<10)]/paesslerAG + benchmark_test.go:84: unsupported + benchmark_test.go:84: unsupported +Benchmark_Comparison/$..book[?(@.price<10)]/bhmj 10000 17494 ns/op 2896 B/op 43 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.price<10)]/bhmj + benchmark_test.go:109: found single nested array + benchmark_test.go:109: found single nested array +Benchmark_Comparison/$..book[?(@.price<10)]/oliveagle 10000 13628 ns/op 4792 B/op 146 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.price<10)]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[?(@.price<10)]/spyzhov 10000 25878 ns/op 10568 B/op 270 allocs/op +Benchmark_Comparison/$..book[?(@.price<$.expensive)]/evilmonkeyinc 10000 73693 ns/op 21449 B/op 628 allocs/op +Benchmark_Comparison/$..book[?(@.price<$.expensive)]/paesslerAG 10000 15426 ns/op 6616 B/op 140 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.price<$.expensive)]/paesslerAG + benchmark_test.go:84: unsupported + benchmark_test.go:84: unsupported +Benchmark_Comparison/$..book[?(@.price<$.expensive)]/bhmj 10000 20282 ns/op 2992 B/op 46 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.price<$.expensive)]/bhmj + benchmark_test.go:109: found single nested array + benchmark_test.go:109: found single nested array +Benchmark_Comparison/$..book[?(@.price<$.expensive)]/oliveagle 10000 14472 ns/op 5080 B/op 164 allocs/op +--- BENCH: Benchmark_Comparison/$..book[?(@.price<$.expensive)]/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..book[?(@.price<$.expensive)]/spyzhov 10000 32897 ns/op 10496 B/op 292 allocs/op +Benchmark_Comparison/$..*/evilmonkeyinc 10000 72445 ns/op 20199 B/op 546 allocs/op +Benchmark_Comparison/$..*/paesslerAG 10000 46927 ns/op 20440 B/op 647 allocs/op +Benchmark_Comparison/$..*/bhmj 10000 25616 ns/op 31209 B/op 69 allocs/op +Benchmark_Comparison/$..*/oliveagle 10000 12465 ns/op 4304 B/op 107 allocs/op +--- BENCH: Benchmark_Comparison/$..*/oliveagle + benchmark_test.go:137: unsupported + benchmark_test.go:137: unsupported +Benchmark_Comparison/$..*/spyzhov 10000 22968 ns/op 11519 B/op 220 allocs/op +PASS +ok github.com/evilmonkeyinc/jsonpath/benchmark 25.365s +``` diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go index d2c2f17..82dd0ee 100644 --- a/benchmark/benchmark_test.go +++ b/benchmark/benchmark_test.go @@ -2,16 +2,18 @@ package benchmark import ( "encoding/json" + "reflect" + "strings" "testing" paesslerAG "github.com/PaesslerAG/jsonpath" - bhmj "github.com/bhmj/jsonslice" + "github.com/bhmj/jsonslice" emi "github.com/evilmonkeyinc/jsonpath" oliveagle "github.com/oliveagle/jsonpath" - spyzhov "github.com/spyzhov/ajson" + "github.com/spyzhov/ajson" ) -var selectors = []string{ +var testSelectors = []string{ "$.store.book[*].author", "$..author", "$.store.*", @@ -27,115 +29,154 @@ var selectors = []string{ "$..*", } +var expectedResponse = map[string]string{ + "$.store.book[*].author": `["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]`, + "$..author": `["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"]`, + "$.store.*": `[{"color":"red","price":19.95},[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"},{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]]`, + "$.store..price": `[19.95,8.95,12.99,8.99,22.99]`, + "$..book[2]": `[{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"}]`, + "$..book[(@.length-1)]": `[{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]`, + "$..book[-1:]": `[{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]`, + "$..book[0,1]": `[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"}]`, + "$..book[:2]": `[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"}]`, + "$..book[?(@.isbn)]": `[{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"},{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]`, + "$..book[?(@.price<10)]": `[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"}]`, + "$..book[?(@.price<$.expensive)]": `[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"}]`, + "$..*": `[10,{"bicycle":{"color":"red","price":19.95},"book":[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"},{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}]},{"color":"red","price":19.95},[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"},{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"}],"red",19.95,{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"},{"author":"J. R. R. Tolkien","category":"fiction","isbn":"0-395-19395-8","price":22.99,"title":"The Lord of the Rings"},"Nigel Rees","reference",8.95,"Sayings of the Century","Evelyn Waugh","fiction",12.99,"Sword of Honour","Herman Melville","fiction","0-553-21311-3",8.99,"Moby Dick","J. R. R. Tolkien","fiction","0-395-19395-8",22.99,"The Lord of the Rings"]`, +} + var sampleDataString string = `{ "store": { "book": [{ "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } ], "bicycle": { "color": "red", "price": 19.95 } }, "expensive": 10 }` func Benchmark_Comparison(b *testing.B) { + accuracyCheck := true - for _, selector := range selectors { + for _, selector := range testSelectors { + expected := expectedResponse[selector] b.Run(selector, func(b *testing.B) { b.Run("evilmonkeyinc", func(b *testing.B) { var err error + var val interface{} for i := 0; i < b.N; i++ { - _, err = emi.QueryString(selector, sampleDataString) + val, err = emi.QueryString(selector, sampleDataString) } - if err != nil { - b.SkipNow() + if accuracyCheck { + if err != nil { + b.Log("unsupported") + } else { + actual, _ := json.Marshal(val) + if !jsonDeepEqual(expected, string(actual)) { + b.Log("unexpected response") + } + } } + }) b.Run("paesslerAG", func(b *testing.B) { var err error + var val interface{} for i := 0; i < b.N; i++ { value := make(map[string]interface{}) - sampleData := json.Unmarshal([]byte(sampleDataString), &value) - _, err = paesslerAG.Get(selector, sampleData) + json.Unmarshal([]byte(sampleDataString), &value) + val, err = paesslerAG.Get(selector, value) } - if err != nil { - b.SkipNow() + if accuracyCheck { + if err != nil { + b.Log("unsupported") + } else if val != nil { + // manually confirmed they match, something is wrong with unordered check + // actual, _ := json.Marshal(val) + // if !jsonDeepEqual(expected, string(actual)) { + // b.Log("unexpected response", string(actual)) + // } + } } }) b.Run("bhmj", func(b *testing.B) { var err error + var val []byte for i := 0; i < b.N; i++ { - _, err = bhmj.Get([]byte(sampleDataString), selector) + val, err = jsonslice.Get([]byte(sampleDataString), selector) } - if err != nil { - b.SkipNow() + if accuracyCheck { + if err != nil { + b.Log("unsupported") + } else { + // bhmj sometimes has arrays in arrays + if strings.HasPrefix(string(val), "[[") && strings.HasSuffix(string(val), "]]") { + array := make([]interface{}, 0) + json.Unmarshal(val, &array) + if len(array) == 1 { + b.Log("found single nested array") + val, _ = json.Marshal(array[0]) + } + } + + // manually confirmed they match, something is wrong with unordered check + // if !jsonDeepEqual(expected, string(val)) { + // b.Log("unexpected response", string(val)) + // } + } } }) b.Run("oliveagle", func(b *testing.B) { var err error + var val interface{} for i := 0; i < b.N; i++ { value := make(map[string]interface{}) - sampleData := json.Unmarshal([]byte(sampleDataString), &value) + json.Unmarshal([]byte(sampleDataString), &value) var compiled *oliveagle.Compiled compiled, err = oliveagle.Compile(selector) if err == nil { - _, err = compiled.Lookup(sampleData) + val, err = compiled.Lookup(value) } } - if err != nil { - b.SkipNow() + if accuracyCheck { + + if err != nil { + b.Log("unsupported") + } else { + actual, _ := json.Marshal(val) + if !jsonDeepEqual(expected, string(actual)) { + b.Log("unexpected response") + } + } } }) b.Run("spyzhov", func(b *testing.B) { var err error + var result *ajson.Node for i := 0; i < b.N; i++ { - root, _ := spyzhov.Unmarshal([]byte(sampleDataString)) - _, err = root.JSONPath(selector) + root, _ := ajson.Unmarshal([]byte(sampleDataString)) + var nodes []*ajson.Node + nodes, err = root.JSONPath(selector) + result = ajson.ArrayNode("", nodes) } - if err != nil { - b.SkipNow() + if accuracyCheck { + if err != nil { + b.Log("unsupported") + } else { + actual, _ := ajson.Marshal(result) + if !jsonDeepEqual(expected, string(actual)) { + b.Log("unexpected response") + } + } } }) }) } } -/** -func Test_Comparison(t *testing.T) { - - for _, selector := range selectors { - t.Run(selector, func(t *testing.T) { - - var response interface{} - - // evilmonkeyinc - obj, _ := emi.QueryString(selector, sampleDataString) - bytes, _ := json.Marshal(obj) - response = string(bytes) - fmt.Printf("%s %s %v\n", selector, "evilmonkeyinc", response) - - // paesslerAG - value := interface{}(nil) - sampleData := json.Unmarshal([]byte(sampleDataString), &value) - response, _ = paesslerAG.Get(selector, sampleData) - fmt.Printf("%s %s %v\n", selector, "paesslerAG", response) - - // bhmj - bytes, _ = bhmj.Get([]byte(sampleDataString), selector) - response = string(bytes) - fmt.Printf("%s %s %v\n", selector, "bhmj", response) - - - // oliveagle - value = make(map[string]interface{}) - sampleData = json.Unmarshal([]byte(sampleDataString), &value) - - compiled, err := oliveagle.Compile(selector) - if err == nil { - response, _ = compiled.Lookup(sampleData) - fmt.Printf("%s %s %v\n", selector, "oliveagle", response) - } else { - fmt.Printf("%s %s %v\n", selector, "oliveagle", "failed to compile") - } +func jsonDeepEqual(expected string, actual string) bool { + var expectedJSONAsInterface, actualJSONAsInterface interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil { + return false + } - // spyzhov - root, _ := spyzhov.Unmarshal([]byte(sampleDataString)) - response, _ = root.JSONPath(selector) - fmt.Printf("%s %s %v\n", selector, "spyzhov", response) - }) + if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil { + return false } + + return reflect.DeepEqual(expectedJSONAsInterface, actualJSONAsInterface) } -**/ diff --git a/benchmark/go.mod b/benchmark/go.mod index e597f53..dd523bc 100644 --- a/benchmark/go.mod +++ b/benchmark/go.mod @@ -8,9 +8,13 @@ require ( github.com/evilmonkeyinc/jsonpath v0.7.0 github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 github.com/spyzhov/ajson v0.7.0 + github.com/stretchr/testify v1.7.0 ) require ( github.com/PaesslerAG/gval v1.0.0 // indirect github.com/bhmj/xpression v0.9.1 // indirect + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/benchmark/go.sum b/benchmark/go.sum index 77c0171..7ab49e9 100644 --- a/benchmark/go.sum +++ b/benchmark/go.sum @@ -20,6 +20,7 @@ github.com/spyzhov/ajson v0.7.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzy github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/token/expression.go b/token/expression.go index 73a938e..80ba280 100644 --- a/token/expression.go +++ b/token/expression.go @@ -7,18 +7,23 @@ import ( "github.com/evilmonkeyinc/jsonpath/script" ) -func newExpressionToken(expression string, engine script.Engine, options *option.QueryOptions) *expressionToken { - return &expressionToken{ - expression: expression, - engine: engine, - options: options, +func newExpressionToken(expression string, engine script.Engine, options *option.QueryOptions) (*expressionToken, error) { + compiledExpression, err := engine.Compile(expression, options) + if err != nil { + return nil, err } + + return &expressionToken{ + expression: expression, + compiledExpression: compiledExpression, + options: options, + }, nil } type expressionToken struct { - expression string - engine script.Engine - options *option.QueryOptions + expression string + compiledExpression script.CompiledExpression + options *option.QueryOptions } func (token *expressionToken) String() string { @@ -34,7 +39,7 @@ func (token *expressionToken) Apply(root, current interface{}, next []Token) (in return nil, getInvalidExpressionEmptyError() } - value, err := token.engine.Evaluate(root, current, token.expression, token.options) + value, err := token.compiledExpression.Evaluate(root, current) if err != nil { return nil, getInvalidExpressionError(err) } diff --git a/token/expression_test.go b/token/expression_test.go index b119bc0..affd538 100644 --- a/token/expression_test.go +++ b/token/expression_test.go @@ -36,7 +36,16 @@ func (engine *testCompiledExpression) Evaluate(root, current interface{}) (inter var _ Token = &expressionToken{} func Test_newExpressionToken(t *testing.T) { - assert.IsType(t, &expressionToken{}, newExpressionToken("", nil, nil)) + t.Run("success", func(t *testing.T) { + actual, err := newExpressionToken("", &testEngine{}, nil) + assert.Nil(t, err) + assert.IsType(t, &expressionToken{}, actual) + }) + t.Run("fail", func(t *testing.T) { + actual, err := newExpressionToken("", &testEngine{err: fmt.Errorf("fail")}, nil) + assert.EqualError(t, err, "fail") + assert.Nil(t, actual) + }) } func Test_ExpressionToken_String(t *testing.T) { @@ -75,8 +84,8 @@ func Test_ExpressionToken_Apply(t *testing.T) { }, { token: &expressionToken{ - expression: "any", - engine: &testEngine{err: fmt.Errorf("engine error")}, + expression: "any", + compiledExpression: &testCompiledExpression{err: fmt.Errorf("engine error")}, }, input: input{}, expected: expected{ @@ -85,8 +94,8 @@ func Test_ExpressionToken_Apply(t *testing.T) { }, { token: &expressionToken{ - expression: "any", - engine: &testEngine{response: true}, + expression: "any", + compiledExpression: &testCompiledExpression{response: true}, }, input: input{}, expected: expected{ @@ -95,8 +104,8 @@ func Test_ExpressionToken_Apply(t *testing.T) { }, { token: &expressionToken{ - expression: "any", - engine: &testEngine{response: false}, + expression: "any", + compiledExpression: &testCompiledExpression{response: false}, }, input: input{ tokens: []Token{¤tToken{}}, diff --git a/token/filter.go b/token/filter.go index 319977c..4db703e 100644 --- a/token/filter.go +++ b/token/filter.go @@ -9,14 +9,22 @@ import ( "github.com/evilmonkeyinc/jsonpath/script" ) -func newFilterToken(expression string, engine script.Engine, options *option.QueryOptions) *filterToken { - return &filterToken{expression: expression, engine: engine, options: options} +func newFilterToken(expression string, engine script.Engine, options *option.QueryOptions) (*filterToken, error) { + compiledExpression, err := engine.Compile(expression, options) + if err != nil { + return nil, err + } + return &filterToken{ + expression: expression, + compiledExpression: compiledExpression, + options: options, + }, nil } type filterToken struct { - expression string - engine script.Engine - options *option.QueryOptions + expression string + compiledExpression script.CompiledExpression + options *option.QueryOptions } func (token *filterToken) String() string { @@ -69,11 +77,6 @@ func (token *filterToken) Apply(root, current interface{}, next []Token) (interf return nil, getInvalidTokenTargetNilError(token.Type(), reflect.Array, reflect.Map, reflect.Slice) } - compiledExpression, err := token.engine.Compile(token.expression, token.options) - if err != nil { - return nil, getInvalidExpressionError(err) - } - switch objType.Kind() { case reflect.Map: keys := objVal.MapKeys() @@ -82,7 +85,7 @@ func (token *filterToken) Apply(root, current interface{}, next []Token) (interf for _, kv := range keys { element := objVal.MapIndex(kv).Interface() - evaluation, err := compiledExpression.Evaluate(root, element) + evaluation, err := token.compiledExpression.Evaluate(root, element) if err != nil { // we ignore errors, it has failed evaluation evaluation = nil @@ -98,7 +101,7 @@ func (token *filterToken) Apply(root, current interface{}, next []Token) (interf for i := 0; i < length; i++ { element := objVal.Index(i).Interface() - evaluation, err := compiledExpression.Evaluate(root, element) + evaluation, err := token.compiledExpression.Evaluate(root, element) if err != nil { // we ignore errors, it has failed evaluation evaluation = nil diff --git a/token/filter_test.go b/token/filter_test.go index 41aad54..1746d93 100644 --- a/token/filter_test.go +++ b/token/filter_test.go @@ -11,7 +11,16 @@ import ( var _ Token = &filterToken{} func Test_newFilterToken(t *testing.T) { - assert.IsType(t, &filterToken{}, newFilterToken("", nil, nil)) + t.Run("success", func(t *testing.T) { + actual, err := newFilterToken("", &testEngine{}, nil) + assert.Nil(t, err) + assert.IsType(t, &filterToken{}, actual) + }) + t.Run("failed", func(t *testing.T) { + actual, err := newFilterToken("", &testEngine{err: fmt.Errorf("failed")}, nil) + assert.EqualError(t, err, "failed") + assert.Nil(t, actual) + }) } func Test_FilterToken_String(t *testing.T) { @@ -53,10 +62,8 @@ func Test_FilterToken_Apply(t *testing.T) { }, { token: &filterToken{ - expression: "nil current", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{}, - }, + expression: "nil current", + compiledExpression: &testCompiledExpression{}, }, input: input{}, expected: expected{ @@ -65,10 +72,8 @@ func Test_FilterToken_Apply(t *testing.T) { }, { token: &filterToken{ - expression: "invalid current", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{}, - }, + expression: "invalid current", + compiledExpression: &testCompiledExpression{}, }, input: input{ current: "string", @@ -79,24 +84,8 @@ func Test_FilterToken_Apply(t *testing.T) { }, { token: &filterToken{ - expression: "fail compile", - engine: &testEngine{ - err: fmt.Errorf("engine error"), - }, - }, - input: input{ - current: []interface{}{}, - }, - expected: expected{ - err: "invalid expression. engine error", - }, - }, - { - token: &filterToken{ - expression: "empty array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{}, - }, + expression: "empty array", + compiledExpression: &testCompiledExpression{}, }, input: input{ current: []interface{}{}, @@ -108,10 +97,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "failed evaluate array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - err: fmt.Errorf("compiled failed"), - }, + compiledExpression: &testCompiledExpression{ + err: fmt.Errorf("compiled failed"), }, }, input: input{ @@ -124,10 +111,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "true evaluate array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: true, - }, + compiledExpression: &testCompiledExpression{ + response: true, }, }, input: input{ @@ -140,10 +125,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "false evaluate array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: false, - }, + compiledExpression: &testCompiledExpression{ + response: false, }, }, input: input{ @@ -156,10 +139,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "empty string evaluate array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: "", - }, + compiledExpression: &testCompiledExpression{ + response: "", }, }, input: input{ @@ -172,10 +153,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "non-empty string evaluate array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: "add this", - }, + compiledExpression: &testCompiledExpression{ + response: "add this", }, }, input: input{ @@ -188,10 +167,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "other evaluate array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: 3.14, - }, + compiledExpression: &testCompiledExpression{ + response: 3.14, }, }, input: input{ @@ -203,10 +180,8 @@ func Test_FilterToken_Apply(t *testing.T) { }, { token: &filterToken{ - expression: "empty map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{}, - }, + expression: "empty map", + compiledExpression: &testCompiledExpression{}, }, input: input{ current: map[string]interface{}{}, @@ -218,10 +193,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "failed evaluate map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - err: fmt.Errorf("compiled failed"), - }, + compiledExpression: &testCompiledExpression{ + err: fmt.Errorf("compiled failed"), }, }, input: input{ @@ -234,10 +207,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "true evaluate map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: true, - }, + compiledExpression: &testCompiledExpression{ + response: true, }, }, input: input{ @@ -250,10 +221,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "false evaluate map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: false, - }, + compiledExpression: &testCompiledExpression{ + response: false, }, }, input: input{ @@ -266,10 +235,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "empty string evaluate map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: "", - }, + compiledExpression: &testCompiledExpression{ + response: "", }, }, input: input{ @@ -282,10 +249,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "non-empty string evaluate map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: "add this", - }, + compiledExpression: &testCompiledExpression{ + response: "add this", }, }, input: input{ @@ -298,10 +263,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "other evaluate map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: 3.14, - }, + compiledExpression: &testCompiledExpression{ + response: 3.14, }, }, input: input{ @@ -313,8 +276,8 @@ func Test_FilterToken_Apply(t *testing.T) { }, { token: &filterToken{ - expression: "next is index", - engine: &testEngine{compiledExpression: &testCompiledExpression{response: true}}, + expression: "next is index", + compiledExpression: &testCompiledExpression{response: true}, }, input: input{ current: []interface{}{1, 2, 3, 4, 5}, @@ -326,8 +289,8 @@ func Test_FilterToken_Apply(t *testing.T) { }, { token: &filterToken{ - expression: "next is not index", - engine: &testEngine{compiledExpression: &testCompiledExpression{response: true}}, + expression: "next is not index", + compiledExpression: &testCompiledExpression{response: true}, }, input: input{ current: []interface{}{ @@ -344,10 +307,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: [1]string{"one"}, - }, + compiledExpression: &testCompiledExpression{ + response: [1]string{"one"}, }, }, input: input{ @@ -361,10 +322,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "slice", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: []string{"one"}, - }, + compiledExpression: &testCompiledExpression{ + response: []string{"one"}, }, }, input: input{ @@ -378,10 +337,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "empty array", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: [0]string{}, - }, + compiledExpression: &testCompiledExpression{ + response: [0]string{}, }, }, input: input{ @@ -395,10 +352,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: map[string]interface{}{"key": "value"}, - }, + compiledExpression: &testCompiledExpression{ + response: map[string]interface{}{"key": "value"}, }, }, input: input{ @@ -412,10 +367,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "empty map", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: map[string]interface{}{}, - }, + compiledExpression: &testCompiledExpression{ + response: map[string]interface{}{}, }, }, input: input{ @@ -429,10 +382,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "nil pointer", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: getNilPointer(), - }, + compiledExpression: &testCompiledExpression{ + response: getNilPointer(), }, }, input: input{ @@ -446,10 +397,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "single quotes empty", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: "''", - }, + compiledExpression: &testCompiledExpression{ + response: "''", }, }, input: input{ @@ -463,10 +412,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "single quotes not empty", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: "' '", - }, + compiledExpression: &testCompiledExpression{ + response: "' '", }, }, input: input{ @@ -480,10 +427,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "double quotes empty", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: `""`, - }, + compiledExpression: &testCompiledExpression{ + response: `""`, }, }, input: input{ @@ -497,10 +442,8 @@ func Test_FilterToken_Apply(t *testing.T) { { token: &filterToken{ expression: "double quotes not empty", - engine: &testEngine{ - compiledExpression: &testCompiledExpression{ - response: `" "`, - }, + compiledExpression: &testCompiledExpression{ + response: `" "`, }, }, input: input{ diff --git a/token/range_test.go b/token/range_test.go index 458266d..acf3258 100644 --- a/token/range_test.go +++ b/token/range_test.go @@ -385,7 +385,10 @@ func Test_RangeToken_Apply(t *testing.T) { }, { token: &rangeToken{ - from: &expressionToken{expression: "", engine: &testEngine{response: ""}}, + from: &expressionToken{ + expression: "", + compiledExpression: &testCompiledExpression{response: ""}, + }, }, input: input{ current: []interface{}{ @@ -400,7 +403,10 @@ func Test_RangeToken_Apply(t *testing.T) { }, { token: &rangeToken{ - from: &expressionToken{expression: "'key'", engine: &testEngine{response: "key"}}, + from: &expressionToken{ + expression: "'key'", + compiledExpression: &testCompiledExpression{response: "key"}, + }, }, input: input{ current: []interface{}{ @@ -430,7 +436,10 @@ func Test_RangeToken_Apply(t *testing.T) { }, { token: &rangeToken{ - from: &expressionToken{expression: "@.length-1", engine: &testEngine{response: 2}}, + from: &expressionToken{ + expression: "@.length-1", + compiledExpression: &testCompiledExpression{response: 2}, + }, }, input: input{ current: []interface{}{ @@ -448,7 +457,10 @@ func Test_RangeToken_Apply(t *testing.T) { { token: &rangeToken{ from: 0, - to: &expressionToken{expression: "", engine: &testEngine{response: ""}}, + to: &expressionToken{ + expression: "", + compiledExpression: &testCompiledExpression{response: ""}, + }, }, input: input{ current: []interface{}{ @@ -464,7 +476,10 @@ func Test_RangeToken_Apply(t *testing.T) { { token: &rangeToken{ from: 0, - to: &expressionToken{expression: "'key'", engine: &testEngine{response: "key"}}, + to: &expressionToken{ + expression: "'key'", + compiledExpression: &testCompiledExpression{response: "key"}, + }, }, input: input{ current: []interface{}{ @@ -496,7 +511,10 @@ func Test_RangeToken_Apply(t *testing.T) { { token: &rangeToken{ from: 0, - to: &expressionToken{expression: "@.length-2", engine: &testEngine{response: 1}}, + to: &expressionToken{ + expression: "@.length-2", + compiledExpression: &testCompiledExpression{response: 1}, + }, }, input: input{ current: []interface{}{ @@ -514,7 +532,10 @@ func Test_RangeToken_Apply(t *testing.T) { { token: &rangeToken{ from: 0, - step: &expressionToken{expression: "", engine: &testEngine{response: ""}}, + step: &expressionToken{ + expression: "", + compiledExpression: &testCompiledExpression{response: ""}, + }, }, input: input{ current: []interface{}{ @@ -530,7 +551,10 @@ func Test_RangeToken_Apply(t *testing.T) { { token: &rangeToken{ from: 0, - step: &expressionToken{expression: "'key'", engine: &testEngine{response: "key"}}, + step: &expressionToken{ + expression: "'key'", + compiledExpression: &testCompiledExpression{response: "key"}, + }, }, input: input{ current: []interface{}{ @@ -562,7 +586,10 @@ func Test_RangeToken_Apply(t *testing.T) { { token: &rangeToken{ from: 0, - step: &expressionToken{expression: "@.length-1", engine: &testEngine{response: 2}}, + step: &expressionToken{ + expression: "@.length-1", + compiledExpression: &testCompiledExpression{response: 2}, + }, }, input: input{ current: []interface{}{ @@ -695,7 +722,10 @@ func Test_RangeToken_Apply(t *testing.T) { }, { token: &rangeToken{ - from: &expressionToken{expression: "nil", engine: &testEngine{response: nil}}, + from: &expressionToken{ + expression: "nil", + compiledExpression: &testCompiledExpression{response: nil}, + }, }, input: input{ current: []interface{}{ @@ -712,7 +742,10 @@ func Test_RangeToken_Apply(t *testing.T) { { token: &rangeToken{ from: 0, - to: &expressionToken{expression: "nil", engine: &testEngine{response: nil}}, + to: &expressionToken{ + expression: "nil", + compiledExpression: &testCompiledExpression{response: nil}, + }, }, input: input{ current: []interface{}{ @@ -730,7 +763,10 @@ func Test_RangeToken_Apply(t *testing.T) { token: &rangeToken{ from: 0, to: 1, - step: &expressionToken{expression: "nil", engine: &testEngine{response: nil}}, + step: &expressionToken{ + expression: "nil", + compiledExpression: &testCompiledExpression{response: nil}, + }, }, input: input{ current: []interface{}{ diff --git a/token/recursive.go b/token/recursive.go index 346dd9d..d9b1e66 100644 --- a/token/recursive.go +++ b/token/recursive.go @@ -20,81 +20,65 @@ func (token *recursiveToken) Type() string { } func (token *recursiveToken) Apply(root, current interface{}, next []Token) (interface{}, error) { + return token.recursiveApply(root, current, next), nil +} - elements := flatten(current) - - if len(next) > 0 { - nextToken := next[0] - futureTokens := next[1:] +func (token *recursiveToken) recursiveApply(root, current interface{}, next []Token) []interface{} { - results := make([]interface{}, 0) + slice := make([]interface{}, 0) - for _, item := range elements { - result, _ := nextToken.Apply(root, item, futureTokens) - objType, objVal := getTypeAndValue(result) - if objType == nil { - continue - } + objType, objVal := getTypeAndValue(current) + if objType == nil { + return slice + } + if len(next) > 0 { + result, _ := next[0].Apply(root, objVal.Interface(), next[1:]) + objType, objVal := getTypeAndValue(result) + if objType != nil { switch objType.Kind() { case reflect.Array, reflect.Slice: length := objVal.Len() for i := 0; i < length; i++ { - results = append(results, objVal.Index(i).Interface()) + slice = append(slice, objVal.Index(i).Interface()) } break default: - results = append(results, result) + slice = append(slice, objVal.Interface()) break } - } - - return results, nil + } else { + slice = append(slice, objVal.Interface()) } - return elements, nil -} -func flatten(obj interface{}) []interface{} { - slice := make([]interface{}, 0) - - objType, objVal := getTypeAndValue(obj) - if objType == nil { - return slice - } - - slice = append(slice, objVal.Interface()) - - elements := make([]interface{}, 0) switch objType.Kind() { case reflect.Map: keys := objVal.MapKeys() sortMapKeys(keys) for _, kv := range keys { value := objVal.MapIndex(kv).Interface() - elements = append(elements, value) + result := token.recursiveApply(root, value, next) + slice = append(slice, result...) } case reflect.Array, reflect.Slice: length := objVal.Len() for i := 0; i < length; i++ { value := objVal.Index(i).Interface() - elements = append(elements, value) + result := token.recursiveApply(root, value, next) + slice = append(slice, result...) } case reflect.Struct: fields := getStructFields(objVal, true) for _, field := range fields { value := objVal.FieldByName(field.Name).Interface() - elements = append(elements, value) + result := token.recursiveApply(root, value, next) + slice = append(slice, result...) + } default: break } - if len(elements) > 0 { - for _, sObj := range elements { - slice = append(slice, flatten(sObj)...) - } - } - return slice } diff --git a/token/recursive_test.go b/token/recursive_test.go index ec57214..b5df00d 100644 --- a/token/recursive_test.go +++ b/token/recursive_test.go @@ -1,7 +1,6 @@ package token import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -22,152 +21,188 @@ func Test_RecursiveToken_Type(t *testing.T) { assert.Equal(t, "recursive", (&recursiveToken{}).Type()) } -func Test_RecursiveToken_Apply(t *testing.T) { - - tests := []*tokenTest{ - { - token: &recursiveToken{}, - input: input{ - current: []interface{}{nil, "one"}, - }, - expected: expected{ - value: []interface{}{ - []interface{}{nil, "one"}, - "one", - }, +var recursiveTokenTests = []*tokenTest{ + { + token: &recursiveToken{}, + input: input{ + current: []interface{}{nil, "one"}, + }, + expected: expected{ + value: []interface{}{ + []interface{}{nil, "one"}, + "one", + }, + }, + }, + { + token: &recursiveToken{}, + input: input{ + current: map[string]interface{}{ + "key1": "one", + "k2": "two", + "k3": "three", }, }, - { - token: &recursiveToken{}, - input: input{ - current: map[string]interface{}{ + expected: expected{ + value: []interface{}{ + map[string]interface{}{ "key1": "one", "k2": "two", "k3": "three", }, + "one", + "two", + "three", }, - expected: expected{ - value: []interface{}{ - map[string]interface{}{ - "key1": "one", - "k2": "two", - "k3": "three", + }, + }, + { + token: &recursiveToken{}, + input: input{ + root: nil, + current: []interface{}{ + map[string]interface{}{ + "name": "one", + "nested": map[string]interface{}{ + "name": "four", }, - "one", - "two", - "three", }, + map[string]interface{}{ + "name": "two", + }, + map[string]interface{}{ + "name": "three", + }, + }, + tokens: []Token{ + &keyToken{key: "name"}, + }, + }, + expected: expected{ + value: []interface{}{ + "one", + "two", + "three", + "four", }, }, - { - token: &recursiveToken{}, - input: input{ - root: nil, - current: []interface{}{ + }, + { + token: &recursiveToken{}, + input: input{ + root: nil, + current: []interface{}{ + []interface{}{ map[string]interface{}{ "name": "one", - "nested": map[string]interface{}{ - "name": "four", - }, }, map[string]interface{}{ "name": "two", }, + }, + []interface{}{ map[string]interface{}{ "name": "three", }, + map[string]interface{}{ + "name": []interface{}{"four", "five"}, + }, }, - tokens: []Token{ - &keyToken{key: "name"}, - }, - }, - expected: expected{ - value: []interface{}{ - "one", - "two", - "three", - "four", - }, - }, - }, - { - token: &recursiveToken{}, - input: input{ - root: nil, - current: []interface{}{ - []interface{}{ - map[string]interface{}{ - "name": "one", - }, - map[string]interface{}{ - "name": "two", - }, + []interface{}{ + map[string]interface{}{ + "name": "six", }, - []interface{}{ - map[string]interface{}{ - "name": "three", - }, - map[string]interface{}{ - "name": []interface{}{"four", "five"}, + map[string]interface{}{ + "name": []interface{}{ + "seven", + map[string]interface{}{ + "name": "eight", + }, + map[string]interface{}{ + "name": "nine", + }, }, }, }, - tokens: []Token{ - &keyToken{key: "name"}, - }, }, - expected: expected{ - value: []interface{}{ - "one", - "two", - "three", - "four", - "five", + tokens: []Token{ + &keyToken{key: "name"}, + }, + }, + expected: expected{ + value: []interface{}{ + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + map[string]interface{}{ + "name": "eight", + }, + map[string]interface{}{ + "name": "nine", }, }, }, - } - - batchTokenTests(t, tests) -} - -func Test_flatten(t *testing.T) { - tests := []struct { - input interface{} - expected []interface{} - }{ - { - input: nil, - expected: []interface{}{}, - }, - { - input: "string", - expected: []interface{}{"string"}, - }, - { - input: []interface{}{"string", "array"}, - expected: []interface{}{ + }, + { + token: &recursiveToken{}, + input: input{}, + expected: expected{ + value: []interface{}{}, + }, + }, + { + token: &recursiveToken{}, + input: input{ + current: "string", + }, + expected: expected{ + value: []interface{}{"string"}, + }, + }, + { + token: &recursiveToken{}, + input: input{ + current: []interface{}{"string", "array"}, + }, + expected: expected{ + value: []interface{}{ []interface{}{"string", "array"}, "string", "array", }, }, - { - input: []string{"string", "array"}, - expected: []interface{}{ + }, + { + token: &recursiveToken{}, + input: input{ + current: []string{"string", "array"}, + }, + expected: expected{ + value: []interface{}{ []string{"string", "array"}, "string", "array", }, }, - { - input: map[string]interface{}{ + }, + { + token: &recursiveToken{}, + input: input{ + current: map[string]interface{}{ "this": "map", "with": []interface{}{ "array", }, }, - expected: []interface{}{ + }, + expected: expected{ + value: []interface{}{ map[string]interface{}{ "this": "map", "with": []interface{}{ @@ -181,17 +216,25 @@ func Test_flatten(t *testing.T) { "array", }, }, - { - input: sampleStruct{}, - expected: []interface{}{ + }, + { + token: &recursiveToken{}, + input: input{ + current: sampleStruct{}, + }, + expected: expected{ + value: []interface{}{ sampleStruct{}, "", int64(0), "", }, }, - { - input: &sampleStruct{ + }, + { + token: &recursiveToken{}, + input: input{ + current: &sampleStruct{ One: "one", Two: "two", Three: 3, @@ -199,7 +242,9 @@ func Test_flatten(t *testing.T) { Five: "five", Six: "six", }, - expected: []interface{}{ + }, + expected: expected{ + value: []interface{}{ sampleStruct{ One: "one", Two: "two", @@ -215,12 +260,13 @@ func Test_flatten(t *testing.T) { "six", }, }, - } + }, +} + +func Test_RecursiveToken_Apply(t *testing.T) { + batchTokenTests(t, recursiveTokenTests) +} - for idx, test := range tests { - t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { - actual := flatten(test.input) - assert.ElementsMatch(t, test.expected, actual) - }) - } +func Benchmark_RecursiveToken_Apply(b *testing.B) { + batchTokenBenchmarks(b, recursiveTokenTests) } diff --git a/token/script.go b/token/script.go index b856acf..cb8934f 100644 --- a/token/script.go +++ b/token/script.go @@ -8,14 +8,22 @@ import ( "github.com/evilmonkeyinc/jsonpath/script" ) -func newScriptToken(expression string, engine script.Engine, options *option.QueryOptions) *scriptToken { - return &scriptToken{expression: expression, engine: engine, options: options} +func newScriptToken(expression string, engine script.Engine, options *option.QueryOptions) (*scriptToken, error) { + compiledExpression, err := engine.Compile(expression, options) + if err != nil { + return nil, err + } + return &scriptToken{ + expression: expression, + compiledExpression: compiledExpression, + options: options, + }, nil } type scriptToken struct { - expression string - engine script.Engine - options *option.QueryOptions + expression string + compiledExpression script.CompiledExpression + options *option.QueryOptions } func (token *scriptToken) String() string { @@ -31,7 +39,7 @@ func (token *scriptToken) Apply(root, current interface{}, next []Token) (interf return nil, getInvalidExpressionEmptyError() } - value, err := token.engine.Evaluate(root, current, token.expression, token.options) + value, err := token.compiledExpression.Evaluate(root, current) if err != nil { return nil, getInvalidExpressionError(err) } diff --git a/token/script_test.go b/token/script_test.go index 143cb5e..f343170 100644 --- a/token/script_test.go +++ b/token/script_test.go @@ -11,7 +11,16 @@ import ( var _ Token = &scriptToken{} func Test_newScriptToken(t *testing.T) { - assert.IsType(t, &scriptToken{}, newScriptToken("", nil, nil)) + t.Run("success", func(t *testing.T) { + actual, err := newScriptToken("", &testEngine{}, nil) + assert.Nil(t, err) + assert.IsType(t, &scriptToken{}, actual) + }) + t.Run("failed", func(t *testing.T) { + actual, err := newScriptToken("", &testEngine{err: fmt.Errorf("failed")}, nil) + assert.EqualError(t, err, "failed") + assert.Nil(t, actual) + }) } func Test_ScriptToken_String(t *testing.T) { @@ -50,8 +59,8 @@ func Test_ScriptToken_Apply(t *testing.T) { }, { token: &scriptToken{ - expression: "engine error", - engine: &testEngine{err: fmt.Errorf("engine error")}, + expression: "engine error", + compiledExpression: &testCompiledExpression{err: fmt.Errorf("engine error")}, }, input: input{}, expected: expected{ @@ -60,8 +69,8 @@ func Test_ScriptToken_Apply(t *testing.T) { }, { token: &scriptToken{ - expression: "nil response", - engine: &testEngine{response: nil}, + expression: "nil response", + compiledExpression: &testCompiledExpression{response: nil}, }, input: input{}, expected: expected{ @@ -70,8 +79,8 @@ func Test_ScriptToken_Apply(t *testing.T) { }, { token: &scriptToken{ - expression: "bool response", - engine: &testEngine{response: true}, + expression: "bool response", + compiledExpression: &testCompiledExpression{response: true}, }, input: input{}, expected: expected{ @@ -80,8 +89,8 @@ func Test_ScriptToken_Apply(t *testing.T) { }, { token: &scriptToken{ - expression: "string response", - engine: &testEngine{response: "key"}, + expression: "string response", + compiledExpression: &testCompiledExpression{response: "key"}, }, input: input{ current: map[string]interface{}{ @@ -94,8 +103,8 @@ func Test_ScriptToken_Apply(t *testing.T) { }, { token: &scriptToken{ - expression: "int response", - engine: &testEngine{response: 1}, + expression: "int response", + compiledExpression: &testCompiledExpression{response: 1}, }, input: input{ current: []string{"one", "two", "three"}, diff --git a/token/token.go b/token/token.go index 7f41bfb..ca0b16b 100644 --- a/token/token.go +++ b/token/token.go @@ -217,7 +217,7 @@ func Parse(tokenString string, engine script.Engine, options *option.QueryOption if !strings.HasPrefix(subscript, "?(") || !strings.HasSuffix(subscript, ")") { return nil, getInvalidTokenFormatError(tokenString) } - return newFilterToken(strings.TrimSpace(subscript[2:len(subscript)-1]), engine, options), nil + return newFilterToken(strings.TrimSpace(subscript[2:len(subscript)-1]), engine, options) } // from this point we have the chance of things being nested or wrapped @@ -368,7 +368,7 @@ func Parse(tokenString string, engine script.Engine, options *option.QueryOption if isKey(strArg) { return newKeyToken(strArg[1 : len(strArg)-1]), nil } else if isScript(strArg) { - return newScriptToken(strArg[1:len(strArg)-1], engine, options), nil + return newScriptToken(strArg[1:len(strArg)-1], engine, options) } } else if intArg, ok := isInteger(arg); ok { return newIndexToken(intArg, options), nil @@ -417,7 +417,11 @@ func Parse(tokenString string, engine script.Engine, options *option.QueryOption for idx, arg := range args { if strArg, ok := arg.(string); ok { if isScript(strArg) { - arg = newExpressionToken(strArg[1:len(strArg)-1], engine, options) + var err error + arg, err = newExpressionToken(strArg[1:len(strArg)-1], engine, options) + if err != nil { + return nil, getInvalidExpressionError(err) + } args[idx] = arg continue } else if isKey(strArg) { @@ -451,19 +455,31 @@ func Parse(tokenString string, engine script.Engine, options *option.QueryOption if !isScript(strFrom) { return nil, getInvalidExpressionFormatError(strFrom) } - from = newExpressionToken(strFrom[1:len(strFrom)-1], engine, options) + var err error + from, err = newExpressionToken(strFrom[1:len(strFrom)-1], engine, options) + if err != nil { + return nil, getInvalidExpressionError(err) + } } if strTo, ok := to.(string); ok { if !isScript(strTo) { return nil, getInvalidExpressionFormatError(strTo) } - to = newExpressionToken(strTo[1:len(strTo)-1], engine, options) + var err error + to, err = newExpressionToken(strTo[1:len(strTo)-1], engine, options) + if err != nil { + return nil, getInvalidExpressionError(err) + } } if strStep, ok := step.(string); ok { if !isScript(strStep) { return nil, getInvalidExpressionFormatError(strStep) } - step = newExpressionToken(strStep[1:len(strStep)-1], engine, options) + var err error + step, err = newExpressionToken(strStep[1:len(strStep)-1], engine, options) + if err != nil { + return nil, getInvalidExpressionError(err) + } } return newRangeToken(from, to, step, options), nil diff --git a/token/token_test.go b/token/token_test.go index 0d9b623..beac94c 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/evilmonkeyinc/jsonpath/script" "github.com/stretchr/testify/assert" ) @@ -11,6 +12,7 @@ func Test_Parse(t *testing.T) { type input struct { selector string + engine script.Engine } type expected struct { @@ -129,7 +131,8 @@ func Test_Parse(t *testing.T) { input: input{selector: "[?(@.isbn)]"}, expected: expected{ token: &filterToken{ - expression: "@.isbn", + expression: "@.isbn", + compiledExpression: &testCompiledExpression{}, }, }, }, @@ -145,7 +148,8 @@ func Test_Parse(t *testing.T) { input: input{selector: "[(@.length-1)]"}, expected: expected{ token: &scriptToken{ - expression: "@.length-1", + expression: "@.length-1", + compiledExpression: &testCompiledExpression{}, }, }, }, @@ -160,12 +164,15 @@ func Test_Parse(t *testing.T) { }, }, { - input: input{selector: "[1:(@.length-1)]"}, + input: input{ + selector: "[1:(@.length-1)]", + }, expected: expected{ token: &rangeToken{ from: int64(1), to: &expressionToken{ - expression: "@.length-1", + expression: "@.length-1", + compiledExpression: &testCompiledExpression{}, }, step: nil, }, @@ -219,7 +226,8 @@ func Test_Parse(t *testing.T) { arguments: []interface{}{ int64(0), &expressionToken{ - expression: "@.length-1", + expression: "@.length-1", + compiledExpression: &testCompiledExpression{}, }, }, }, @@ -234,7 +242,8 @@ func Test_Parse(t *testing.T) { "one", int64(2), &expressionToken{ - expression: "1+2", + expression: "1+2", + compiledExpression: &testCompiledExpression{}, }, }, }, @@ -246,10 +255,12 @@ func Test_Parse(t *testing.T) { token: &unionToken{ arguments: []interface{}{ &expressionToken{ - expression: "@.length-2", + expression: "@.length-2", + compiledExpression: &testCompiledExpression{}, }, &expressionToken{ - expression: "@.length-1", + expression: "@.length-1", + compiledExpression: &testCompiledExpression{}, }, int64(1), }, @@ -269,7 +280,8 @@ func Test_Parse(t *testing.T) { expected: expected{ token: &rangeToken{ to: &expressionToken{ - expression: "@.length-1", + expression: "@.length-1", + compiledExpression: &testCompiledExpression{}, }, }, }, @@ -286,7 +298,8 @@ func Test_Parse(t *testing.T) { token: &rangeToken{ from: int64(0), to: &expressionToken{ - expression: "@.length-1", + expression: "@.length-1", + compiledExpression: &testCompiledExpression{}, }, step: int64(2), }, @@ -297,7 +310,8 @@ func Test_Parse(t *testing.T) { expected: expected{ token: &rangeToken{ from: &expressionToken{ - expression: "@.length-1", + expression: "@.length-1", + compiledExpression: &testCompiledExpression{}, }, to: int64(1), step: int64(2), @@ -328,7 +342,8 @@ func Test_Parse(t *testing.T) { input: input{selector: "[(1+2*(3+4)+5')]"}, expected: expected{ token: &scriptToken{ - expression: "1+2*(3+4)+5'", + expression: "1+2*(3+4)+5'", + compiledExpression: &testCompiledExpression{}, }, }, }, @@ -427,7 +442,8 @@ func Test_Parse(t *testing.T) { from: int64(0), to: int64(100), step: &expressionToken{ - expression: "1+1", + expression: "1+1", + compiledExpression: &testCompiledExpression{}, }, }, }, @@ -492,11 +508,42 @@ func Test_Parse(t *testing.T) { }, }, }, + { + input: input{ + selector: "[(<):]", + engine: &testEngine{err: fmt.Errorf("fail")}, + }, + expected: expected{ + err: "invalid expression. fail", + }, + }, + { + input: input{ + selector: "[:(<)]", + engine: &testEngine{err: fmt.Errorf("fail")}, + }, + expected: expected{ + err: "invalid expression. fail", + }, + }, + { + input: input{ + selector: "[::(<)]", + engine: &testEngine{err: fmt.Errorf("fail")}, + }, + expected: expected{ + err: "invalid expression. fail", + }, + }, } for idx, test := range tests { t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { - token, err := Parse(test.input.selector, nil, nil) + engine := test.input.engine + if engine == nil { + engine = &testEngine{compiledExpression: &testCompiledExpression{}} + } + token, err := Parse(test.input.selector, engine, nil) if test.expected.err == "" { assert.Nil(t, err, fmt.Sprintf("input '%s' err check failed. expected 'nil' actual '%v'", test.input.selector, err)) @@ -765,6 +812,12 @@ func Test_Tokenize(t *testing.T) { tokens: []string{"@", "isbn"}, }, }, + { + input: "@.'.'", + expected: expected{ + tokens: []string{"@", "'.'"}, + }, + }, } for idx, test := range tests { @@ -843,3 +896,31 @@ func batchTokenTests(t *testing.T, tests []*tokenTest) { }) } } + +func batchTokenBenchmarks(b *testing.B, tests []*tokenTest) { + for idx, test := range tests { + b.Run(fmt.Sprintf("%d", idx), func(b *testing.B) { + actual, err := test.token.Apply(test.input.root, test.input.current, test.input.tokens) + + if test.expected.err == "" { + assert.Nil(b, err) + } else { + assert.EqualError(b, err, test.expected.err) + } + + if test.expected.value != nil { + if actual == nil { + assert.Fail(b, "expected non-nil response") + return + } + if array, ok := test.expected.value.([]interface{}); ok { + assert.ElementsMatch(b, array, actual) + } else { + assert.Equal(b, test.expected.value, actual) + } + } else { + assert.Nil(b, actual) + } + }) + } +} diff --git a/token/union_test.go b/token/union_test.go index 7e58d50..902d7c3 100644 --- a/token/union_test.go +++ b/token/union_test.go @@ -140,7 +140,11 @@ func Test_UnionToken_String(t *testing.T) { expected: "[1,3,4]", }, { - input: &unionToken{arguments: []interface{}{1, &expressionToken{expression: "4%2", engine: &testEngine{response: 0}}, "last"}}, + input: &unionToken{arguments: []interface{}{ + 1, + &expressionToken{expression: "4%2", compiledExpression: &testCompiledExpression{response: 0}}, + "last", + }}, expected: "[1,(4%2),'last']", }, } @@ -167,7 +171,7 @@ func Test_UnionToken_Apply(t *testing.T) { { token: &unionToken{ arguments: []interface{}{ - &expressionToken{expression: "nil", engine: &testEngine{response: nil}}, + &expressionToken{expression: "nil", compiledExpression: &testCompiledExpression{response: nil}}, }, }, input: input{ @@ -213,7 +217,7 @@ func Test_UnionToken_Apply(t *testing.T) { { token: &unionToken{ arguments: []interface{}{ - &expressionToken{expression: "", engine: &testEngine{response: ""}}, + &expressionToken{expression: "", compiledExpression: &testCompiledExpression{response: ""}}, "one", }, }, @@ -227,7 +231,7 @@ func Test_UnionToken_Apply(t *testing.T) { { token: &unionToken{ arguments: []interface{}{ - &expressionToken{expression: "1+1", engine: &testEngine{response: 2}}, + &expressionToken{expression: "1+1", compiledExpression: &testCompiledExpression{response: 2}}, "one", }, }, @@ -241,7 +245,7 @@ func Test_UnionToken_Apply(t *testing.T) { { token: &unionToken{ arguments: []interface{}{ - &expressionToken{expression: "1+1", engine: &testEngine{response: 2}}, + &expressionToken{expression: "1+1", compiledExpression: &testCompiledExpression{response: 2}}, 3, }, }, diff --git a/token/wildcard.go b/token/wildcard.go index f9e1677..193c0a5 100644 --- a/token/wildcard.go +++ b/token/wildcard.go @@ -23,6 +23,25 @@ func (token *wildcardToken) Apply(root, current interface{}, next []Token) (inte elements := make([]interface{}, 0) + var nextToken Token + var futureTokens []Token + + if len(next) > 0 { + nextToken = next[0] + futureTokens = next[1:] + } + + handleNext := func(item interface{}) (interface{}, bool) { + if nextToken == nil { + return item, true + } + result, _ := nextToken.Apply(root, item, futureTokens) + if result == nil { + return nil, false + } + return result, true + } + objType, objVal := getTypeAndValue(current) if objType == nil { return nil, getInvalidTokenTargetNilError( @@ -37,20 +56,26 @@ func (token *wildcardToken) Apply(root, current interface{}, next []Token) (inte sortMapKeys(keys) for _, kv := range keys { value := objVal.MapIndex(kv).Interface() - elements = append(elements, value) + if item, add := handleNext(value); add { + elements = append(elements, item) + } } break case reflect.Array, reflect.Slice: length := objVal.Len() for i := 0; i < length; i++ { value := objVal.Index(i).Interface() - elements = append(elements, value) + if item, add := handleNext(value); add { + elements = append(elements, item) + } } case reflect.Struct: fields := getStructFields(objVal, true) for _, field := range fields { value := objVal.FieldByName(field.Name).Interface() - elements = append(elements, value) + if item, add := handleNext(value); add { + elements = append(elements, item) + } } break default: @@ -61,20 +86,5 @@ func (token *wildcardToken) Apply(root, current interface{}, next []Token) (inte ) } - if len(next) > 0 { - nextToken := next[0] - futureTokens := next[1:] - - results := make([]interface{}, 0) - - for _, item := range elements { - result, _ := nextToken.Apply(root, item, futureTokens) - if result != nil { - results = append(results, result) - } - } - - return results, nil - } return elements, nil } diff --git a/token/wildcard_test.go b/token/wildcard_test.go index b2b88d4..678cd53 100644 --- a/token/wildcard_test.go +++ b/token/wildcard_test.go @@ -21,179 +21,206 @@ func Test_WildcardToken_Type(t *testing.T) { assert.Equal(t, "wildcard", (&wildcardToken{}).Type()) } -func Test_WildcardToken_Apply(t *testing.T) { - - tests := []*tokenTest{ - { - token: &wildcardToken{}, - input: input{ - current: nil, - }, - expected: expected{ - value: nil, - err: "wildcard: invalid token target. expected [array map slice] got [nil]", - }, +var wildcardTests = []*tokenTest{ + { + token: &wildcardToken{}, + input: input{ + current: nil, }, - { - token: &wildcardToken{}, - input: input{ - current: "not array or map", - }, - expected: expected{ - value: nil, - err: "wildcard: invalid token target. expected [array map slice] got [string]", - }, + expected: expected{ + value: nil, + err: "wildcard: invalid token target. expected [array map slice] got [nil]", }, - { - token: &wildcardToken{}, - input: input{ - current: []string{"one", "two", "three"}, - }, - expected: expected{ - value: []interface{}{"one", "two", "three"}, - }, + }, + { + token: &wildcardToken{}, + input: input{ + current: "not array or map", }, - { - token: &wildcardToken{}, - input: input{ - current: []interface{}{"one", "two", "three", 4, 5}, - }, - expected: expected{ - value: []interface{}{"one", "two", "three", 4, 5}, - }, + expected: expected{ + value: nil, + err: "wildcard: invalid token target. expected [array map slice] got [string]", }, - { - token: &wildcardToken{}, - input: input{ - current: map[string]int64{ - "one": 1, - "two": 2, - "three": 3, - }, - }, - expected: expected{ - value: []interface{}{ - int64(1), - int64(2), - int64(3), - }, - }, + }, + { + token: &wildcardToken{}, + input: input{ + current: []string{"one", "two", "three"}, }, - { - token: &wildcardToken{}, - input: input{ - current: map[string]string{ - "one": "1", - "two": "2", - "three": "3", - }, - }, - expected: expected{ - value: []interface{}{"1", "2", "3"}, - }, + expected: expected{ + value: []interface{}{"one", "two", "three"}, }, - { - token: &wildcardToken{}, - input: input{ - current: map[string]interface{}{ - "one": "1", - "two": 2, - "three": "3", - }, + }, + { + token: &wildcardToken{}, + input: input{ + current: []interface{}{"one", "two", nil}, + }, + expected: expected{ + value: []interface{}{"one", "two", nil}, + }, + }, + { + token: &wildcardToken{}, + input: input{ + current: []interface{}{"one", "two", "three", 4, 5}, + }, + expected: expected{ + value: []interface{}{"one", "two", "three", 4, 5}, + }, + }, + { + token: &wildcardToken{}, + input: input{ + current: map[string]int64{ + "one": 1, + "two": 2, + "three": 3, }, - expected: expected{ - value: []interface{}{"1", 2, "3"}, + }, + expected: expected{ + value: []interface{}{ + int64(1), + int64(2), + int64(3), }, }, - { - token: &wildcardToken{}, - input: input{ - current: [3]string{ - "1", - "2", - "3", - }, + }, + { + token: &wildcardToken{}, + input: input{ + current: map[string]string{ + "one": "1", + "two": "2", + "three": "3", }, - expected: expected{ - value: []interface{}{"1", "2", "3"}, + }, + expected: expected{ + value: []interface{}{"1", "2", "3"}, + }, + }, + { + token: &wildcardToken{}, + input: input{ + current: map[string]interface{}{ + "one": "1", + "two": 2, + "three": "3", }, }, - { - token: &wildcardToken{}, - input: input{ - current: []map[string]interface{}{ - {"name": "one"}, - {"name": "two"}, - {"name": "three"}, - }, + expected: expected{ + value: []interface{}{"1", 2, "3"}, + }, + }, + { + token: &wildcardToken{}, + input: input{ + current: [3]string{ + "1", + "2", + "3", }, - expected: expected{ - value: []interface{}{ - map[string]interface{}{"name": "one"}, - map[string]interface{}{"name": "two"}, - map[string]interface{}{"name": "three"}, - }, + }, + expected: expected{ + value: []interface{}{"1", "2", "3"}, + }, + }, + { + token: &wildcardToken{}, + input: input{ + current: []map[string]interface{}{ + {"name": "one"}, + {"name": "two"}, + {"name": "three"}, }, }, - { - token: &wildcardToken{}, - input: input{ - current: []map[string]interface{}{ - {"name": "one"}, - {"name": "two"}, - {"name": "three"}, - }, - tokens: []Token{ - &keyToken{ - key: "name", - }, - }, + expected: expected{ + value: []interface{}{ + map[string]interface{}{"name": "one"}, + map[string]interface{}{"name": "two"}, + map[string]interface{}{"name": "three"}, }, - expected: expected{ - value: []interface{}{ - "one", - "two", - "three", + }, + }, + { + token: &wildcardToken{}, + input: input{ + current: []map[string]interface{}{ + {"name": "one"}, + {"name": "two"}, + {"name": "three"}, + }, + tokens: []Token{ + &keyToken{ + key: "name", }, }, }, - { - token: &wildcardToken{}, - input: input{ - current: sampleStruct{}, + expected: expected{ + value: []interface{}{ + "one", + "two", + "three", }, - expected: expected{ - value: []interface{}{ - "", - int64(0), - "", - }, + }, + }, + { + token: &wildcardToken{}, + input: input{ + current: sampleStruct{}, + }, + expected: expected{ + value: []interface{}{ + "", + int64(0), + "", }, }, - { - token: &wildcardToken{}, - input: input{ - current: sampleStruct{ - One: "one", - Two: "two", - Three: 3, - Four: 4, - Five: "five", - Six: "six", - }, + }, + { + token: &wildcardToken{}, + input: input{ + current: sampleStruct{ + One: "one", + Two: "two", + Three: 3, + Four: 4, + Five: "five", + Six: "six", }, - expected: expected{ - value: []interface{}{ - "one", - "two", - int64(4), - "five", - "six", - }, + }, + expected: expected{ + value: []interface{}{ + "one", + "two", + int64(4), + "five", + "six", }, }, - } + }, - batchTokenTests(t, tests) + { + token: &wildcardToken{}, + input: input{ + current: [4]interface{}{ + "123", + "246", + "369", + nil, + }, + tokens: []Token{&indexToken{index: 0, allowString: true}}, + }, + expected: expected{ + value: []interface{}{"1", "2", "3"}, + }, + }, +} + +func Test_WildcardToken_Apply(t *testing.T) { + batchTokenTests(t, wildcardTests) +} +func Benchmark_WildcardToken_Apply(b *testing.B) { + batchTokenBenchmarks(b, wildcardTests) }