Revised after review
* Dropped 'script' support * Improved documentation * Made pathFn members private
This commit is contained in:
@@ -70,6 +70,42 @@ your application. Using positions works much like values:
|
||||
* `tree.GetPosition("comma.separated.path")` Returns the position of the given
|
||||
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
|
||||
|
||||
The documentation is available at
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,10 @@ func tomlValueCheck(node interface{}, ctx *queryContext) interface{} {
|
||||
|
||||
// base match
|
||||
type matchBase struct {
|
||||
next PathFn
|
||||
next pathFn
|
||||
}
|
||||
|
||||
func (f *matchBase) SetNext(next PathFn) {
|
||||
func (f *matchBase) setNext(next pathFn) {
|
||||
f.next = next
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ func newTerminatingFn() *terminatingFn {
|
||||
return &terminatingFn{}
|
||||
}
|
||||
|
||||
func (f *terminatingFn) SetNext(next PathFn) {
|
||||
func (f *terminatingFn) setNext(next pathFn) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (f *terminatingFn) Call(node interface{}, ctx *queryContext) {
|
||||
func (f *terminatingFn) call(node interface{}, ctx *queryContext) {
|
||||
switch castNode := node.(type) {
|
||||
case *TomlTree:
|
||||
ctx.result.appendResult(node, castNode.position)
|
||||
@@ -66,11 +66,11 @@ func newMatchKeyFn(name string) *matchKeyFn {
|
||||
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 {
|
||||
item := tree.values[f.Name]
|
||||
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}
|
||||
}
|
||||
|
||||
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 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}
|
||||
}
|
||||
|
||||
func (f *matchSliceFn) Call(node interface{}, ctx *queryContext) {
|
||||
func (f *matchSliceFn) call(node interface{}, ctx *queryContext) {
|
||||
if arr, ok := tomlValueCheck(node, ctx).([]interface{}); ok {
|
||||
// adjust indexes for negative values, reverse ordering
|
||||
realStart, realEnd := f.Start, f.End
|
||||
@@ -118,7 +118,7 @@ func (f *matchSliceFn) Call(node interface{}, ctx *queryContext) {
|
||||
}
|
||||
// loop and gather
|
||||
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{}
|
||||
}
|
||||
|
||||
func (f *matchAnyFn) Call(node interface{}, ctx *queryContext) {
|
||||
func (f *matchAnyFn) call(node interface{}, ctx *queryContext) {
|
||||
if tree, ok := node.(*TomlTree); ok {
|
||||
for _, v := range tree.values {
|
||||
f.next.Call(v, ctx)
|
||||
f.next.call(v, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter through union
|
||||
type matchUnionFn struct {
|
||||
Union []PathFn
|
||||
Union []pathFn
|
||||
}
|
||||
|
||||
func (f *matchUnionFn) SetNext(next PathFn) {
|
||||
func (f *matchUnionFn) setNext(next pathFn) {
|
||||
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 {
|
||||
fn.Call(node, ctx)
|
||||
fn.call(node, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,12 +166,12 @@ func newMatchRecursiveFn() *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 {
|
||||
var visit func(tree *TomlTree)
|
||||
visit = func(tree *TomlTree) {
|
||||
for _, v := range tree.values {
|
||||
f.next.Call(v, ctx)
|
||||
f.next.call(v, ctx)
|
||||
switch node := v.(type) {
|
||||
case *TomlTree:
|
||||
visit(node)
|
||||
@@ -182,6 +182,7 @@ func (f *matchRecursiveFn) Call(node interface{}, ctx *queryContext) {
|
||||
}
|
||||
}
|
||||
}
|
||||
f.next.call(tree, ctx)
|
||||
visit(tree)
|
||||
}
|
||||
}
|
||||
@@ -197,7 +198,7 @@ func newMatchFilterFn(name string, pos Position) *matchFilterFn {
|
||||
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]
|
||||
if !ok {
|
||||
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) {
|
||||
case *TomlTree:
|
||||
for _, v := range castNode.values {
|
||||
if fn(v) {
|
||||
f.next.Call(v, ctx)
|
||||
if tv, ok := v.(*tomlValue); ok {
|
||||
if fn(tv.value) {
|
||||
f.next.call(v, ctx)
|
||||
}
|
||||
} else {
|
||||
if fn(v) {
|
||||
f.next.call(v, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, v := range castNode {
|
||||
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
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// dump path tree to a string
|
||||
func pathString(root PathFn) string {
|
||||
func pathString(root pathFn) string {
|
||||
result := fmt.Sprintf("%T:", root)
|
||||
switch fn := root.(type) {
|
||||
case *terminatingFn:
|
||||
@@ -37,9 +37,6 @@ func pathString(root PathFn) string {
|
||||
case *matchFilterFn:
|
||||
result += fmt.Sprintf("{%s}", fn.Name)
|
||||
result += pathString(fn.next)
|
||||
case *matchScriptFn:
|
||||
result += fmt.Sprintf("{%s}", fn.Name)
|
||||
result += pathString(fn.next)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -61,7 +58,7 @@ func assertPath(t *testing.T, query string, ref *Query) {
|
||||
assertPathMatch(t, path, ref)
|
||||
}
|
||||
|
||||
func buildPath(parts ...PathFn) *Query {
|
||||
func buildPath(parts ...pathFn) *Query {
|
||||
query := newQuery()
|
||||
for _, v := range parts {
|
||||
query.appendPath(v)
|
||||
@@ -177,7 +174,7 @@ func TestPathUnion(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[foo, bar, baz]",
|
||||
buildPath(
|
||||
&matchUnionFn{[]PathFn{
|
||||
&matchUnionFn{[]pathFn{
|
||||
newMatchKeyFn("foo"),
|
||||
newMatchKeyFn("bar"),
|
||||
newMatchKeyFn("baz"),
|
||||
@@ -197,20 +194,9 @@ func TestPathFilterExpr(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[?('foo'),?(bar)]",
|
||||
buildPath(
|
||||
&matchUnionFn{[]PathFn{
|
||||
&matchUnionFn{[]pathFn{
|
||||
newMatchFilterFn("foo", Position{}),
|
||||
newMatchFilterFn("bar", Position{}),
|
||||
}},
|
||||
))
|
||||
}
|
||||
|
||||
func TestPathScriptExpr(t *testing.T) {
|
||||
assertPath(t,
|
||||
"$[('foo'),(bar)]",
|
||||
buildPath(
|
||||
&matchUnionFn{[]PathFn{
|
||||
newMatchScriptFn("foo", Position{}),
|
||||
newMatchScriptFn("bar", Position{}),
|
||||
}},
|
||||
))
|
||||
}
|
||||
|
||||
+8
-2
@@ -6,7 +6,13 @@ import (
|
||||
"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 {
|
||||
Line int // line within the document
|
||||
Col int // column within the line
|
||||
@@ -18,7 +24,7 @@ func (p *Position) String() string {
|
||||
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)
|
||||
func (p *Position) Invalid() bool {
|
||||
return p.Line <= 0 || p.Col <= 0
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
package toml
|
||||
|
||||
type nodeFilterFn func(node interface{}) bool
|
||||
type nodeFn func(node interface{}) interface{}
|
||||
import (
|
||||
"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 {
|
||||
items []interface{}
|
||||
positions []Position
|
||||
}
|
||||
|
||||
// appends a value/position pair to the result set
|
||||
func (r *QueryResult) appendResult(node interface{}, pos Position) {
|
||||
r.items = append(r.items, node)
|
||||
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{} {
|
||||
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 {
|
||||
return r.positions
|
||||
}
|
||||
@@ -24,23 +41,22 @@ func (r *QueryResult) Positions() []Position {
|
||||
// runtime context for executing query paths
|
||||
type queryContext struct {
|
||||
result *QueryResult
|
||||
filters *map[string]nodeFilterFn
|
||||
scripts *map[string]nodeFn
|
||||
filters *map[string]NodeFilterFn
|
||||
lastPosition Position
|
||||
}
|
||||
|
||||
// generic path functor interface
|
||||
type PathFn interface {
|
||||
SetNext(next PathFn)
|
||||
Call(node interface{}, ctx *queryContext)
|
||||
type pathFn interface {
|
||||
setNext(next pathFn)
|
||||
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 {
|
||||
root PathFn
|
||||
tail PathFn
|
||||
filters *map[string]nodeFilterFn
|
||||
scripts *map[string]nodeFn
|
||||
root pathFn
|
||||
tail pathFn
|
||||
filters *map[string]NodeFilterFn
|
||||
}
|
||||
|
||||
func newQuery() *Query {
|
||||
@@ -48,25 +64,26 @@ func newQuery() *Query {
|
||||
root: nil,
|
||||
tail: nil,
|
||||
filters: &defaultFilterFunctions,
|
||||
scripts: &defaultScriptFunctions,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) appendPath(next PathFn) {
|
||||
func (q *Query) appendPath(next pathFn) {
|
||||
if q.root == nil {
|
||||
q.root = next
|
||||
} else {
|
||||
q.tail.SetNext(next)
|
||||
q.tail.setNext(next)
|
||||
}
|
||||
q.tail = next
|
||||
next.SetNext(newTerminatingFn()) // init the next functor
|
||||
next.setNext(newTerminatingFn()) // init the next functor
|
||||
}
|
||||
|
||||
// TODO: return (err,query) instead
|
||||
func Compile(path string) (*Query, error) {
|
||||
// Compiles a TOML path expression. The returned Query can be used to match
|
||||
// elements within a TomlTree and its descendants.
|
||||
func CompileQuery(path string) (*Query, error) {
|
||||
return parseQuery(lexQuery(path))
|
||||
}
|
||||
|
||||
// Executes a query against a TomlTree, and returns the result of the query.
|
||||
func (q *Query) Execute(tree *TomlTree) *QueryResult {
|
||||
result := &QueryResult{
|
||||
items: []interface{}{},
|
||||
@@ -78,17 +95,18 @@ func (q *Query) Execute(tree *TomlTree) *QueryResult {
|
||||
ctx := &queryContext{
|
||||
result: result,
|
||||
filters: q.filters,
|
||||
scripts: q.scripts,
|
||||
}
|
||||
q.root.Call(tree, ctx)
|
||||
q.root.call(tree, ctx)
|
||||
}
|
||||
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 {
|
||||
// clone the static table
|
||||
q.filters = &map[string]nodeFilterFn{}
|
||||
q.filters = &map[string]NodeFilterFn{}
|
||||
for k, v := range defaultFilterFunctions {
|
||||
(*q.filters)[k] = v
|
||||
}
|
||||
@@ -96,37 +114,29 @@ func (q *Query) SetFilter(name string, fn nodeFilterFn) {
|
||||
(*q.filters)[name] = fn
|
||||
}
|
||||
|
||||
func (q *Query) SetScript(name string, fn nodeFn) {
|
||||
if q.scripts == &defaultScriptFunctions {
|
||||
// clone the static table
|
||||
q.scripts = &map[string]nodeFn{}
|
||||
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
|
||||
var defaultFilterFunctions = map[string]NodeFilterFn{
|
||||
"tree": func(node interface{}) bool {
|
||||
_, ok := node.(*TomlTree)
|
||||
return ok
|
||||
},
|
||||
"even": func(node interface{}) bool {
|
||||
if ii, ok := node.(int64); ok {
|
||||
return (ii & 1) == 0
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
var defaultScriptFunctions = map[string]nodeFn{
|
||||
"last": func(node interface{}) interface{} {
|
||||
if arr, ok := node.([]interface{}); ok {
|
||||
return len(arr) - 1
|
||||
}
|
||||
return nil
|
||||
"int": func(node interface{}) bool {
|
||||
_, ok := node.(int64)
|
||||
return ok
|
||||
},
|
||||
"float": func(node interface{}) bool {
|
||||
_, ok := node.(float64)
|
||||
return ok
|
||||
},
|
||||
"string": func(node interface{}) bool {
|
||||
_, ok := node.(string)
|
||||
return ok
|
||||
},
|
||||
"time": func(node interface{}) bool {
|
||||
_, ok := node.(time.Time)
|
||||
return ok
|
||||
},
|
||||
"bool": func(node interface{}) bool {
|
||||
_, ok := node.(bool)
|
||||
return ok
|
||||
},
|
||||
}
|
||||
|
||||
+2
-18
@@ -16,7 +16,7 @@ type queryParser struct {
|
||||
flow chan token
|
||||
tokensBuffer []token
|
||||
query *Query
|
||||
union []PathFn
|
||||
union []pathFn
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func (p *queryParser) parseUnionExpr() queryParserStateFn {
|
||||
// this state can be traversed after some sub-expressions
|
||||
// so be careful when setting up state in the parser
|
||||
if p.union == nil {
|
||||
p.union = []PathFn{}
|
||||
p.union = []pathFn{}
|
||||
}
|
||||
|
||||
loop: // labeled loop for easy breaking
|
||||
@@ -185,8 +185,6 @@ loop: // labeled loop for easy breaking
|
||||
p.union = append(p.union, newMatchKeyFn(tok.val))
|
||||
case tokenQuestion:
|
||||
return p.parseFilterExpr
|
||||
case tokenLeftParen:
|
||||
return p.parseScriptExpr
|
||||
default:
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
parser := &queryParser{
|
||||
flow: flow,
|
||||
|
||||
+149
-34
@@ -2,9 +2,11 @@ package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type queryTestNode struct {
|
||||
@@ -56,6 +58,12 @@ func valueString(root interface{}) string {
|
||||
result += fmt.Sprintf("%d", node)
|
||||
case string:
|
||||
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
|
||||
}
|
||||
@@ -76,7 +84,7 @@ func assertQueryPositions(t *testing.T, toml, query string, ref []interface{}) {
|
||||
t.Errorf("Non-nil toml parse error: %v", err)
|
||||
return
|
||||
}
|
||||
q, err := Compile(query)
|
||||
q, err := CompileQuery(query)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
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",
|
||||
"$..*",
|
||||
[]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{
|
||||
map[string]interface{}{
|
||||
"bar": map[string]interface{}{
|
||||
@@ -291,8 +321,10 @@ func TestQueryRecursionUnionSimple(t *testing.T) {
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
"bar": map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
},
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
@@ -301,6 +333,12 @@ func TestQueryRecursionUnionSimple(t *testing.T) {
|
||||
"b": int64(4),
|
||||
}, Position{4, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": int64(2),
|
||||
}, Position{1, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
map[string]interface{}{
|
||||
"a": int64(5),
|
||||
@@ -310,59 +348,136 @@ func TestQueryRecursionUnionSimple(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryScriptFnLast(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
|
||||
"$.foo.a[(last)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(9), Position{2, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
func TestQueryFilterFn(t *testing.T) {
|
||||
buff, err := ioutil.ReadFile("example.toml")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
func TestQueryFilterFnOdd(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
|
||||
"$.foo.a[?(odd)]",
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(int)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(1), Position{2, 1},
|
||||
int64(8001), Position{13, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(3), Position{2, 1},
|
||||
int64(8001), Position{13, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(5), Position{2, 1},
|
||||
int64(8002), Position{13, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(7), Position{2, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(9), Position{2, 1},
|
||||
int64(5000), Position{14, 1},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryFilterFnEven(t *testing.T) {
|
||||
assertQueryPositions(t,
|
||||
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
|
||||
"$.foo.a[?(even)]",
|
||||
assertQueryPositions(t, string(buff),
|
||||
"$..[?(string)]",
|
||||
[]interface{}{
|
||||
queryTestNode{
|
||||
int64(0), Position{2, 1},
|
||||
"TOML Example", Position{3, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(2), Position{2, 1},
|
||||
"Tom Preston-Werner", Position{6, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(4), Position{2, 1},
|
||||
"GitHub", Position{7, 1},
|
||||
},
|
||||
queryTestNode{
|
||||
int64(6), Position{2, 1},
|
||||
"GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||
Position{8, 1},
|
||||
},
|
||||
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,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
|
||||
|
||||
import (
|
||||
@@ -320,7 +316,7 @@ func (t *TomlTree) toToml(indent, keyspace string) string {
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
return q.Execute(t), nil
|
||||
|
||||
Reference in New Issue
Block a user