Revised after review * Dropped 'script' support * Improved documentation * Made pathFn members private
diff --git a/README.md b/README.md index db8e427..0d62a7b 100644 --- a/README.md +++ b/README.md
@@ -70,6 +70,42 @@ * `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
diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..a231199 --- /dev/null +++ b/doc.go
@@ -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
diff --git a/doc_test.go b/doc_test.go new file mode 100644 index 0000000..c5c27a0 --- /dev/null +++ b/doc_test.go
@@ -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) + } +}
diff --git a/match.go b/match.go index 2b89be9..423a869 100644 --- a/match.go +++ b/match.go
@@ -24,10 +24,10 @@ // 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ } // 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 @@ 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 @@ 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 @@ } } } + f.next.call(tree, ctx) visit(tree) } } @@ -197,7 +198,7 @@ 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 @@ 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? - } -}
diff --git a/match_test.go b/match_test.go index cd0390b..c594eb5 100644 --- a/match_test.go +++ b/match_test.go
@@ -7,7 +7,7 @@ ) // 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 @@ 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 @@ 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 @@ assertPath(t, "$[foo, bar, baz]", buildPath( - &matchUnionFn{[]PathFn{ + &matchUnionFn{[]pathFn{ newMatchKeyFn("foo"), newMatchKeyFn("bar"), newMatchKeyFn("baz"), @@ -197,20 +194,9 @@ 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{}), - }}, - )) -}
diff --git a/position.go b/position.go index fd42a0d..1f26245 100644 --- a/position.go +++ b/position.go
@@ -6,7 +6,13 @@ "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 @@ 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
diff --git a/query.go b/query.go index dfda4e8..597f4ff 100644 --- a/query.go +++ b/query.go
@@ -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 @@ // 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 @@ 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 @@ 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 @@ (*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 + "int": func(node interface{}) bool { + _, ok := node.(int64) + return ok }, -} - -var defaultScriptFunctions = map[string]nodeFn{ - "last": func(node interface{}) interface{} { - if arr, ok := node.([]interface{}); ok { - return len(arr) - 1 - } - return nil + "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 }, }
diff --git a/queryparser.go b/queryparser.go index 8fdb10b..be6954f 100644 --- a/queryparser.go +++ b/queryparser.go
@@ -16,7 +16,7 @@ flow chan token tokensBuffer []token query *Query - union []PathFn + union []pathFn err error } @@ -156,7 +156,7 @@ // 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 @@ 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 @@ 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,
diff --git a/queryparser_test.go b/queryparser_test.go index bd147a0..b2b85ce 100644 --- a/queryparser_test.go +++ b/queryparser_test.go
@@ -2,9 +2,11 @@ import ( "fmt" + "io/ioutil" "sort" "strings" "testing" + "time" ) type queryTestNode struct { @@ -56,6 +58,12 @@ 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 @@ 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 @@ -223,6 +231,28 @@ []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{}{ "a": int64(1), "b": int64(2), @@ -291,8 +321,10 @@ []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{ @@ -303,6 +335,12 @@ }, queryTestNode{ map[string]interface{}{ + "a": int64(1), + "b": int64(2), + }, Position{1, 1}, + }, + queryTestNode{ + map[string]interface{}{ "a": int64(5), "b": int64(6), }, Position{7, 1}, @@ -310,59 +348,136 @@ }) } -func TestQueryScriptFnLast(t *testing.T) { - assertQueryPositions(t, - "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", - "$.foo.a[(last)]", +func TestQueryFilterFn(t *testing.T) { + buff, err := ioutil.ReadFile("example.toml") + if err != nil { + t.Error(err) + return + } + + assertQueryPositions(t, string(buff), + "$..[?(int)]", []interface{}{ queryTestNode{ - int64(9), Position{2, 1}, + int64(8001), Position{13, 1}, + }, + queryTestNode{ + int64(8001), Position{13, 1}, + }, + queryTestNode{ + int64(8002), Position{13, 1}, + }, + queryTestNode{ + int64(5000), Position{14, 1}, }, }) -} -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), + "$..[?(string)]", []interface{}{ queryTestNode{ - int64(1), Position{2, 1}, + "TOML Example", Position{3, 1}, }, queryTestNode{ - int64(3), Position{2, 1}, + "Tom Preston-Werner", Position{6, 1}, }, queryTestNode{ - int64(5), Position{2, 1}, + "GitHub", Position{7, 1}, }, queryTestNode{ - int64(7), Position{2, 1}, + "GitHub Cofounder & CEO\nLikes tater tots and beer.", + Position{8, 1}, }, queryTestNode{ - int64(9), 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}, }, }) -} -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), + "$..[?(float)]", + []interface{}{ + // no float values in document + }) + + tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") + assertQueryPositions(t, string(buff), + "$..[?(tree)]", []interface{}{ queryTestNode{ - int64(0), Position{2, 1}, + map[string]interface{}{ + "name": "Tom Preston-Werner", + "organization": "GitHub", + "bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.", + "dob": tv, + }, Position{5, 1}, }, queryTestNode{ - int64(2), Position{2, 1}, + 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{ - int64(4), Position{2, 1}, + 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{ - int64(6), Position{2, 1}, + map[string]interface{}{ + "ip": "10.0.0.1", + "dc": "eqdc10", + }, Position{20, 3}, }, queryTestNode{ - int64(8), Position{2, 1}, + 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}, }, }) }
diff --git a/toml.go b/toml.go index 2c7c55f..13ef35e 100644 --- a/toml.go +++ b/toml.go
@@ -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) 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