Fix index and slice expressions for query (#405)

* Fix index and slice expressions for query

Support negative step for slice expressions
This commit is contained in:
x-hgg-x
2020-05-14 08:21:51 +02:00
committed by GitHub
parent 44aed552fd
commit a60e466129
8 changed files with 601 additions and 182 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ Or use a query:
q, _ := query.Compile("$..[user,password]") q, _ := query.Compile("$..[user,password]")
results := q.Execute(config) results := q.Execute(config)
for ii, item := range results.Values() { for ii, item := range results.Values() {
fmt.Println("Query result %d: %v", ii, item) fmt.Printf("Query result %d: %v\n", ii, item)
} }
``` ```
+201
View File
@@ -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)
```
+5 -7
View File
@@ -80,25 +80,23 @@
// Slice expressions also allow negative indexes for the start and stop // Slice expressions also allow negative indexes for the start and stop
// arguments. // arguments.
// //
// // select all array elements. // // select all array elements except the last one.
// query.CompileAndExecute("$.foo[0:-1]", tree) // query.CompileAndExecute("$.foo[0:-1]", tree)
// //
// Slice expressions may have an optional stride/step parameter: // Slice expressions may have an optional stride/step parameter:
// //
// // select every other element // // 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: // Slice start and end parameters are also optional:
// //
// // these are all equivalent and select all the values in the array // // these are all equivalent and select all the values in the array
// query.CompileAndExecute("$.foo[:]", tree) // query.CompileAndExecute("$.foo[:]", tree)
// query.CompileAndExecute("$.foo[0:]", tree) // query.CompileAndExecute("$.foo[::]", tree)
// query.CompileAndExecute("$.foo[:-1]", tree)
// query.CompileAndExecute("$.foo[0:-1:]", tree)
// query.CompileAndExecute("$.foo[::1]", 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[0::1]", tree)
// query.CompileAndExecute("$.foo[:-1:1]", tree)
// query.CompileAndExecute("$.foo[0:-1:1]", tree)
// //
// Query Filters // Query Filters
// //
+103 -25
View File
@@ -2,6 +2,7 @@ package query
import ( import (
"fmt" "fmt"
"reflect"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
) )
@@ -71,51 +72,128 @@ func newMatchIndexFn(idx int) *matchIndexFn {
} }
func (f *matchIndexFn) call(node interface{}, ctx *queryContext) { func (f *matchIndexFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok { v := reflect.ValueOf(node)
if f.Idx < len(arr) && f.Idx >= 0 { 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 { if treesArray, ok := node.([]*toml.Tree); ok {
if len(treesArray) > 0 {
ctx.lastPosition = treesArray[0].Position() ctx.lastPosition = treesArray[0].Position()
} }
} next.call(value, ctx)
f.next.call(arr[f.Idx], ctx)
}
}
} }
// filter by slicing // filter by slicing
type matchSliceFn struct { type matchSliceFn struct {
matchBase matchBase
Start, End, Step int Start, End, Step *int
} }
func newMatchSliceFn(start, end, step int) *matchSliceFn { func newMatchSliceFn() *matchSliceFn {
return &matchSliceFn{Start: start, End: end, Step: step} 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) { func (f *matchSliceFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok { v := reflect.ValueOf(node)
// adjust indexes for negative values, reverse ordering if v.Kind() == reflect.Slice {
realStart, realEnd := f.Start, f.End if v.Len() == 0 {
if realStart < 0 { return
realStart = len(arr) + realStart
} }
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 // Initialize start
if f.Start != nil {
start = *f.Start
// Manage negative values
if start < 0 {
start += v.Len()
} }
// loop and gather // Manage out of range values
for idx := realStart; idx < realEnd; idx += f.Step { start = max(start, 0)
if treesArray, ok := node.([]*toml.Tree); ok { start = min(start, v.Len()-1)
if len(treesArray) > 0 { } else if step > 0 {
ctx.lastPosition = treesArray[0].Position() 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 // match anything
+21 -10
View File
@@ -2,8 +2,10 @@ package query
import ( import (
"fmt" "fmt"
"github.com/pelletier/go-toml" "strconv"
"testing" "testing"
"github.com/pelletier/go-toml"
) )
// dump path tree to a string // dump path tree to a string
@@ -19,8 +21,17 @@ func pathString(root pathFn) string {
result += fmt.Sprintf("{%d}", fn.Idx) result += fmt.Sprintf("{%d}", fn.Idx)
result += pathString(fn.next) result += pathString(fn.next)
case *matchSliceFn: case *matchSliceFn:
result += fmt.Sprintf("{%d:%d:%d}", startString, endString, stepString := "nil", "nil", "nil"
fn.Start, fn.End, fn.Step) 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) result += pathString(fn.next)
case *matchAnyFn: case *matchAnyFn:
result += "{}" result += "{}"
@@ -110,7 +121,7 @@ func TestPathSliceStart(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:]", "$[123:]",
buildPath( buildPath(
newMatchSliceFn(123, maxInt, 1), newMatchSliceFn().setStart(123),
)) ))
} }
@@ -118,7 +129,7 @@ func TestPathSliceStartEnd(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456]", "$[123:456]",
buildPath( buildPath(
newMatchSliceFn(123, 456, 1), newMatchSliceFn().setStart(123).setEnd(456),
)) ))
} }
@@ -126,7 +137,7 @@ func TestPathSliceStartEndColon(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456:]", "$[123:456:]",
buildPath( buildPath(
newMatchSliceFn(123, 456, 1), newMatchSliceFn().setStart(123).setEnd(456),
)) ))
} }
@@ -134,7 +145,7 @@ func TestPathSliceStartStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[123::7]", "$[123::7]",
buildPath( buildPath(
newMatchSliceFn(123, maxInt, 7), newMatchSliceFn().setStart(123).setStep(7),
)) ))
} }
@@ -142,7 +153,7 @@ func TestPathSliceEndStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[:456:7]", "$[:456:7]",
buildPath( buildPath(
newMatchSliceFn(0, 456, 7), newMatchSliceFn().setEnd(456).setStep(7),
)) ))
} }
@@ -150,7 +161,7 @@ func TestPathSliceStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[::7]", "$[::7]",
buildPath( buildPath(
newMatchSliceFn(0, maxInt, 7), newMatchSliceFn().setStep(7),
)) ))
} }
@@ -158,7 +169,7 @@ func TestPathSliceAll(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:456:7]", "$[123:456:7]",
buildPath( buildPath(
newMatchSliceFn(123, 456, 7), newMatchSliceFn().setStart(123).setEnd(456).setStep(7),
)) ))
} }
+11 -8
View File
@@ -203,12 +203,13 @@ loop: // labeled loop for easy breaking
func (p *queryParser) parseSliceExpr() queryParserStateFn { func (p *queryParser) parseSliceExpr() queryParserStateFn {
// init slice to grab all elements // init slice to grab all elements
start, end, step := 0, maxInt, 1 var start, end, step *int = nil, nil, nil
// parse optional start // parse optional start
tok := p.getToken() tok := p.getToken()
if tok.typ == tokenInteger { if tok.typ == tokenInteger {
start = tok.Int() v := tok.Int()
start = &v
tok = p.getToken() tok = p.getToken()
} }
if tok.typ != tokenColon { if tok.typ != tokenColon {
@@ -218,11 +219,12 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn {
// parse optional end // parse optional end
tok = p.getToken() tok = p.getToken()
if tok.typ == tokenInteger { if tok.typ == tokenInteger {
end = tok.Int() v := tok.Int()
end = &v
tok = p.getToken() tok = p.getToken()
} }
if tok.typ == tokenRightBracket { if tok.typ == tokenRightBracket {
p.query.appendPath(newMatchSliceFn(start, end, step)) p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step})
return p.parseMatchExpr return p.parseMatchExpr
} }
if tok.typ != tokenColon { if tok.typ != tokenColon {
@@ -232,17 +234,18 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn {
// parse optional step // parse optional step
tok = p.getToken() tok = p.getToken()
if tok.typ == tokenInteger { if tok.typ == tokenInteger {
step = tok.Int() v := tok.Int()
if step < 0 { if v == 0 {
return p.parseError(tok, "step must be a positive value") return p.parseError(tok, "step cannot be zero")
} }
step = &v
tok = p.getToken() tok = p.getToken()
} }
if tok.typ != tokenRightBracket { if tok.typ != tokenRightBracket {
return p.parseError(tok, "expected ']'") 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 return p.parseMatchExpr
} }
+224 -97
View File
@@ -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{}) { func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) {
tree, err := toml.Load(tomlDoc) tree, err := toml.Load(tomlDoc)
if err != nil { if err != nil {
@@ -128,54 +141,213 @@ func TestQueryKeyString(t *testing.T) {
}) })
} }
func TestQueryIndex(t *testing.T) { func TestQueryKeyUnicodeString(t *testing.T) {
assertQueryPositions(t, assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]", "['f𝟘.o']\na = 42",
"$.foo.a[5]", "$['f𝟘.o']['a']",
[]interface{}{ []interface{}{
queryTestNode{ 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) { func TestQuerySliceRange(t *testing.T) {
assertQueryPositions(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]", "$.foo.a[:5]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{int64(0), toml.Position{2, 1}},
int64(1), toml.Position{2, 1}, queryTestNode{int64(1), toml.Position{2, 1}},
}, queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{ queryTestNode{int64(3), toml.Position{2, 1}},
int64(2), toml.Position{2, 1}, queryTestNode{int64(4), 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},
},
}) })
} }
func TestQuerySliceStep(t *testing.T) { func TestQuerySliceStep(t *testing.T) {
assertQueryPositions(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]", "$.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{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(1), toml.Position{2, 1}, []interface{}{
}, int64(0), int64(1), int64(2), int64(3), int64(4),
queryTestNode{ int64(5), int64(6), int64(7), int64(8), int64(9)},
int64(3), toml.Position{2, 1}, toml.Position{4, 1}},
}, queryTestNode{"ok", toml.Position{6, 1}},
queryTestNode{
int64(5), toml.Position{2, 1},
},
}) })
} }
@@ -265,12 +437,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(2), "b": int64(2),
}, toml.Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{int64(1), toml.Position{2, 1}},
int64(1), toml.Position{2, 1}, queryTestNode{int64(2), toml.Position{3, 1}},
},
queryTestNode{
int64(2), toml.Position{3, 1},
},
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"foo": map[string]interface{}{ "foo": map[string]interface{}{
@@ -285,12 +453,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(4), "b": int64(4),
}, toml.Position{4, 1}, }, toml.Position{4, 1},
}, },
queryTestNode{ queryTestNode{int64(3), toml.Position{5, 1}},
int64(3), toml.Position{5, 1}, queryTestNode{int64(4), toml.Position{6, 1}},
},
queryTestNode{
int64(4), toml.Position{6, 1},
},
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"foo": map[string]interface{}{ "foo": map[string]interface{}{
@@ -305,12 +469,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(6), "b": int64(6),
}, toml.Position{7, 1}, }, toml.Position{7, 1},
}, },
queryTestNode{ queryTestNode{int64(5), toml.Position{8, 1}},
int64(5), toml.Position{8, 1}, queryTestNode{int64(6), toml.Position{9, 1}},
},
queryTestNode{
int64(6), toml.Position{9, 1},
},
}) })
} }
@@ -358,59 +518,30 @@ func TestQueryFilterFn(t *testing.T) {
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(int)]", "$..[?(int)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{int64(8001), toml.Position{13, 1}},
int64(8001), toml.Position{13, 1}, queryTestNode{int64(8001), toml.Position{13, 1}},
}, queryTestNode{int64(8002), toml.Position{13, 1}},
queryTestNode{ queryTestNode{int64(5000), toml.Position{14, 1}},
int64(8001), toml.Position{13, 1},
},
queryTestNode{
int64(8002), toml.Position{13, 1},
},
queryTestNode{
int64(5000), toml.Position{14, 1},
},
}) })
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(string)]", "$..[?(string)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{"TOML Example", toml.Position{3, 1}},
"TOML Example", toml.Position{3, 1}, queryTestNode{"Tom Preston-Werner", toml.Position{6, 1}},
}, queryTestNode{"GitHub", toml.Position{7, 1}},
queryTestNode{ queryTestNode{"GitHub Cofounder & CEO\nLikes tater tots and beer.", toml.Position{8, 1}},
"Tom Preston-Werner", toml.Position{6, 1}, queryTestNode{"192.168.1.1", toml.Position{12, 1}},
}, queryTestNode{"10.0.0.1", toml.Position{21, 3}},
queryTestNode{ queryTestNode{"eqdc10", toml.Position{22, 3}},
"GitHub", toml.Position{7, 1}, queryTestNode{"10.0.0.2", toml.Position{25, 3}},
}, queryTestNode{"eqdc10", toml.Position{26, 3}},
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), assertQueryPositions(t, string(buff),
"$..[?(float)]", "$..[?(float)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{4e-08, toml.Position{30, 1}},
4e-08, toml.Position{30, 1},
},
}) })
tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
@@ -471,16 +602,12 @@ func TestQueryFilterFn(t *testing.T) {
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(time)]", "$..[?(time)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{tv, toml.Position{9, 1}},
tv, toml.Position{9, 1},
},
}) })
assertQueryPositions(t, string(buff), assertQueryPositions(t, string(buff),
"$..[?(bool)]", "$..[?(bool)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{true, toml.Position{15, 1}},
true, toml.Position{15, 1},
},
}) })
} }
+20 -19
View File
@@ -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) { func TestQueryExample(t *testing.T) {
config, _ := toml.Load(` config, _ := toml.Load(`
[[book]] [[book]]
@@ -38,15 +46,17 @@ func TestQueryExample(t *testing.T) {
title = "Neuromancer" title = "Neuromancer"
author = "William Gibson" author = "William Gibson"
`) `)
authors, err := CompileAndExecute("$.book.author", config)
if err != nil { checkQuery(t, config, "$.book.author", "Stephen King", "Ernest Hemmingway", "William Gibson")
t.Fatal("unexpected error:", err)
} checkQuery(t, config, "$.book[0].author", "Stephen King")
names := authors.Values() checkQuery(t, config, "$.book[-1].author", "William Gibson")
if len(names) != 3 { checkQuery(t, config, "$.book[1:].author", "Ernest Hemmingway", "William Gibson")
t.Fatalf("query should return 3 names but returned %d", len(names)) checkQuery(t, config, "$.book[-1:].author", "William Gibson")
} checkQuery(t, config, "$.book[::2].author", "William Gibson", "Stephen King")
assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson") 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) { func TestQueryReadmeExample(t *testing.T) {
@@ -56,16 +66,7 @@ user = "pelletier"
password = "mypassword" password = "mypassword"
`) `)
query, err := Compile("$..[user,password]") checkQuery(t, config, "$..[user,password]", "pelletier", "mypassword")
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")
} }
func TestQueryPathNotPresent(t *testing.T) { func TestQueryPathNotPresent(t *testing.T) {