From a60e466129af3efb3277a46ec01bab50f89a7f4e Mon Sep 17 00:00:00 2001 From: x-hgg-x <39058530+x-hgg-x@users.noreply.github.com> Date: Thu, 14 May 2020 08:21:51 +0200 Subject: [PATCH] Fix index and slice expressions for query (#405) * Fix index and slice expressions for query Support negative step for slice expressions --- README.md | 6 +- query/README.md | 201 +++++++++++++++++++++++++++ query/doc.go | 28 ++-- query/match.go | 134 ++++++++++++++---- query/match_test.go | 31 +++-- query/parser.go | 19 +-- query/parser_test.go | 323 ++++++++++++++++++++++++++++++------------- query/query_test.go | 41 +++--- 8 files changed, 601 insertions(+), 182 deletions(-) create mode 100644 query/README.md diff --git a/README.md b/README.md index 4ef303a..a590b21 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Or use a query: q, _ := query.Compile("$..[user,password]") results := q.Execute(config) for ii, item := range results.Values() { - fmt.Println("Query result %d: %v", ii, item) + fmt.Printf("Query result %d: %v\n", ii, item) } ``` @@ -99,9 +99,9 @@ Go-toml provides two handy command line tools: go install github.com/pelletier/go-toml/cmd/tomljson tomljson --help ``` - + * `jsontoml`: Reads a JSON file and outputs a TOML representation. - + ``` go install github.com/pelletier/go-toml/cmd/jsontoml jsontoml --help diff --git a/query/README.md b/query/README.md new file mode 100644 index 0000000..75b3759 --- /dev/null +++ b/query/README.md @@ -0,0 +1,201 @@ +# Query package + +## Overview + +Package query performs JSONPath-like queries on a TOML document. + +The query path implementation is based loosely on the JSONPath specification: +http://goessner.net/articles/JsonPath/. + +The idea behind a query path is to allow quick access to any element, or set +of elements within TOML document, with a single expression. + +```go +result, err := query.CompileAndExecute("$.foo.bar.baz", tree) +``` + +This is roughly equivalent to: + +```go +next := tree.Get("foo") +if next != nil { + next = next.Get("bar") + if next != nil { + next = next.Get("baz") + } +} +result := next +``` + +err is nil if any parsing exception occurs. + +If no node in the tree matches the query, result will simply contain an empty list of +items. + +As illustrated above, the query path is much more efficient, especially since +the structure of the TOML file can vary. Rather than making assumptions about +a document's structure, a query allows the programmer to make structured +requests into the document, and get zero or more values as a result. + +## Query syntax + +The syntax of a query begins with a root token, followed by any number +sub-expressions: + +``` +$ + Root of the TOML tree. This must always come first. +.name + Selects child of this node, where 'name' is a TOML key + name. +['name'] + Selects child of this node, where 'name' is a string + containing a TOML key name. +[index] + Selcts child array element at 'index'. +..expr + Recursively selects all children, filtered by an a union, + index, or slice expression. +..* + Recursive selection of all nodes at this point in the + tree. +.* + Selects all children of the current node. +[expr,expr] + Union operator - a logical 'or' grouping of two or more + sub-expressions: index, key name, or filter. +[start:end:step] + Slice operator - selects array elements from start to + end-1, at the given step. All three arguments are + optional. +[?(filter)] + Named filter expression - the function 'filter' is + used to filter children at this node. +``` + +## Query Indexes And Slices + +Index expressions perform no bounds checking, and will contribute no +values to the result set if the provided index or index range is invalid. +Negative indexes represent values from the end of the array, counting backwards. + +```go +// select the last index of the array named 'foo' +query.CompileAndExecute("$.foo[-1]", tree) +``` + +Slice expressions are supported, by using ':' to separate a start/end index pair. + +```go +// select up to the first five elements in the array +query.CompileAndExecute("$.foo[0:5]", tree) +``` + +Slice expressions also allow negative indexes for the start and stop +arguments. + +```go +// select all array elements except the last one. +query.CompileAndExecute("$.foo[0:-1]", tree) +``` + +Slice expressions may have an optional stride/step parameter: + +```go +// select every other element +query.CompileAndExecute("$.foo[0::2]", tree) +``` + +Slice start and end parameters are also optional: + +```go +// these are all equivalent and select all the values in the array +query.CompileAndExecute("$.foo[:]", tree) +query.CompileAndExecute("$.foo[::]", tree) +query.CompileAndExecute("$.foo[::1]", tree) +query.CompileAndExecute("$.foo[0:]", tree) +query.CompileAndExecute("$.foo[0::]", tree) +query.CompileAndExecute("$.foo[0::1]", tree) +``` + +## Query Filters + +Query filters are used within a Union [,] or single Filter [] expression. +A filter only allows nodes that qualify through to the next expression, +and/or into the result set. + +```go +// returns children of foo that are permitted by the 'bar' filter. +query.CompileAndExecute("$.foo[?(bar)]", tree) +``` + +There are several filters provided with the library: + +``` +tree + Allows nodes of type Tree. +int + Allows nodes of type int64. +float + Allows nodes of type float64. +string + Allows nodes of type string. +time + Allows nodes of type time.Time. +bool + Allows nodes of type bool. +``` + +## Query Results + +An executed query returns a Result object. This contains the nodes +in the TOML tree that qualify the query expression. Position information +is also available for each value in the set. + +```go +// display the results of a query +results := query.CompileAndExecute("$.foo.bar.baz", tree) +for idx, value := results.Values() { + fmt.Println("%v: %v", results.Positions()[idx], value) +} +``` + +## Compiled Queries + +Queries may be executed directly on a Tree object, or compiled ahead +of time and executed discretely. The former is more convenient, but has the +penalty of having to recompile the query expression each time. + +```go +// basic query +results := query.CompileAndExecute("$.foo.bar.baz", tree) + +// compiled query +query, err := toml.Compile("$.foo.bar.baz") +results := query.Execute(tree) + +// run the compiled query again on a different tree +moreResults := query.Execute(anotherTree) +``` + +## User Defined Query Filters + +Filter expressions may also be user defined by using the SetFilter() +function on the Query object. The function must return true/false, which +signifies if the passed node is kept or discarded, respectively. + +```go +// create a query that references a user-defined filter +query, _ := query.Compile("$[?(bazOnly)]") + +// define the filter, and assign it to the query +query.SetFilter("bazOnly", func(node interface{}) bool{ + if tree, ok := node.(*Tree); ok { + return tree.Has("baz") + } + return false // reject all other node types +}) + +// run the query +query.Execute(tree) +``` diff --git a/query/doc.go b/query/doc.go index ed63c11..d0efb21 100644 --- a/query/doc.go +++ b/query/doc.go @@ -25,7 +25,7 @@ // items. // // As illustrated above, the query path is much more efficient, especially since -// the structure of the TOML file can vary. Rather than making assumptions about +// the structure of the TOML file can vary. Rather than making assumptions about // a document's structure, a query allows the programmer to make structured // requests into the document, and get zero or more values as a result. // @@ -35,7 +35,7 @@ // sub-expressions: // // $ -// Root of the TOML tree. This must always come first. +// Root of the TOML tree. This must always come first. // .name // Selects child of this node, where 'name' is a TOML key // name. @@ -57,7 +57,7 @@ // sub-expressions: index, key name, or filter. // [start:end:step] // Slice operator - selects array elements from start to -// end-1, at the given step. All three arguments are +// end-1, at the given step. All three arguments are // optional. // [?(filter)] // Named filter expression - the function 'filter' is @@ -80,25 +80,23 @@ // Slice expressions also allow negative indexes for the start and stop // arguments. // -// // select all array elements. +// // select all array elements except the last one. // query.CompileAndExecute("$.foo[0:-1]", tree) // // Slice expressions may have an optional stride/step parameter: // // // select every other element -// query.CompileAndExecute("$.foo[0:-1:2]", tree) +// query.CompileAndExecute("$.foo[0::2]", tree) // // Slice start and end parameters are also optional: // // // these are all equivalent and select all the values in the array // query.CompileAndExecute("$.foo[:]", tree) -// query.CompileAndExecute("$.foo[0:]", tree) -// query.CompileAndExecute("$.foo[:-1]", tree) -// query.CompileAndExecute("$.foo[0:-1:]", tree) +// query.CompileAndExecute("$.foo[::]", tree) // query.CompileAndExecute("$.foo[::1]", tree) +// query.CompileAndExecute("$.foo[0:]", tree) +// query.CompileAndExecute("$.foo[0::]", tree) // query.CompileAndExecute("$.foo[0::1]", tree) -// query.CompileAndExecute("$.foo[:-1:1]", tree) -// query.CompileAndExecute("$.foo[0:-1:1]", tree) // // Query Filters // @@ -126,8 +124,8 @@ // // Query Results // -// An executed query returns a Result object. This contains the nodes -// in the TOML tree that qualify the query expression. Position information +// An executed query returns a Result object. This contains the nodes +// in the TOML tree that qualify the query expression. Position information // is also available for each value in the set. // // // display the results of a query @@ -139,7 +137,7 @@ // Compiled Queries // // Queries may be executed directly on a Tree object, or compiled ahead -// of time and executed discretely. The former is more convenient, but has the +// of time and executed discretely. The former is more convenient, but has the // penalty of having to recompile the query expression each time. // // // basic query @@ -155,7 +153,7 @@ // User Defined Query Filters // // Filter expressions may also be user defined by using the SetFilter() -// function on the Query object. The function must return true/false, which +// function on the Query object. The function must return true/false, which // signifies if the passed node is kept or discarded, respectively. // // // create a query that references a user-defined filter @@ -166,7 +164,7 @@ // if tree, ok := node.(*Tree); ok { // return tree.Has("baz") // } -// return false // reject all other node types +// return false // reject all other node types // }) // // // run the query diff --git a/query/match.go b/query/match.go index d2207ef..37b43da 100644 --- a/query/match.go +++ b/query/match.go @@ -2,6 +2,7 @@ package query import ( "fmt" + "reflect" "github.com/pelletier/go-toml" ) @@ -71,53 +72,130 @@ func newMatchIndexFn(idx int) *matchIndexFn { } func (f *matchIndexFn) call(node interface{}, ctx *queryContext) { - if arr, ok := node.([]interface{}); ok { - if f.Idx < len(arr) && f.Idx >= 0 { - if treesArray, ok := node.([]*toml.Tree); ok { - if len(treesArray) > 0 { - ctx.lastPosition = treesArray[0].Position() - } - } - f.next.call(arr[f.Idx], ctx) + v := reflect.ValueOf(node) + if v.Kind() == reflect.Slice { + if v.Len() == 0 { + return + } + + // Manage negative values + idx := f.Idx + if idx < 0 { + idx += v.Len() + } + if 0 <= idx && idx < v.Len() { + callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface()) } } } +func callNextIndexSlice(next pathFn, node interface{}, ctx *queryContext, value interface{}) { + if treesArray, ok := node.([]*toml.Tree); ok { + ctx.lastPosition = treesArray[0].Position() + } + next.call(value, ctx) +} + // filter by slicing type matchSliceFn struct { matchBase - Start, End, Step int + Start, End, Step *int } -func newMatchSliceFn(start, end, step int) *matchSliceFn { - return &matchSliceFn{Start: start, End: end, Step: step} +func newMatchSliceFn() *matchSliceFn { + return &matchSliceFn{} +} + +func (f *matchSliceFn) setStart(start int) *matchSliceFn { + f.Start = &start + return f +} + +func (f *matchSliceFn) setEnd(end int) *matchSliceFn { + f.End = &end + return f +} + +func (f *matchSliceFn) setStep(step int) *matchSliceFn { + f.Step = &step + return f } func (f *matchSliceFn) call(node interface{}, ctx *queryContext) { - if arr, ok := node.([]interface{}); ok { - // adjust indexes for negative values, reverse ordering - realStart, realEnd := f.Start, f.End - if realStart < 0 { - realStart = len(arr) + realStart + v := reflect.ValueOf(node) + if v.Kind() == reflect.Slice { + if v.Len() == 0 { + return } - if realEnd < 0 { - realEnd = len(arr) + realEnd + + var start, end, step int + + // Initialize step + if f.Step != nil { + step = *f.Step + } else { + step = 1 } - if realEnd < realStart { - realEnd, realStart = realStart, realEnd // swap - } - // loop and gather - for idx := realStart; idx < realEnd; idx += f.Step { - if treesArray, ok := node.([]*toml.Tree); ok { - if len(treesArray) > 0 { - ctx.lastPosition = treesArray[0].Position() - } + + // Initialize start + if f.Start != nil { + start = *f.Start + // Manage negative values + if start < 0 { + start += v.Len() + } + // Manage out of range values + start = max(start, 0) + start = min(start, v.Len()-1) + } else if step > 0 { + start = 0 + } else { + start = v.Len() - 1 + } + + // Initialize end + if f.End != nil { + end = *f.End + // Manage negative values + if end < 0 { + end += v.Len() + } + // Manage out of range values + end = max(end, -1) + end = min(end, v.Len()) + } else if step > 0 { + end = v.Len() + } else { + end = -1 + } + + // Loop on values + if step > 0 { + for idx := start; idx < end; idx += step { + callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface()) + } + } else { + for idx := start; idx > end; idx += step { + callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface()) } - f.next.call(arr[idx], ctx) } } } +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + // match anything type matchAnyFn struct { matchBase diff --git a/query/match_test.go b/query/match_test.go index 429b8f6..47472c1 100644 --- a/query/match_test.go +++ b/query/match_test.go @@ -2,8 +2,10 @@ package query import ( "fmt" - "github.com/pelletier/go-toml" + "strconv" "testing" + + "github.com/pelletier/go-toml" ) // dump path tree to a string @@ -19,8 +21,17 @@ func pathString(root pathFn) string { result += fmt.Sprintf("{%d}", fn.Idx) result += pathString(fn.next) case *matchSliceFn: - result += fmt.Sprintf("{%d:%d:%d}", - fn.Start, fn.End, fn.Step) + startString, endString, stepString := "nil", "nil", "nil" + if fn.Start != nil { + startString = strconv.Itoa(*fn.Start) + } + if fn.End != nil { + endString = strconv.Itoa(*fn.End) + } + if fn.Step != nil { + stepString = strconv.Itoa(*fn.Step) + } + result += fmt.Sprintf("{%s:%s:%s}", startString, endString, stepString) result += pathString(fn.next) case *matchAnyFn: result += "{}" @@ -110,7 +121,7 @@ func TestPathSliceStart(t *testing.T) { assertPath(t, "$[123:]", buildPath( - newMatchSliceFn(123, maxInt, 1), + newMatchSliceFn().setStart(123), )) } @@ -118,7 +129,7 @@ func TestPathSliceStartEnd(t *testing.T) { assertPath(t, "$[123:456]", buildPath( - newMatchSliceFn(123, 456, 1), + newMatchSliceFn().setStart(123).setEnd(456), )) } @@ -126,7 +137,7 @@ func TestPathSliceStartEndColon(t *testing.T) { assertPath(t, "$[123:456:]", buildPath( - newMatchSliceFn(123, 456, 1), + newMatchSliceFn().setStart(123).setEnd(456), )) } @@ -134,7 +145,7 @@ func TestPathSliceStartStep(t *testing.T) { assertPath(t, "$[123::7]", buildPath( - newMatchSliceFn(123, maxInt, 7), + newMatchSliceFn().setStart(123).setStep(7), )) } @@ -142,7 +153,7 @@ func TestPathSliceEndStep(t *testing.T) { assertPath(t, "$[:456:7]", buildPath( - newMatchSliceFn(0, 456, 7), + newMatchSliceFn().setEnd(456).setStep(7), )) } @@ -150,7 +161,7 @@ func TestPathSliceStep(t *testing.T) { assertPath(t, "$[::7]", buildPath( - newMatchSliceFn(0, maxInt, 7), + newMatchSliceFn().setStep(7), )) } @@ -158,7 +169,7 @@ func TestPathSliceAll(t *testing.T) { assertPath(t, "$[123:456:7]", buildPath( - newMatchSliceFn(123, 456, 7), + newMatchSliceFn().setStart(123).setEnd(456).setStep(7), )) } diff --git a/query/parser.go b/query/parser.go index 5f69b70..be27d35 100644 --- a/query/parser.go +++ b/query/parser.go @@ -203,12 +203,13 @@ loop: // labeled loop for easy breaking func (p *queryParser) parseSliceExpr() queryParserStateFn { // init slice to grab all elements - start, end, step := 0, maxInt, 1 + var start, end, step *int = nil, nil, nil // parse optional start tok := p.getToken() if tok.typ == tokenInteger { - start = tok.Int() + v := tok.Int() + start = &v tok = p.getToken() } if tok.typ != tokenColon { @@ -218,11 +219,12 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn { // parse optional end tok = p.getToken() if tok.typ == tokenInteger { - end = tok.Int() + v := tok.Int() + end = &v tok = p.getToken() } if tok.typ == tokenRightBracket { - p.query.appendPath(newMatchSliceFn(start, end, step)) + p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step}) return p.parseMatchExpr } if tok.typ != tokenColon { @@ -232,17 +234,18 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn { // parse optional step tok = p.getToken() if tok.typ == tokenInteger { - step = tok.Int() - if step < 0 { - return p.parseError(tok, "step must be a positive value") + v := tok.Int() + if v == 0 { + return p.parseError(tok, "step cannot be zero") } + step = &v tok = p.getToken() } if tok.typ != tokenRightBracket { return p.parseError(tok, "expected ']'") } - p.query.appendPath(newMatchSliceFn(start, end, step)) + p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step}) return p.parseMatchExpr } diff --git a/query/parser_test.go b/query/parser_test.go index af93276..91d3f70 100644 --- a/query/parser_test.go +++ b/query/parser_test.go @@ -78,6 +78,19 @@ func assertValue(t *testing.T, result, ref interface{}) { } } +func assertParseError(t *testing.T, query string, errString string) { + _, err := Compile(query) + if err == nil { + t.Error("error should be non-nil") + return + } + if err.Error() != errString { + t.Errorf("error does not match") + t.Log("test:", err.Error()) + t.Log("ref: ", errString) + } +} + func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) { tree, err := toml.Load(tomlDoc) if err != nil { @@ -128,54 +141,213 @@ func TestQueryKeyString(t *testing.T) { }) } -func TestQueryIndex(t *testing.T) { +func TestQueryKeyUnicodeString(t *testing.T) { assertQueryPositions(t, - "[foo]\na = [1,2,3,4,5,6,7,8,9,0]", - "$.foo.a[5]", + "['f𝟘.o']\na = 42", + "$['f𝟘.o']['a']", []interface{}{ queryTestNode{ - int64(6), toml.Position{2, 1}, + int64(42), toml.Position{2, 1}, }, }) } +func TestQueryIndexError1(t *testing.T) { + assertParseError(t, "$.foo.a[5", "(1, 10): expected ',' or ']', not ''") +} + +func TestQueryIndexError2(t *testing.T) { + assertParseError(t, "$.foo.a[]", "(1, 9): expected union sub expression, not ']', 0") +} + +func TestQueryIndex(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[5]", + []interface{}{ + queryTestNode{int64(5), toml.Position{2, 1}}, + }) +} + +func TestQueryIndexNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[-2]", + []interface{}{ + queryTestNode{int64(8), toml.Position{2, 1}}, + }) +} + +func TestQueryIndexWrong(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[99]", + []interface{}{}) +} + +func TestQueryIndexEmpty(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = []", + "$.foo.a[5]", + []interface{}{}) +} + +func TestQueryIndexTree(t *testing.T) { + assertQueryPositions(t, + "[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\nb = 3", + "$.foo[1].b", + []interface{}{ + queryTestNode{int64(3), toml.Position{4, 1}}, + }) +} + +func TestQuerySliceError1(t *testing.T) { + assertParseError(t, "$.foo.a[3:?]", "(1, 11): expected ']' or ':'") +} + +func TestQuerySliceError2(t *testing.T) { + assertParseError(t, "$.foo.a[:::]", "(1, 11): expected ']'") +} + +func TestQuerySliceError3(t *testing.T) { + assertParseError(t, "$.foo.a[::0]", "(1, 11): step cannot be zero") +} + func TestQuerySliceRange(t *testing.T) { assertQueryPositions(t, - "[foo]\na = [1,2,3,4,5,6,7,8,9,0]", - "$.foo.a[0:5]", + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[:5]", []interface{}{ - queryTestNode{ - int64(1), toml.Position{2, 1}, - }, - queryTestNode{ - int64(2), toml.Position{2, 1}, - }, - queryTestNode{ - int64(3), toml.Position{2, 1}, - }, - queryTestNode{ - int64(4), toml.Position{2, 1}, - }, - queryTestNode{ - int64(5), toml.Position{2, 1}, - }, + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(3), toml.Position{2, 1}}, + queryTestNode{int64(4), toml.Position{2, 1}}, }) } func TestQuerySliceStep(t *testing.T) { assertQueryPositions(t, - "[foo]\na = [1,2,3,4,5,6,7,8,9,0]", + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", "$.foo.a[0:5:2]", + []interface{}{ + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(4), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStartNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[-3:]", + []interface{}{ + queryTestNode{int64(7), toml.Position{2, 1}}, + queryTestNode{int64(8), toml.Position{2, 1}}, + queryTestNode{int64(9), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceEndNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[:-6]", + []interface{}{ + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(3), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStepNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[::-2]", + []interface{}{ + queryTestNode{int64(9), toml.Position{2, 1}}, + queryTestNode{int64(7), toml.Position{2, 1}}, + queryTestNode{int64(5), toml.Position{2, 1}}, + queryTestNode{int64(3), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStartOverRange(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[-99:3]", + []interface{}{ + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStartOverRangeNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[99:7:-1]", + []interface{}{ + queryTestNode{int64(9), toml.Position{2, 1}}, + queryTestNode{int64(8), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceEndOverRange(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[7:99]", + []interface{}{ + queryTestNode{int64(7), toml.Position{2, 1}}, + queryTestNode{int64(8), toml.Position{2, 1}}, + queryTestNode{int64(9), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceEndOverRangeNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[2:-99:-1]", + []interface{}{ + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(0), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceWrongRange(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[5:3]", + []interface{}{}) +} + +func TestQuerySliceWrongRangeNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[3:5:-1]", + []interface{}{}) +} + +func TestQuerySliceEmpty(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = []", + "$.foo.a[5:]", + []interface{}{}) +} + +func TestQuerySliceTree(t *testing.T) { + assertQueryPositions(t, + "[[foo]]\na='nok'\n[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\na='ok'\nb = 3", + "$.foo[1:].a", []interface{}{ queryTestNode{ - int64(1), toml.Position{2, 1}, - }, - queryTestNode{ - int64(3), toml.Position{2, 1}, - }, - queryTestNode{ - int64(5), toml.Position{2, 1}, - }, + []interface{}{ + int64(0), int64(1), int64(2), int64(3), int64(4), + int64(5), int64(6), int64(7), int64(8), int64(9)}, + toml.Position{4, 1}}, + queryTestNode{"ok", toml.Position{6, 1}}, }) } @@ -265,12 +437,8 @@ func TestQueryRecursionAll(t *testing.T) { "b": int64(2), }, toml.Position{1, 1}, }, - queryTestNode{ - int64(1), toml.Position{2, 1}, - }, - queryTestNode{ - int64(2), toml.Position{3, 1}, - }, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{3, 1}}, queryTestNode{ map[string]interface{}{ "foo": map[string]interface{}{ @@ -285,12 +453,8 @@ func TestQueryRecursionAll(t *testing.T) { "b": int64(4), }, toml.Position{4, 1}, }, - queryTestNode{ - int64(3), toml.Position{5, 1}, - }, - queryTestNode{ - int64(4), toml.Position{6, 1}, - }, + queryTestNode{int64(3), toml.Position{5, 1}}, + queryTestNode{int64(4), toml.Position{6, 1}}, queryTestNode{ map[string]interface{}{ "foo": map[string]interface{}{ @@ -305,12 +469,8 @@ func TestQueryRecursionAll(t *testing.T) { "b": int64(6), }, toml.Position{7, 1}, }, - queryTestNode{ - int64(5), toml.Position{8, 1}, - }, - queryTestNode{ - int64(6), toml.Position{9, 1}, - }, + queryTestNode{int64(5), toml.Position{8, 1}}, + queryTestNode{int64(6), toml.Position{9, 1}}, }) } @@ -358,59 +518,30 @@ func TestQueryFilterFn(t *testing.T) { assertQueryPositions(t, string(buff), "$..[?(int)]", []interface{}{ - queryTestNode{ - int64(8001), toml.Position{13, 1}, - }, - queryTestNode{ - int64(8001), toml.Position{13, 1}, - }, - queryTestNode{ - int64(8002), toml.Position{13, 1}, - }, - queryTestNode{ - int64(5000), toml.Position{14, 1}, - }, + queryTestNode{int64(8001), toml.Position{13, 1}}, + queryTestNode{int64(8001), toml.Position{13, 1}}, + queryTestNode{int64(8002), toml.Position{13, 1}}, + queryTestNode{int64(5000), toml.Position{14, 1}}, }) assertQueryPositions(t, string(buff), "$..[?(string)]", []interface{}{ - queryTestNode{ - "TOML Example", toml.Position{3, 1}, - }, - queryTestNode{ - "Tom Preston-Werner", toml.Position{6, 1}, - }, - queryTestNode{ - "GitHub", toml.Position{7, 1}, - }, - queryTestNode{ - "GitHub Cofounder & CEO\nLikes tater tots and beer.", - toml.Position{8, 1}, - }, - queryTestNode{ - "192.168.1.1", toml.Position{12, 1}, - }, - queryTestNode{ - "10.0.0.1", toml.Position{21, 3}, - }, - queryTestNode{ - "eqdc10", toml.Position{22, 3}, - }, - queryTestNode{ - "10.0.0.2", toml.Position{25, 3}, - }, - queryTestNode{ - "eqdc10", toml.Position{26, 3}, - }, + queryTestNode{"TOML Example", toml.Position{3, 1}}, + queryTestNode{"Tom Preston-Werner", toml.Position{6, 1}}, + queryTestNode{"GitHub", toml.Position{7, 1}}, + queryTestNode{"GitHub Cofounder & CEO\nLikes tater tots and beer.", toml.Position{8, 1}}, + queryTestNode{"192.168.1.1", toml.Position{12, 1}}, + queryTestNode{"10.0.0.1", toml.Position{21, 3}}, + queryTestNode{"eqdc10", toml.Position{22, 3}}, + queryTestNode{"10.0.0.2", toml.Position{25, 3}}, + queryTestNode{"eqdc10", toml.Position{26, 3}}, }) assertQueryPositions(t, string(buff), "$..[?(float)]", - []interface{}{ - queryTestNode{ - 4e-08, toml.Position{30, 1}, - }, + []interface{}{ + queryTestNode{4e-08, toml.Position{30, 1}}, }) tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") @@ -471,16 +602,12 @@ func TestQueryFilterFn(t *testing.T) { assertQueryPositions(t, string(buff), "$..[?(time)]", []interface{}{ - queryTestNode{ - tv, toml.Position{9, 1}, - }, + queryTestNode{tv, toml.Position{9, 1}}, }) assertQueryPositions(t, string(buff), "$..[?(bool)]", []interface{}{ - queryTestNode{ - true, toml.Position{15, 1}, - }, + queryTestNode{true, toml.Position{15, 1}}, }) } diff --git a/query/query_test.go b/query/query_test.go index 903a8dc..f63eac1 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -26,6 +26,14 @@ func assertArrayContainsInAnyOrder(t *testing.T, array []interface{}, objects .. } } +func checkQuery(t *testing.T, tree *toml.Tree, query string, objects ...interface{}) { + results, err := CompileAndExecute(query, tree) + if err != nil { + t.Fatal("unexpected error:", err) + } + assertArrayContainsInAnyOrder(t, results.Values(), objects...) +} + func TestQueryExample(t *testing.T) { config, _ := toml.Load(` [[book]] @@ -37,16 +45,18 @@ func TestQueryExample(t *testing.T) { [[book]] title = "Neuromancer" author = "William Gibson" - `) - authors, err := CompileAndExecute("$.book.author", config) - if err != nil { - t.Fatal("unexpected error:", err) - } - names := authors.Values() - if len(names) != 3 { - t.Fatalf("query should return 3 names but returned %d", len(names)) - } - assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson") + `) + + checkQuery(t, config, "$.book.author", "Stephen King", "Ernest Hemmingway", "William Gibson") + + checkQuery(t, config, "$.book[0].author", "Stephen King") + checkQuery(t, config, "$.book[-1].author", "William Gibson") + checkQuery(t, config, "$.book[1:].author", "Ernest Hemmingway", "William Gibson") + checkQuery(t, config, "$.book[-1:].author", "William Gibson") + checkQuery(t, config, "$.book[::2].author", "William Gibson", "Stephen King") + checkQuery(t, config, "$.book[::-1].author", "William Gibson", "Ernest Hemmingway", "Stephen King") + checkQuery(t, config, "$.book[:].author", "Stephen King", "Ernest Hemmingway", "William Gibson") + checkQuery(t, config, "$.book[::].author", "Stephen King", "Ernest Hemmingway", "William Gibson") } func TestQueryReadmeExample(t *testing.T) { @@ -56,16 +66,7 @@ user = "pelletier" password = "mypassword" `) - query, err := Compile("$..[user,password]") - if err != nil { - t.Fatal("unexpected error:", err) - } - results := query.Execute(config) - values := results.Values() - if len(values) != 2 { - t.Fatalf("query should return 2 values but returned %d", len(values)) - } - assertArrayContainsInAnyOrder(t, values, "pelletier", "mypassword") + checkQuery(t, config, "$..[user,password]", "pelletier", "mypassword") } func TestQueryPathNotPresent(t *testing.T) {