Revised after review

* Dropped 'script' support
* Improved documentation
* Made pathFn members private
This commit is contained in:
eanderton
2014-09-14 20:26:59 -04:00
parent d9e8f54d1c
commit d9de45b5b5
10 changed files with 589 additions and 183 deletions
+36
View File
@@ -70,6 +70,42 @@ your application. Using positions works much like values:
* `tree.GetPosition("comma.separated.path")` Returns the position of the given * `tree.GetPosition("comma.separated.path")` Returns the position of the given
path in the source. path in the source.
### Support for queries
:
Go-toml also supports a JSON-Path style syntax for querying a document for
collections of elements, and more.
```go
import (
"fmt"
"github.com/pelletier/go-toml"
)
config, err := toml.Load(`
[[book]]
title = "The Stand"
author = "Stephen King"
[[book]]
title = "For Whom the Bell Tolls"
author = "Earnest Hemmingway"
[[book]]
title = "Neuromancer"
author = "William Gibson"
`)
if err != nil {
fmt.Println("Error ", err.Error())
} else {
// find and print all the authors in the document
authors := config.Query("$.book.author")
for _, name := authors.Value() {
fmt.Println(name)
}
}
```
More information about the format of TOML queries can be found on the
[godoc page for go-toml](http://godoc.org/github.com/pelletier/go-toml).
## Documentation ## Documentation
The documentation is available at The documentation is available at
+245
View File
@@ -0,0 +1,245 @@
// Package toml is a TOML markup language parser.
//
// This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/toml-v0.2.0.md
//
// TOML Parsing
//
// TOML data may be parsed in two ways: by file, or by string.
//
// // load TOML data by filename
// tree, err := toml.LoadFile("filename.toml")
//
// // load TOML data stored in a string
// tree, err := toml.Load(stringContainingTomlData)
//
// Either way, the result is a TomlTree object that can be used to navigate the
// structure and data within the original document.
//
//
// Getting data from the TomlTree
//
// After parsing TOML data with Load() or LoadFile(), use the Has() and Get()
// methods on the returned TomlTree, to find your way through the document data.
//
// if tree.Has('foo') {
// fmt.Prinln("foo is: %v", tree.Get('foo'))
// }
//
// Working with Paths
//
// Go-toml has support for basic dot-separated key paths on the Has(), Get(), Set()
// and GetDefault() methods. These are the same kind of key paths used within the
// TOML specification for struct tames.
//
// // looks for a key named 'baz', within struct 'bar', within struct 'foo'
// tree.Has("foo.bar.baz")
//
// // returns the key at this path, if it is there
// tree.Get("foo.bar.baz")
//
// TOML allows keys to contain '.', which can cause this syntax to be problematic
// for some documents. In such cases, use the GetPath(), HasPath(), and SetPath(),
// methods to explicitly define the path. This form is also faster, since
// it avoids having to parse the passed key for '.' delimiters.
//
// // looks for a key named 'baz', within struct 'bar', within struct 'foo'
// tree.HasPath(string{}{"foo","bar","baz"})
//
// // returns the key at this path, if it is there
// tree.GetPath(string{}{"foo","bar","baz"})
//
// Note that this is distinct from the heavyweight query syntax supported by
// TomlTree.Query() and the Query() struct (see below).
//
// Position Support
//
// Each element within the TomlTree is stored with position metadata, which is
// invaluable for providing semantic feedback to a user. This helps in
// situations where the TOML file parses correctly, but contains data that is
// not correct for the application. In such cases, an error message can be
// generated that indicates the problem line and column number in the source
// TOML document.
//
// // load TOML data
// tree, _ := toml.Load("filename.toml")
//
// // get an entry and report an error if it's the wrong type
// element := tree.Get("foo")
// if value, ok := element.(int64); !ok {
// return fmt.Errorf("%v: Element 'foo' must be an integer", tree.GetPosition("foo"))
// }
//
// // report an error if an expected element is missing
// if !tree.Has("bar") {
// return fmt.Errorf("%v: Expected 'bar' element", tree.GetPosition(""))
// }
//
// Query Support
//
// The TOML 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.
//
// result := tree.Query("$.foo.bar.baz") // result is 'nil' if the path is not present
//
// This is equivalent to:
//
// next := tree.Get("foo")
// if next != nil {
// next = next.Get("bar")
// if next != nil {
// next = next.Get("baz")
// }
// }
// result := next
//
// 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.
//
// 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.
//
// // select the last index of the array named 'foo'
// tree.Query("$.foo[-1]")
//
// Slice expressions are supported, by using ':' to separate a start/end index pair.
//
// // select up to the first five elements in the array
// tree.Query("$.foo[0:5]")
//
// Slice expressions also allow negative indexes for the start and stop
// arguments.
//
// // select all array elements.
// tree.Query("$.foo[0:-1]")
//
// Slice expressions may have an optional stride/step parameter:
//
// // select every other element
// tree.Query("$.foo[0:-1:2]")
//
// Slice start and end parameters are also optional:
//
// // these are all equivalent and select all the values in the array
// tree.Query("$.foo[:]")
// tree.Query("$.foo[0:]")
// tree.Query("$.foo[:-1]")
// tree.Query("$.foo[0:-1:]")
// tree.Query("$.foo[::1]")
// tree.Query("$.foo[0::1]")
// tree.Query("$.foo[:-1:1]")
// tree.Query("$.foo[0:-1:1]")
//
// 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.
//
// // returns children of foo that are permitted by the 'bar' filter.
// tree.Query("$.foo[?(bar)]")
//
// There are several filters provided with the library:
//
// tree
// Allows nodes of type TomlTree.
// 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 QueryResult 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
// results := tree.Query("$.foo.bar.baz")
// for idx, value := results.Values() {
// fmt.Println("%v: %v", results.Positions()[idx], value)
// }
//
// Compiled Queries
//
// Queries may be executed directly on a TomlTree object, or compiled ahead
// of time and executed discretely. The former is more convienent, but has the
// penalty of having to recompile the query expression each time.
//
// // basic query
// results := tree.Query("$.foo.bar.baz")
//
// // compiled query
// query := toml.CompileQuery("$.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.
//
// // create a query that references a user-defined filter
// query, _ := CompileQuery("$[?(bazOnly)]")
//
// // define the filter, and assign it to the query
// query.SetFilter("bazOnly", func(node interface{}) bool{
// if tree, ok := node.(*TomlTree); ok {
// return tree.Has("baz")
// }
// return false // reject all other node types
// })
//
// // run the query
// query.Execute(tree)
//
package toml
+51
View File
@@ -0,0 +1,51 @@
// code examples for godoc
package toml
import "fmt"
func ExampleNodeFilterFn_filterExample() {
tree, _ := Load(`
[struct_one]
foo = "foo"
bar = "bar"
[struct_two]
baz = "baz"
gorf = "gorf"
`)
// create a query that references a user-defined-filter
query, _ := CompileQuery("$[?(bazOnly)]")
// define the filter, and assign it to the query
query.SetFilter("bazOnly", func(node interface{}) bool {
if tree, ok := node.(*TomlTree); ok {
return tree.Has("baz")
}
return false // reject all other node types
})
// results contain only the 'struct_two' TomlTree
query.Execute(tree)
}
func ExampleQuery_queryExample() {
config, _ := Load(`
[[book]]
title = "The Stand"
author = "Stephen King"
[[book]]
title = "For Whom the Bell Tolls"
author = "Ernest Hemmingway"
[[book]]
title = "Neuromancer"
author = "William Gibson"
`)
// find and print all the authors in the document
authors, _ := config.Query("$.book.author")
for _, name := range authors.Values() {
fmt.Println(name)
}
}
+30 -53
View File
@@ -24,10 +24,10 @@ func tomlValueCheck(node interface{}, ctx *queryContext) interface{} {
// base match // base match
type matchBase struct { type matchBase struct {
next PathFn next pathFn
} }
func (f *matchBase) SetNext(next PathFn) { func (f *matchBase) setNext(next pathFn) {
f.next = next f.next = next
} }
@@ -40,11 +40,11 @@ func newTerminatingFn() *terminatingFn {
return &terminatingFn{} return &terminatingFn{}
} }
func (f *terminatingFn) SetNext(next PathFn) { func (f *terminatingFn) setNext(next pathFn) {
// do nothing // do nothing
} }
func (f *terminatingFn) Call(node interface{}, ctx *queryContext) { func (f *terminatingFn) call(node interface{}, ctx *queryContext) {
switch castNode := node.(type) { switch castNode := node.(type) {
case *TomlTree: case *TomlTree:
ctx.result.appendResult(node, castNode.position) ctx.result.appendResult(node, castNode.position)
@@ -66,11 +66,11 @@ func newMatchKeyFn(name string) *matchKeyFn {
return &matchKeyFn{Name: name} return &matchKeyFn{Name: name}
} }
func (f *matchKeyFn) Call(node interface{}, ctx *queryContext) { func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*TomlTree); ok { if tree, ok := node.(*TomlTree); ok {
item := tree.values[f.Name] item := tree.values[f.Name]
if item != nil { if item != nil {
f.next.Call(item, ctx) f.next.call(item, ctx)
} }
} }
} }
@@ -85,10 +85,10 @@ func newMatchIndexFn(idx int) *matchIndexFn {
return &matchIndexFn{Idx: idx} return &matchIndexFn{Idx: idx}
} }
func (f *matchIndexFn) Call(node interface{}, ctx *queryContext) { func (f *matchIndexFn) call(node interface{}, ctx *queryContext) {
if arr, ok := tomlValueCheck(node, ctx).([]interface{}); ok { if arr, ok := tomlValueCheck(node, ctx).([]interface{}); ok {
if f.Idx < len(arr) && f.Idx >= 0 { if f.Idx < len(arr) && f.Idx >= 0 {
f.next.Call(arr[f.Idx], ctx) f.next.call(arr[f.Idx], ctx)
} }
} }
} }
@@ -103,7 +103,7 @@ func newMatchSliceFn(start, end, step int) *matchSliceFn {
return &matchSliceFn{Start: start, End: end, Step: step} return &matchSliceFn{Start: start, End: end, Step: step}
} }
func (f *matchSliceFn) Call(node interface{}, ctx *queryContext) { func (f *matchSliceFn) call(node interface{}, ctx *queryContext) {
if arr, ok := tomlValueCheck(node, ctx).([]interface{}); ok { if arr, ok := tomlValueCheck(node, ctx).([]interface{}); ok {
// adjust indexes for negative values, reverse ordering // adjust indexes for negative values, reverse ordering
realStart, realEnd := f.Start, f.End realStart, realEnd := f.Start, f.End
@@ -118,7 +118,7 @@ func (f *matchSliceFn) Call(node interface{}, ctx *queryContext) {
} }
// loop and gather // loop and gather
for idx := realStart; idx < realEnd; idx += f.Step { for idx := realStart; idx < realEnd; idx += f.Step {
f.next.Call(arr[idx], ctx) f.next.call(arr[idx], ctx)
} }
} }
} }
@@ -132,28 +132,28 @@ func newMatchAnyFn() *matchAnyFn {
return &matchAnyFn{} return &matchAnyFn{}
} }
func (f *matchAnyFn) Call(node interface{}, ctx *queryContext) { func (f *matchAnyFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*TomlTree); ok { if tree, ok := node.(*TomlTree); ok {
for _, v := range tree.values { for _, v := range tree.values {
f.next.Call(v, ctx) f.next.call(v, ctx)
} }
} }
} }
// filter through union // filter through union
type matchUnionFn struct { type matchUnionFn struct {
Union []PathFn Union []pathFn
} }
func (f *matchUnionFn) SetNext(next PathFn) { func (f *matchUnionFn) setNext(next pathFn) {
for _, fn := range f.Union { for _, fn := range f.Union {
fn.SetNext(next) fn.setNext(next)
} }
} }
func (f *matchUnionFn) Call(node interface{}, ctx *queryContext) { func (f *matchUnionFn) call(node interface{}, ctx *queryContext) {
for _, fn := range f.Union { for _, fn := range f.Union {
fn.Call(node, ctx) fn.call(node, ctx)
} }
} }
@@ -166,12 +166,12 @@ func newMatchRecursiveFn() *matchRecursiveFn {
return &matchRecursiveFn{} return &matchRecursiveFn{}
} }
func (f *matchRecursiveFn) Call(node interface{}, ctx *queryContext) { func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*TomlTree); ok { if tree, ok := node.(*TomlTree); ok {
var visit func(tree *TomlTree) var visit func(tree *TomlTree)
visit = func(tree *TomlTree) { visit = func(tree *TomlTree) {
for _, v := range tree.values { for _, v := range tree.values {
f.next.Call(v, ctx) f.next.call(v, ctx)
switch node := v.(type) { switch node := v.(type) {
case *TomlTree: case *TomlTree:
visit(node) visit(node)
@@ -182,6 +182,7 @@ func (f *matchRecursiveFn) Call(node interface{}, ctx *queryContext) {
} }
} }
} }
f.next.call(tree, ctx)
visit(tree) visit(tree)
} }
} }
@@ -197,7 +198,7 @@ func newMatchFilterFn(name string, pos Position) *matchFilterFn {
return &matchFilterFn{Name: name, Pos: pos} return &matchFilterFn{Name: name, Pos: pos}
} }
func (f *matchFilterFn) Call(node interface{}, ctx *queryContext) { func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
fn, ok := (*ctx.filters)[f.Name] fn, ok := (*ctx.filters)[f.Name]
if !ok { if !ok {
panic(fmt.Sprintf("%s: query context does not have filter '%s'", panic(fmt.Sprintf("%s: query context does not have filter '%s'",
@@ -206,45 +207,21 @@ func (f *matchFilterFn) Call(node interface{}, ctx *queryContext) {
switch castNode := tomlValueCheck(node, ctx).(type) { switch castNode := tomlValueCheck(node, ctx).(type) {
case *TomlTree: case *TomlTree:
for _, v := range castNode.values { for _, v := range castNode.values {
if fn(v) { if tv, ok := v.(*tomlValue); ok {
f.next.Call(v, ctx) if fn(tv.value) {
f.next.call(v, ctx)
}
} else {
if fn(v) {
f.next.call(v, ctx)
}
} }
} }
case []interface{}: case []interface{}:
for _, v := range castNode { for _, v := range castNode {
if fn(v) { if fn(v) {
f.next.Call(v, ctx) f.next.call(v, ctx)
} }
} }
} }
} }
// match based using result of an externally provided functional filter
type matchScriptFn struct {
matchBase
Pos Position
Name string
}
func newMatchScriptFn(name string, pos Position) *matchScriptFn {
return &matchScriptFn{Name: name, Pos: pos}
}
func (f *matchScriptFn) Call(node interface{}, ctx *queryContext) {
fn, ok := (*ctx.scripts)[f.Name]
if !ok {
panic(fmt.Sprintf("%s: query context does not have script '%s'",
f.Pos, f.Name))
}
switch result := fn(tomlValueCheck(node, ctx)).(type) {
case string:
nextMatch := newMatchKeyFn(result)
nextMatch.SetNext(f.next)
nextMatch.Call(node, ctx)
case int:
nextMatch := newMatchIndexFn(result)
nextMatch.SetNext(f.next)
nextMatch.Call(node, ctx)
//TODO: support other return types?
}
}
+4 -18
View File
@@ -7,7 +7,7 @@ import (
) )
// dump path tree to a string // dump path tree to a string
func pathString(root PathFn) string { func pathString(root pathFn) string {
result := fmt.Sprintf("%T:", root) result := fmt.Sprintf("%T:", root)
switch fn := root.(type) { switch fn := root.(type) {
case *terminatingFn: case *terminatingFn:
@@ -37,9 +37,6 @@ func pathString(root PathFn) string {
case *matchFilterFn: case *matchFilterFn:
result += fmt.Sprintf("{%s}", fn.Name) result += fmt.Sprintf("{%s}", fn.Name)
result += pathString(fn.next) result += pathString(fn.next)
case *matchScriptFn:
result += fmt.Sprintf("{%s}", fn.Name)
result += pathString(fn.next)
} }
return result return result
} }
@@ -61,7 +58,7 @@ func assertPath(t *testing.T, query string, ref *Query) {
assertPathMatch(t, path, ref) assertPathMatch(t, path, ref)
} }
func buildPath(parts ...PathFn) *Query { func buildPath(parts ...pathFn) *Query {
query := newQuery() query := newQuery()
for _, v := range parts { for _, v := range parts {
query.appendPath(v) query.appendPath(v)
@@ -177,7 +174,7 @@ func TestPathUnion(t *testing.T) {
assertPath(t, assertPath(t,
"$[foo, bar, baz]", "$[foo, bar, baz]",
buildPath( buildPath(
&matchUnionFn{[]PathFn{ &matchUnionFn{[]pathFn{
newMatchKeyFn("foo"), newMatchKeyFn("foo"),
newMatchKeyFn("bar"), newMatchKeyFn("bar"),
newMatchKeyFn("baz"), newMatchKeyFn("baz"),
@@ -197,20 +194,9 @@ func TestPathFilterExpr(t *testing.T) {
assertPath(t, assertPath(t,
"$[?('foo'),?(bar)]", "$[?('foo'),?(bar)]",
buildPath( buildPath(
&matchUnionFn{[]PathFn{ &matchUnionFn{[]pathFn{
newMatchFilterFn("foo", Position{}), newMatchFilterFn("foo", Position{}),
newMatchFilterFn("bar", Position{}), newMatchFilterFn("bar", Position{}),
}}, }},
)) ))
} }
func TestPathScriptExpr(t *testing.T) {
assertPath(t,
"$[('foo'),(bar)]",
buildPath(
&matchUnionFn{[]PathFn{
newMatchScriptFn("foo", Position{}),
newMatchScriptFn("bar", Position{}),
}},
))
}
+8 -2
View File
@@ -6,7 +6,13 @@ import (
"fmt" "fmt"
) )
// Position within a TOML document /*
Position of a document element within a TOML document.
Line and Col are both 1-indexed positions for the element's line number and
column number, respectively. Values of zero or less will cause Invalid(),
to return true.
*/
type Position struct { type Position struct {
Line int // line within the document Line int // line within the document
Col int // column within the line Col int // column within the line
@@ -18,7 +24,7 @@ func (p *Position) String() string {
return fmt.Sprintf("(%d, %d)", p.Line, p.Col) return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
} }
// Invalid returns wheter or not the position is valid (i.e. with negative or // Returns whether or not the position is valid (i.e. with negative or
// null values) // null values)
func (p *Position) Invalid() bool { func (p *Position) Invalid() bool {
return p.Line <= 0 || p.Col <= 0 return p.Line <= 0 || p.Col <= 0
+63 -53
View File
@@ -1,22 +1,39 @@
package toml package toml
type nodeFilterFn func(node interface{}) bool import (
type nodeFn func(node interface{}) interface{} "time"
)
// Type of a user-defined filter function, for use with Query.SetFilter().
//
// The return value of the function must indicate if 'node' is to be included
// at this stage of the TOML path. Returning true will include the node, and
// returning false will exclude it.
//
// NOTE: Care should be taken to write script callbacks such that they are safe
// to use from multiple goroutines.
type NodeFilterFn func(node interface{}) bool
// The result of Executing a Query
type QueryResult struct { type QueryResult struct {
items []interface{} items []interface{}
positions []Position positions []Position
} }
// appends a value/position pair to the result set
func (r *QueryResult) appendResult(node interface{}, pos Position) { func (r *QueryResult) appendResult(node interface{}, pos Position) {
r.items = append(r.items, node) r.items = append(r.items, node)
r.positions = append(r.positions, pos) r.positions = append(r.positions, pos)
} }
// Set of values within a QueryResult. The order of values is not guaranteed
// to be in document order, and may be different each time a query is executed.
func (r *QueryResult) Values() []interface{} { func (r *QueryResult) Values() []interface{} {
return r.items return r.items
} }
// Set of positions for values within a QueryResult. Each index in Positions()
// corresponds to the entry in Value() of the same index.
func (r *QueryResult) Positions() []Position { func (r *QueryResult) Positions() []Position {
return r.positions return r.positions
} }
@@ -24,23 +41,22 @@ func (r *QueryResult) Positions() []Position {
// runtime context for executing query paths // runtime context for executing query paths
type queryContext struct { type queryContext struct {
result *QueryResult result *QueryResult
filters *map[string]nodeFilterFn filters *map[string]NodeFilterFn
scripts *map[string]nodeFn
lastPosition Position lastPosition Position
} }
// generic path functor interface // generic path functor interface
type PathFn interface { type pathFn interface {
SetNext(next PathFn) setNext(next pathFn)
Call(node interface{}, ctx *queryContext) call(node interface{}, ctx *queryContext)
} }
// encapsulates a query functor chain and script callbacks // A Query is the representation of a compiled TOML path. A Query is safe
// for concurrent use by multiple goroutines.
type Query struct { type Query struct {
root PathFn root pathFn
tail PathFn tail pathFn
filters *map[string]nodeFilterFn filters *map[string]NodeFilterFn
scripts *map[string]nodeFn
} }
func newQuery() *Query { func newQuery() *Query {
@@ -48,25 +64,26 @@ func newQuery() *Query {
root: nil, root: nil,
tail: nil, tail: nil,
filters: &defaultFilterFunctions, filters: &defaultFilterFunctions,
scripts: &defaultScriptFunctions,
} }
} }
func (q *Query) appendPath(next PathFn) { func (q *Query) appendPath(next pathFn) {
if q.root == nil { if q.root == nil {
q.root = next q.root = next
} else { } else {
q.tail.SetNext(next) q.tail.setNext(next)
} }
q.tail = next q.tail = next
next.SetNext(newTerminatingFn()) // init the next functor next.setNext(newTerminatingFn()) // init the next functor
} }
// TODO: return (err,query) instead // Compiles a TOML path expression. The returned Query can be used to match
func Compile(path string) (*Query, error) { // elements within a TomlTree and its descendants.
func CompileQuery(path string) (*Query, error) {
return parseQuery(lexQuery(path)) return parseQuery(lexQuery(path))
} }
// Executes a query against a TomlTree, and returns the result of the query.
func (q *Query) Execute(tree *TomlTree) *QueryResult { func (q *Query) Execute(tree *TomlTree) *QueryResult {
result := &QueryResult{ result := &QueryResult{
items: []interface{}{}, items: []interface{}{},
@@ -78,17 +95,18 @@ func (q *Query) Execute(tree *TomlTree) *QueryResult {
ctx := &queryContext{ ctx := &queryContext{
result: result, result: result,
filters: q.filters, filters: q.filters,
scripts: q.scripts,
} }
q.root.Call(tree, ctx) q.root.call(tree, ctx)
} }
return result return result
} }
func (q *Query) SetFilter(name string, fn nodeFilterFn) { // Sets a user-defined filter function. These may be used inside "?(..)" query
// expressions to filter TOML document elements within a query.
func (q *Query) SetFilter(name string, fn NodeFilterFn) {
if q.filters == &defaultFilterFunctions { if q.filters == &defaultFilterFunctions {
// clone the static table // clone the static table
q.filters = &map[string]nodeFilterFn{} q.filters = &map[string]NodeFilterFn{}
for k, v := range defaultFilterFunctions { for k, v := range defaultFilterFunctions {
(*q.filters)[k] = v (*q.filters)[k] = v
} }
@@ -96,37 +114,29 @@ func (q *Query) SetFilter(name string, fn nodeFilterFn) {
(*q.filters)[name] = fn (*q.filters)[name] = fn
} }
func (q *Query) SetScript(name string, fn nodeFn) { var defaultFilterFunctions = map[string]NodeFilterFn{
if q.scripts == &defaultScriptFunctions { "tree": func(node interface{}) bool {
// clone the static table _, ok := node.(*TomlTree)
q.scripts = &map[string]nodeFn{} return ok
for k, v := range defaultScriptFunctions {
(*q.scripts)[k] = v
}
}
(*q.scripts)[name] = fn
}
var defaultFilterFunctions = map[string]nodeFilterFn{
"odd": func(node interface{}) bool {
if ii, ok := node.(int64); ok {
return (ii & 1) == 1
}
return false
}, },
"even": func(node interface{}) bool { "int": func(node interface{}) bool {
if ii, ok := node.(int64); ok { _, ok := node.(int64)
return (ii & 1) == 0 return ok
} },
return false "float": func(node interface{}) bool {
}, _, ok := node.(float64)
} return ok
},
var defaultScriptFunctions = map[string]nodeFn{ "string": func(node interface{}) bool {
"last": func(node interface{}) interface{} { _, ok := node.(string)
if arr, ok := node.([]interface{}); ok { return ok
return len(arr) - 1 },
} "time": func(node interface{}) bool {
return nil _, ok := node.(time.Time)
return ok
},
"bool": func(node interface{}) bool {
_, ok := node.(bool)
return ok
}, },
} }
+2 -18
View File
@@ -16,7 +16,7 @@ type queryParser struct {
flow chan token flow chan token
tokensBuffer []token tokensBuffer []token
query *Query query *Query
union []PathFn union []pathFn
err error err error
} }
@@ -156,7 +156,7 @@ func (p *queryParser) parseUnionExpr() queryParserStateFn {
// this state can be traversed after some sub-expressions // this state can be traversed after some sub-expressions
// so be careful when setting up state in the parser // so be careful when setting up state in the parser
if p.union == nil { if p.union == nil {
p.union = []PathFn{} p.union = []pathFn{}
} }
loop: // labeled loop for easy breaking loop: // labeled loop for easy breaking
@@ -185,8 +185,6 @@ loop: // labeled loop for easy breaking
p.union = append(p.union, newMatchKeyFn(tok.val)) p.union = append(p.union, newMatchKeyFn(tok.val))
case tokenQuestion: case tokenQuestion:
return p.parseFilterExpr return p.parseFilterExpr
case tokenLeftParen:
return p.parseScriptExpr
default: default:
return p.parseError(tok, "expected union sub expression, not '%s', %d", tok.val, len(p.union)) return p.parseError(tok, "expected union sub expression, not '%s', %d", tok.val, len(p.union))
} }
@@ -266,20 +264,6 @@ func (p *queryParser) parseFilterExpr() queryParserStateFn {
return p.parseUnionExpr return p.parseUnionExpr
} }
func (p *queryParser) parseScriptExpr() queryParserStateFn {
tok := p.getToken()
if tok.typ != tokenKey && tok.typ != tokenString {
return p.parseError(tok, "expected key or string for script funciton name")
}
name := tok.val
tok = p.getToken()
if tok.typ != tokenRightParen {
return p.parseError(tok, "expected right-parenthesis for script expression")
}
p.union = append(p.union, newMatchScriptFn(name, tok.Position))
return p.parseUnionExpr
}
func parseQuery(flow chan token) (*Query, error) { func parseQuery(flow chan token) (*Query, error) {
parser := &queryParser{ parser := &queryParser{
flow: flow, flow: flow,
+149 -34
View File
@@ -2,9 +2,11 @@ package toml
import ( import (
"fmt" "fmt"
"io/ioutil"
"sort" "sort"
"strings" "strings"
"testing" "testing"
"time"
) )
type queryTestNode struct { type queryTestNode struct {
@@ -56,6 +58,12 @@ func valueString(root interface{}) string {
result += fmt.Sprintf("%d", node) result += fmt.Sprintf("%d", node)
case string: case string:
result += "'" + node + "'" result += "'" + node + "'"
case float64:
result += fmt.Sprintf("%f", node)
case bool:
result += fmt.Sprintf("%t", node)
case time.Time:
result += fmt.Sprintf("'%v'", node)
} }
return result return result
} }
@@ -76,7 +84,7 @@ func assertQueryPositions(t *testing.T, toml, query string, ref []interface{}) {
t.Errorf("Non-nil toml parse error: %v", err) t.Errorf("Non-nil toml parse error: %v", err)
return return
} }
q, err := Compile(query) q, err := CompileQuery(query)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@@ -221,6 +229,28 @@ func TestQueryRecursionAll(t *testing.T) {
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6", "[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
"$..*", "$..*",
[]interface{}{ []interface{}{
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
"bar": map[string]interface{}{
"a": int64(1),
"b": int64(2),
},
},
"baz": map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(3),
"b": int64(4),
},
},
"gorf": map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(5),
"b": int64(6),
},
},
}, Position{1, 1},
},
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"bar": map[string]interface{}{ "bar": map[string]interface{}{
@@ -291,8 +321,10 @@ func TestQueryRecursionUnionSimple(t *testing.T) {
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(1), "bar": map[string]interface{}{
"b": int64(2), "a": int64(1),
"b": int64(2),
},
}, Position{1, 1}, }, Position{1, 1},
}, },
queryTestNode{ queryTestNode{
@@ -301,6 +333,12 @@ func TestQueryRecursionUnionSimple(t *testing.T) {
"b": int64(4), "b": int64(4),
}, Position{4, 1}, }, Position{4, 1},
}, },
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, Position{1, 1},
},
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(5), "a": int64(5),
@@ -310,59 +348,136 @@ func TestQueryRecursionUnionSimple(t *testing.T) {
}) })
} }
func TestQueryScriptFnLast(t *testing.T) { func TestQueryFilterFn(t *testing.T) {
assertQueryPositions(t, buff, err := ioutil.ReadFile("example.toml")
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]", if err != nil {
"$.foo.a[(last)]", t.Error(err)
[]interface{}{ return
queryTestNode{ }
int64(9), Position{2, 1},
},
})
}
func TestQueryFilterFnOdd(t *testing.T) { assertQueryPositions(t, string(buff),
assertQueryPositions(t, "$..[?(int)]",
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[?(odd)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(1), Position{2, 1}, int64(8001), Position{13, 1},
}, },
queryTestNode{ queryTestNode{
int64(3), Position{2, 1}, int64(8001), Position{13, 1},
}, },
queryTestNode{ queryTestNode{
int64(5), Position{2, 1}, int64(8002), Position{13, 1},
}, },
queryTestNode{ queryTestNode{
int64(7), Position{2, 1}, int64(5000), Position{14, 1},
},
queryTestNode{
int64(9), Position{2, 1},
}, },
}) })
}
func TestQueryFilterFnEven(t *testing.T) { assertQueryPositions(t, string(buff),
assertQueryPositions(t, "$..[?(string)]",
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[?(even)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(0), Position{2, 1}, "TOML Example", Position{3, 1},
}, },
queryTestNode{ queryTestNode{
int64(2), Position{2, 1}, "Tom Preston-Werner", Position{6, 1},
}, },
queryTestNode{ queryTestNode{
int64(4), Position{2, 1}, "GitHub", Position{7, 1},
}, },
queryTestNode{ queryTestNode{
int64(6), Position{2, 1}, "GitHub Cofounder & CEO\nLikes tater tots and beer.",
Position{8, 1},
}, },
queryTestNode{ queryTestNode{
int64(8), Position{2, 1}, "192.168.1.1", Position{12, 1},
},
queryTestNode{
"10.0.0.1", Position{21, 3},
},
queryTestNode{
"eqdc10", Position{22, 3},
},
queryTestNode{
"10.0.0.2", Position{25, 3},
},
queryTestNode{
"eqdc10", Position{26, 3},
},
})
assertQueryPositions(t, string(buff),
"$..[?(float)]",
[]interface{}{
// no float values in document
})
tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
assertQueryPositions(t, string(buff),
"$..[?(tree)]",
[]interface{}{
queryTestNode{
map[string]interface{}{
"name": "Tom Preston-Werner",
"organization": "GitHub",
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
"dob": tv,
}, Position{5, 1},
},
queryTestNode{
map[string]interface{}{
"server": "192.168.1.1",
"ports": []interface{}{int64(8001), int64(8001), int64(8002)},
"connection_max": int64(5000),
"enabled": true,
}, Position{11, 1},
},
queryTestNode{
map[string]interface{}{
"alpha": map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
},
"beta": map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
},
}, Position{17, 1},
},
queryTestNode{
map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
}, Position{20, 3},
},
queryTestNode{
map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
}, Position{24, 3},
},
queryTestNode{
map[string]interface{}{
"data": []interface{}{
[]interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)},
},
}, Position{28, 1},
},
})
assertQueryPositions(t, string(buff),
"$..[?(time)]",
[]interface{}{
queryTestNode{
tv, Position{9, 1},
},
})
assertQueryPositions(t, string(buff),
"$..[?(bool)]",
[]interface{}{
queryTestNode{
true, Position{15, 1},
}, },
}) })
} }
+1 -5
View File
@@ -1,7 +1,3 @@
// Package toml is a TOML markup language parser.
//
// This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/toml-v0.2.0.md
package toml package toml
import ( import (
@@ -320,7 +316,7 @@ func (t *TomlTree) toToml(indent, keyspace string) string {
} }
func (t *TomlTree) Query(query string) (*QueryResult, error) { func (t *TomlTree) Query(query string) (*QueryResult, error) {
if q, err := Compile(query); err != nil { if q, err := CompileQuery(query); err != nil {
return nil, err return nil, err
} else { } else {
return q.Execute(t), nil return q.Execute(t), nil