Compare commits

...

18 Commits

Author SHA1 Message Date
Thomas Pelletier 5ccdfb18c7 Write empty tables as well (#169)
Empty tables are allowed by the spec, so they should not be removed:

  [[empty-tables]]
  [[empty-tables]]

is perfectly valid.

Fixes #163
2017-05-30 18:35:27 -07:00
Thomas Pelletier 40ecdac242 Clean up documentation (#168)
Fixes #135
2017-05-30 18:33:25 -07:00
Albert Nigmatzianov 26ae43fdee Use bytes.Buffer for tomlLexer.buffer (#166)
* Use bytes.Buffer for tomlLexer.buffer
* Add BenchmarkLexer

Fix #165

name     old time/op    new time/op    delta
Lexer-4     343µs ± 1%     331µs ± 1%  -3.56%  (p=0.000 n=20+19)

name     old alloc/op   new alloc/op   delta
Lexer-4    55.8kB ± 0%    50.8kB ± 0%  -8.86%  (p=0.000 n=20+20)

name     old allocs/op  new allocs/op  delta
Lexer-4     2.01k ± 0%     1.84k ± 0%  -8.46%  (p=0.000 n=20+20)
2017-05-30 10:27:36 -07:00
Jordan Krage 048765b449 Switch kindToTypeMapping from map to array (#164)
Improve lookup performance by 8x.
2017-05-25 09:02:42 -07:00
Cameron Moore 5c26a6ff6f Fix Tree.ToMap godoc comment (#162)
Fixes #160
2017-05-16 10:14:30 -07:00
Thomas Pelletier 685a1f1cb7 Rename TomlTree to Tree (#159)
Avoid stutter.

Fixes #55
2017-05-10 17:53:23 -07:00
Thomas Pelletier 23f644976a Move query to its own subpackage (#152)
Move all the query system to its own package. The reason is to
avoid it to rely on unexported methods and structures, and move
it out of the main package since this is really not a core
feature. It is still tied to the toml.TomlTree and toml.Position
structures for now.

* Move query mechanism to its own subpackage
* Rename QueryResult to Result to avoid stutter
* Add query.CompileAndExecute

Fixes #116
2017-05-07 17:14:13 -07:00
Thomas Pelletier 64bc956d5e Remove clean.sh (#158) 2017-05-07 16:09:32 -07:00
John K. Luebs 53be957dac Allow unmarshal from any TomlTree (#157)
Fixes #153
2017-05-07 15:55:38 -07:00
Kevin Burke 97253b98df Fix plural mistake in Set* docs (#154) 2017-05-03 21:03:14 -07:00
Kevin Burke 76c552dcd7 Initialize keys array to final length (#155)
Previously we'd create an empty array and need to continuously resize
it as we appended more entries. This way we immediately create the
correct size array, and then add entries to it.
2017-05-03 21:02:36 -07:00
Carolyn Van Slyck fe206efb84 Provide Marshaler interface (#151)
The toml.Marhshaler interface allows marshalling custom objects implementing
the interface. Design based off json.Marshaler.
2017-04-04 18:41:05 -07:00
tro3 e32a2e0474 Reflection-based marshaling / unmarshaling (#149)
Fixes #146
2017-03-29 14:49:41 -07:00
Thomas Pelletier f6e7596e8d Reflect actual slice type in TreeFromMap (#145)
* Reflect actual slice type in TreeFromMap
* Fix writeTo for slices tomlValues

Fixes #143
2017-03-23 11:20:46 +01:00
tro3 25e50242f6 Fix TestMissingFile on Windows (#148)
Closing #147
2017-03-21 15:10:48 +01:00
Albert Nigmatzianov 62e2d802ed Fix #141 (#142)
* Use String() of key if it exists during TreeFromMap
2017-03-21 10:01:44 +01:00
Thomas Pelletier fee7787d3f Rework tree from map (#139)
* Make TreeFromMap reflect to construct tree
* Fix wording of invalid value type in writeTo

Fixes #138, #139, #134 

⚠️ TreeFromMap signature changed to `TreeFromMap(map[string]interface{}) (*TomlTree, error)`
2017-03-14 13:16:40 -07:00
Cameron Moore 3b00596b2e Support lowercase unicode escape sequences (#140) 2017-03-13 20:04:08 -07:00
33 changed files with 2400 additions and 814 deletions
+44 -46
View File
@@ -16,65 +16,63 @@ This library supports TOML version
Go-toml provides the following features for using data parsed from TOML documents: Go-toml provides the following features for using data parsed from TOML documents:
* Load TOML documents from files and string data * Load TOML documents from files and string data
* Easily navigate TOML structure using TomlTree * Easily navigate TOML structure using Tree
* Mashaling and unmarshaling to and from data structures
* Line & column position data for all parsed elements * Line & column position data for all parsed elements
* Query support similar to JSON-Path * [Query support similar to JSON-Path](query/)
* Syntax errors contain line and column numbers * Syntax errors contain line and column numbers
Go-toml is designed to help cover use-cases not covered by reflection-based TOML parsing:
* Semantic evaluation of parsed TOML
* Informing a user of mistakes in the source document, after it has been parsed
* Programatic handling of default values on a case-by-case basis
* Using a TOML document as a flexible data-store
## Import ## Import
import "github.com/pelletier/go-toml" ```go
import "github.com/pelletier/go-toml"
## Usage
### Example
Say you have a TOML file that looks like this:
```toml
[postgres]
user = "pelletier"
password = "mypassword"
``` ```
Read the username and password like this: ## Usage example
Read a TOML document:
```go ```go
import ( config, _ := toml.LoadString(`
"fmt" [postgres]
"github.com/pelletier/go-toml" user = "pelletier"
) password = "mypassword"`)
// retrieve data directly
user := config.Get("postgres.user").(string)
config, err := toml.LoadFile("config.toml") // or using an intermediate object
if err != nil { postgresConfig := config.Get("postgres").(*toml.Tree)
fmt.Println("Error ", err.Error()) password = postgresConfig.Get("password").(string)
} else { ```
// retrieve data directly
user := config.Get("postgres.user").(string)
password := config.Get("postgres.password").(string)
// or using an intermediate object Or use Unmarshal:
configTree := config.Get("postgres").(*toml.TomlTree)
user = configTree.Get("user").(string)
password = configTree.Get("password").(string)
fmt.Println("User is ", user, ". Password is ", password)
// show where elements are in the file ```go
fmt.Println("User position: %v", configTree.GetPosition("user")) type Postgres struct {
fmt.Println("Password position: %v", configTree.GetPosition("password")) User string
Password string
}
type Config struct {
Postgres Postgres
}
// use a query to gather elements without walking the tree doc := []byte(`
results, _ := config.Query("$..[user,password]") [postgres]
for ii, item := range results.Values() { user = "pelletier"
fmt.Println("Query result %d: %v", ii, item) password = "mypassword"`)
}
config := Config{}
Unmarshal(doc, &config)
fmt.Println("user=", config.Postgres.User)
```
Or use a query:
```go
// use a query to gather elements without walking the tree
results, _ := config.Query("$..[user,password]")
for ii, item := range results.Values() {
fmt.Println("Query result %d: %v", ii, item)
} }
``` ```
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
# fail out of the script if anything here fails
set -e
# clear out stuff generated by test.sh
rm -rf src test_program_bin toml-test
+3 -3
View File
@@ -41,16 +41,16 @@ func translate(tomlData interface{}) interface{} {
typed[k] = translate(v) typed[k] = translate(v)
} }
return typed return typed
case *toml.TomlTree: case *toml.Tree:
return translate(*orig) return translate(*orig)
case toml.TomlTree: case toml.Tree:
keys := orig.Keys() keys := orig.Keys()
typed := make(map[string]interface{}, len(keys)) typed := make(map[string]interface{}, len(keys))
for _, k := range keys { for _, k := range keys {
typed[k] = translate(orig.GetPath([]string{k})) typed[k] = translate(orig.GetPath([]string{k}))
} }
return typed return typed
case []*toml.TomlTree: case []*toml.Tree:
typed := make([]map[string]interface{}, len(orig)) typed := make([]map[string]interface{}, len(orig))
for i, v := range orig { for i, v := range orig {
typed[i] = translate(v).(map[string]interface{}) typed[i] = translate(v).(map[string]interface{})
+6 -1
View File
@@ -1,3 +1,8 @@
// Tomljson reads TOML and converts to JSON.
//
// Usage:
// cat file.toml | tomljson > file.json
// tomljson file1.toml > file.json
package main package main
import ( import (
@@ -57,7 +62,7 @@ func reader(r io.Reader) (string, error) {
return mapToJSON(tree) return mapToJSON(tree)
} }
func mapToJSON(tree *toml.TomlTree) (string, error) { func mapToJSON(tree *toml.Tree) (string, error) {
treeMap := tree.ToMap() treeMap := tree.ToMap()
bytes, err := json.MarshalIndent(treeMap, "", " ") bytes, err := json.MarshalIndent(treeMap, "", " ")
if err != nil { if err != nil {
+5
View File
@@ -1,3 +1,8 @@
// Tomll is a linter for TOML
//
// Usage:
// cat file.toml | tomll > file_linted.toml
// tomll file1.toml file2.toml # lint the two files in place
package main package main
import ( import (
+12 -239
View File
@@ -1,250 +1,23 @@
// Package toml is a TOML markup language parser. // Package toml is a TOML parser and manipulation library.
// //
// This version supports the specification as described in // This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md // https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md
// //
// TOML Parsing // Marshaling
// //
// TOML data may be parsed in two ways: by file, or by string. // Go-toml can marshal and unmarshal TOML documents from and to data
// structures.
// //
// // load TOML data by filename // TOML document as a tree
// tree, err := toml.LoadFile("filename.toml")
// //
// // load TOML data stored in a string // Go-toml can operate on a TOML document as a tree. Use one of the Load*
// tree, err := toml.Load(stringContainingTomlData) // functions to parse TOML data and obtain a Tree instance, then one of its
// methods to manipulate the tree.
// //
// Either way, the result is a TomlTree object that can be used to navigate the // JSONPath-like queries
// structure and data within the original document.
// //
// // The package github.com/pelletier/go-toml/query implements a system
// Getting data from the TomlTree // similar to JSONPath to quickly retrive elements of a TOML document using a
// // single expression. See the package documentation for more information.
// 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.Println("foo is:", 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, err := tree.Query("$.foo.bar.baz")
//
// This is roughly equivalent to:
//
// next := tree.Get("foo")
// if next != nil {
// next = next.Get("bar")
// if next != nil {
// next = next.Get("baz")
// }
// }
// result := next
//
// err is nil if any parsing exception occurs.
//
// If no node in the tree matches the query, result will simply contain an empty list of
// items.
//
// As illustrated above, the query path is much more efficient, especially since
// the structure of the TOML file can vary. Rather than making assumptions about
// a document's structure, a query allows the programmer to make structured
// requests into the document, and get zero or more values as a result.
//
// 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 package toml
+27 -55
View File
@@ -6,53 +6,7 @@ import (
"fmt" "fmt"
) )
func ExampleNodeFilterFn_filterExample() { func Example_tree() {
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)
}
}
func Example_comprehensiveExample() {
config, err := LoadFile("config.toml") config, err := LoadFile("config.toml")
if err != nil { if err != nil {
@@ -63,19 +17,37 @@ func Example_comprehensiveExample() {
password := config.Get("postgres.password").(string) password := config.Get("postgres.password").(string)
// or using an intermediate object // or using an intermediate object
configTree := config.Get("postgres").(*TomlTree) configTree := config.Get("postgres").(*Tree)
user = configTree.Get("user").(string) user = configTree.Get("user").(string)
password = configTree.Get("password").(string) password = configTree.Get("password").(string)
fmt.Println("User is ", user, ". Password is ", password) fmt.Println("User is", user, " and password is", password)
// show where elements are in the file // show where elements are in the file
fmt.Printf("User position: %v\n", configTree.GetPosition("user")) fmt.Printf("User position: %v\n", configTree.GetPosition("user"))
fmt.Printf("Password position: %v\n", configTree.GetPosition("password")) fmt.Printf("Password position: %v\n", configTree.GetPosition("password"))
// use a query to gather elements without walking the tree
results, _ := config.Query("$..[user,password]")
for ii, item := range results.Values() {
fmt.Printf("Query result %d: %v\n", ii, item)
}
} }
} }
func Example_unmarshal() {
type Employer struct {
Name string
Phone string
}
type Person struct {
Name string
Age int64
Employer Employer
}
document := []byte(`
name = "John"
age = 30
[employer]
name = "Company Inc."
phone = "+1 234 567 89012"
`)
person := Person{}
Unmarshal(document, &person)
fmt.Println(person.Name, "is", person.Age, "and works at", person.Employer.Name)
}
+7 -6
View File
@@ -6,6 +6,7 @@
package toml package toml
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -24,7 +25,7 @@ type tomlLexStateFn func() tomlLexStateFn
// Define lexer // Define lexer
type tomlLexer struct { type tomlLexer struct {
input *buffruneio.Reader // Textual source input *buffruneio.Reader // Textual source
buffer []rune // Runes composing the current token buffer bytes.Buffer // Runes composing the current token
tokens chan token tokens chan token
depth int depth int
line int line int
@@ -53,13 +54,13 @@ func (l *tomlLexer) next() rune {
r := l.read() r := l.read()
if r != eof { if r != eof {
l.buffer = append(l.buffer, r) l.buffer.WriteRune(r)
} }
return r return r
} }
func (l *tomlLexer) ignore() { func (l *tomlLexer) ignore() {
l.buffer = make([]rune, 0) l.buffer.Reset()
l.line = l.endbufferLine l.line = l.endbufferLine
l.col = l.endbufferCol l.col = l.endbufferCol
} }
@@ -85,7 +86,7 @@ func (l *tomlLexer) emitWithValue(t tokenType, value string) {
} }
func (l *tomlLexer) emit(t tokenType) { func (l *tomlLexer) emit(t tokenType) {
l.emitWithValue(t, string(l.buffer)) l.emitWithValue(t, l.buffer.String())
} }
func (l *tomlLexer) peek() rune { func (l *tomlLexer) peek() rune {
@@ -536,7 +537,7 @@ func (l *tomlLexer) lexInsideTableArrayKey() tomlLexStateFn {
for r := l.peek(); r != eof; r = l.peek() { for r := l.peek(); r != eof; r = l.peek() {
switch r { switch r {
case ']': case ']':
if len(l.buffer) > 0 { if l.buffer.Len() > 0 {
l.emit(tokenKeyGroupArray) l.emit(tokenKeyGroupArray)
} }
l.next() l.next()
@@ -559,7 +560,7 @@ func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn {
for r := l.peek(); r != eof; r = l.peek() { for r := l.peek(); r != eof; r = l.peek() {
switch r { switch r {
case ']': case ']':
if len(l.buffer) > 0 { if l.buffer.Len() > 0 {
l.emit(tokenKeyGroup) l.emit(tokenKeyGroup)
} }
l.next() l.next()
+53
View File
@@ -1,6 +1,7 @@
package toml package toml
import ( import (
"os"
"strings" "strings"
"testing" "testing"
) )
@@ -531,6 +532,30 @@ func TestKeyEqualStringUnicodeEscape(t *testing.T) {
{Position{1, 8}, tokenString, "hello δ"}, {Position{1, 8}, tokenString, "hello δ"},
{Position{1, 25}, tokenEOF, ""}, {Position{1, 25}, tokenEOF, ""},
}) })
testFlow(t, `foo = "\uabcd"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\uabcd"},
{Position{1, 15}, tokenEOF, ""},
})
testFlow(t, `foo = "\uABCD"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\uABCD"},
{Position{1, 15}, tokenEOF, ""},
})
testFlow(t, `foo = "\U000bcdef"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\U000bcdef"},
{Position{1, 19}, tokenEOF, ""},
})
testFlow(t, `foo = "\U000BCDEF"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "\U000BCDEF"},
{Position{1, 19}, tokenEOF, ""},
})
testFlow(t, `foo = "\u2"`, []token{ testFlow(t, `foo = "\u2"`, []token{
{Position{1, 1}, tokenKey, "foo"}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="}, {Position{1, 5}, tokenEqual, "="},
@@ -724,3 +749,31 @@ func TestLexUnknownRvalue(t *testing.T) {
{Position{1, 5}, tokenError, `no value can start with \`}, {Position{1, 5}, tokenError, `no value can start with \`},
}) })
} }
func BenchmarkLexer(b *testing.B) {
sample := `title = "Hugo: A Fast and Flexible Website Generator"
baseurl = "http://gohugo.io/"
MetaDataFormat = "yaml"
pluralizeListTitles = false
[params]
description = "Documentation of Hugo, a fast and flexible static site generator built with love by spf13, bep and friends in Go"
author = "Steve Francia (spf13) and friends"
release = "0.22-DEV"
[[menu.main]]
name = "Download Hugo"
pre = "<i class='fa fa-download'></i>"
url = "https://github.com/spf13/hugo/releases"
weight = -200
`
rd := strings.NewReader(sample)
b.ResetTimer()
for i := 0; i < b.N; i++ {
rd.Seek(0, os.SEEK_SET)
ch := lexToml(rd)
for _ = range ch {
}
}
}
+484
View File
@@ -0,0 +1,484 @@
package toml
import (
"bytes"
"errors"
"fmt"
"reflect"
"strings"
"time"
)
type tomlOpts struct {
name string
include bool
omitempty bool
}
var timeType = reflect.TypeOf(time.Time{})
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
// Check if the given marshall type maps to a Tree primitive
func isPrimitive(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isPrimitive(mtype.Elem())
case reflect.Bool:
return true
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
case reflect.Float32, reflect.Float64:
return true
case reflect.String:
return true
case reflect.Struct:
return mtype == timeType || isCustomMarshaler(mtype)
default:
return false
}
}
// Check if the given marshall type maps to a Tree slice
func isTreeSlice(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Slice:
return !isOtherSlice(mtype)
default:
return false
}
}
// Check if the given marshall type maps to a non-Tree slice
func isOtherSlice(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isOtherSlice(mtype.Elem())
case reflect.Slice:
return isPrimitive(mtype.Elem()) || isOtherSlice(mtype.Elem())
default:
return false
}
}
// Check if the given marshall type maps to a Tree
func isTree(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Map:
return true
case reflect.Struct:
return !isPrimitive(mtype)
default:
return false
}
}
func isCustomMarshaler(mtype reflect.Type) bool {
return mtype.Implements(marshalerType)
}
func callCustomMarshaler(mval reflect.Value) ([]byte, error) {
return mval.Interface().(Marshaler).MarshalTOML()
}
// Marshaler is the interface implemented by types that
// can marshal themselves into valid TOML.
type Marshaler interface {
MarshalTOML() ([]byte, error)
}
/*
Marshal returns the TOML encoding of v. Behavior is similar to the Go json
encoder, except that there is no concept of a Marshaler interface or MarshalTOML
function for sub-structs, and currently only definite types can be marshaled
(i.e. no `interface{}`).
Note that pointers are automatically assigned the "omitempty" option, as TOML
explicity does not handle null values (saying instead the label should be
dropped).
Tree structural types and corresponding marshal types:
*Tree (*)struct, (*)map[string]interface{}
[]*Tree (*)[](*)struct, (*)[](*)map[string]interface{}
[]interface{} (as interface{}) (*)[]primitive, (*)[]([]interface{})
interface{} (*)primitive
Tree primitive types and corresponding marshal types:
uint64 uint, uint8-uint64, pointers to same
int64 int, int8-uint64, pointers to same
float64 float32, float64, pointers to same
string string, pointers to same
bool bool, pointers to same
time.Time time.Time{}, pointers to same
*/
func Marshal(v interface{}) ([]byte, error) {
mtype := reflect.TypeOf(v)
if mtype.Kind() != reflect.Struct {
return []byte{}, errors.New("Only a struct can be marshaled to TOML")
}
sval := reflect.ValueOf(v)
if isCustomMarshaler(mtype) {
return callCustomMarshaler(sval)
}
t, err := valueToTree(mtype, sval)
if err != nil {
return []byte{}, err
}
s, err := t.ToTomlString()
return []byte(s), err
}
// Convert given marshal struct or map value to toml tree
func valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, error) {
if mtype.Kind() == reflect.Ptr {
return valueToTree(mtype.Elem(), mval.Elem())
}
tval := newTree()
switch mtype.Kind() {
case reflect.Struct:
for i := 0; i < mtype.NumField(); i++ {
mtypef, mvalf := mtype.Field(i), mval.Field(i)
opts := tomlOptions(mtypef)
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
val, err := valueToToml(mtypef.Type, mvalf)
if err != nil {
return nil, err
}
tval.Set(opts.name, val)
}
}
case reflect.Map:
for _, key := range mval.MapKeys() {
mvalf := mval.MapIndex(key)
val, err := valueToToml(mtype.Elem(), mvalf)
if err != nil {
return nil, err
}
tval.Set(key.String(), val)
}
}
return tval, nil
}
// Convert given marshal slice to slice of Toml trees
func valueToTreeSlice(mtype reflect.Type, mval reflect.Value) ([]*Tree, error) {
tval := make([]*Tree, mval.Len(), mval.Len())
for i := 0; i < mval.Len(); i++ {
val, err := valueToTree(mtype.Elem(), mval.Index(i))
if err != nil {
return nil, err
}
tval[i] = val
}
return tval, nil
}
// Convert given marshal slice to slice of toml values
func valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
tval := make([]interface{}, mval.Len(), mval.Len())
for i := 0; i < mval.Len(); i++ {
val, err := valueToToml(mtype.Elem(), mval.Index(i))
if err != nil {
return nil, err
}
tval[i] = val
}
return tval, nil
}
// Convert given marshal value to toml value
func valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
if mtype.Kind() == reflect.Ptr {
return valueToToml(mtype.Elem(), mval.Elem())
}
switch {
case isCustomMarshaler(mtype):
return callCustomMarshaler(mval)
case isTree(mtype):
return valueToTree(mtype, mval)
case isTreeSlice(mtype):
return valueToTreeSlice(mtype, mval)
case isOtherSlice(mtype):
return valueToOtherSlice(mtype, mval)
default:
switch mtype.Kind() {
case reflect.Bool:
return mval.Bool(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return mval.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return mval.Uint(), nil
case reflect.Float32, reflect.Float64:
return mval.Float(), nil
case reflect.String:
return mval.String(), nil
case reflect.Struct:
return mval.Interface().(time.Time), nil
default:
return nil, fmt.Errorf("Marshal can't handle %v(%v)", mtype, mtype.Kind())
}
}
}
// Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v.
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
// sub-structs, and only definite types can be unmarshaled.
func (t *Tree) Unmarshal(v interface{}) error {
mtype := reflect.TypeOf(v)
if mtype.Kind() != reflect.Ptr || mtype.Elem().Kind() != reflect.Struct {
return errors.New("Only a pointer to struct can be unmarshaled from TOML")
}
sval, err := valueFromTree(mtype.Elem(), t)
if err != nil {
return err
}
reflect.ValueOf(v).Elem().Set(sval)
return nil
}
// Unmarshal parses the TOML-encoded data and stores the result in the value
// pointed to by v. Behavior is similar to the Go json encoder, except that there
// is no concept of an Unmarshaler interface or UnmarshalTOML function for
// sub-structs, and currently only definite types can be unmarshaled to (i.e. no
// `interface{}`).
//
// See Marshal() documentation for types mapping table.
func Unmarshal(data []byte, v interface{}) error {
t, err := LoadReader(bytes.NewReader(data))
if err != nil {
return err
}
return t.Unmarshal(v)
}
// Convert toml tree to marshal struct or map, using marshal type
func valueFromTree(mtype reflect.Type, tval *Tree) (reflect.Value, error) {
if mtype.Kind() == reflect.Ptr {
return unwrapPointer(mtype, tval)
}
var mval reflect.Value
switch mtype.Kind() {
case reflect.Struct:
mval = reflect.New(mtype).Elem()
for i := 0; i < mtype.NumField(); i++ {
mtypef := mtype.Field(i)
opts := tomlOptions(mtypef)
if opts.include {
key := opts.name
exists := tval.Has(key)
if exists {
val := tval.Get(key)
mvalf, err := valueFromToml(mtypef.Type, val)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.Field(i).Set(mvalf)
}
}
}
case reflect.Map:
mval = reflect.MakeMap(mtype)
for _, key := range tval.Keys() {
val := tval.Get(key)
mvalf, err := valueFromToml(mtype.Elem(), val)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.SetMapIndex(reflect.ValueOf(key), mvalf)
}
}
return mval, nil
}
// Convert toml value to marshal struct/map slice, using marshal type
func valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
for i := 0; i < len(tval); i++ {
val, err := valueFromTree(mtype.Elem(), tval[i])
if err != nil {
return mval, err
}
mval.Index(i).Set(val)
}
return mval, nil
}
// Convert toml value to marshal primitive slice, using marshal type
func valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
for i := 0; i < len(tval); i++ {
val, err := valueFromToml(mtype.Elem(), tval[i])
if err != nil {
return mval, err
}
mval.Index(i).Set(val)
}
return mval, nil
}
// Convert toml value to marshal value, using marshal type
func valueFromToml(mtype reflect.Type, tval interface{}) (reflect.Value, error) {
if mtype.Kind() == reflect.Ptr {
return unwrapPointer(mtype, tval)
}
switch {
case isTree(mtype):
return valueFromTree(mtype, tval.(*Tree))
case isTreeSlice(mtype):
return valueFromTreeSlice(mtype, tval.([]*Tree))
case isOtherSlice(mtype):
return valueFromOtherSlice(mtype, tval.([]interface{}))
default:
switch mtype.Kind() {
case reflect.Bool:
val, ok := tval.(bool)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to bool", tval, tval)
}
return reflect.ValueOf(val), nil
case reflect.Int:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
}
return reflect.ValueOf(int(val)), nil
case reflect.Int8:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
}
return reflect.ValueOf(int8(val)), nil
case reflect.Int16:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
}
return reflect.ValueOf(int16(val)), nil
case reflect.Int32:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
}
return reflect.ValueOf(int32(val)), nil
case reflect.Int64:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to int", tval, tval)
}
return reflect.ValueOf(val), nil
case reflect.Uint:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
}
return reflect.ValueOf(uint(val)), nil
case reflect.Uint8:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
}
return reflect.ValueOf(uint8(val)), nil
case reflect.Uint16:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
}
return reflect.ValueOf(uint16(val)), nil
case reflect.Uint32:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
}
return reflect.ValueOf(uint32(val)), nil
case reflect.Uint64:
val, ok := tval.(int64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to uint", tval, tval)
}
return reflect.ValueOf(uint64(val)), nil
case reflect.Float32:
val, ok := tval.(float64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to float", tval, tval)
}
return reflect.ValueOf(float32(val)), nil
case reflect.Float64:
val, ok := tval.(float64)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to float", tval, tval)
}
return reflect.ValueOf(val), nil
case reflect.String:
val, ok := tval.(string)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to string", tval, tval)
}
return reflect.ValueOf(val), nil
case reflect.Struct:
val, ok := tval.(time.Time)
if !ok {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to time", tval, tval)
}
return reflect.ValueOf(val), nil
default:
return reflect.ValueOf(nil), fmt.Errorf("Unmarshal can't handle %v(%v)", mtype, mtype.Kind())
}
}
}
func unwrapPointer(mtype reflect.Type, tval interface{}) (reflect.Value, error) {
val, err := valueFromToml(mtype.Elem(), tval)
if err != nil {
return reflect.ValueOf(nil), err
}
mval := reflect.New(mtype.Elem())
mval.Elem().Set(val)
return mval, nil
}
func tomlOptions(vf reflect.StructField) tomlOpts {
tag := vf.Tag.Get("toml")
parse := strings.Split(tag, ",")
result := tomlOpts{vf.Name, true, false}
if parse[0] != "" {
if parse[0] == "-" && len(parse) == 1 {
result.include = false
} else {
result.name = strings.Trim(parse[0], " ")
}
}
if vf.PkgPath != "" {
result.include = false
}
if len(parse) > 1 && strings.Trim(parse[1], " ") == "omitempty" {
result.omitempty = true
}
if vf.Type.Kind() == reflect.Ptr {
result.omitempty = true
}
return result
}
func isZero(val reflect.Value) bool {
switch val.Type().Kind() {
case reflect.Map:
fallthrough
case reflect.Array:
fallthrough
case reflect.Slice:
return val.Len() == 0
default:
return reflect.DeepEqual(val.Interface(), reflect.Zero(val.Type()).Interface())
}
}
func formatError(err error, pos Position) error {
if err.Error()[0] == '(' { // Error already contains position information
return err
}
return fmt.Errorf("%s: %s", pos, err)
}
+619
View File
@@ -0,0 +1,619 @@
package toml
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"testing"
"time"
)
type basicMarshalTestStruct struct {
String string `toml:"string"`
StringList []string `toml:"strlist"`
Sub basicMarshalTestSubStruct `toml:"subdoc"`
SubList []basicMarshalTestSubStruct `toml:"sublist"`
}
type basicMarshalTestSubStruct struct {
String2 string
}
var basicTestData = basicMarshalTestStruct{
String: "Hello",
StringList: []string{"Howdy", "Hey There"},
Sub: basicMarshalTestSubStruct{"One"},
SubList: []basicMarshalTestSubStruct{{"Two"}, {"Three"}},
}
var basicTestToml = []byte(`string = "Hello"
strlist = ["Howdy","Hey There"]
[subdoc]
String2 = "One"
[[sublist]]
String2 = "Two"
[[sublist]]
String2 = "Three"
`)
func TestBasicMarshal(t *testing.T) {
result, err := Marshal(basicTestData)
if err != nil {
t.Fatal(err)
}
expected := basicTestToml
if !bytes.Equal(result, expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
func TestBasicUnmarshal(t *testing.T) {
result := basicMarshalTestStruct{}
err := Unmarshal(basicTestToml, &result)
expected := basicTestData
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Bad unmarshal: expected %v, got %v", expected, result)
}
}
type testDoc struct {
Title string `toml:"title"`
Basics testDocBasics `toml:"basic"`
BasicLists testDocBasicLists `toml:"basic_lists"`
BasicMap map[string]string `toml:"basic_map"`
Subdocs testDocSubs `toml:"subdoc"`
SubDocList []testSubDoc `toml:"subdoclist"`
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
err int `toml:"shouldntBeHere"`
unexported int `toml:"shouldntBeHere"`
Unexported2 int `toml:"-"`
}
type testDocBasics struct {
Bool bool `toml:"bool"`
Date time.Time `toml:"date"`
Float float32 `toml:"float"`
Int int `toml:"int"`
Uint uint `toml:"uint"`
String *string `toml:"string"`
unexported int `toml:"shouldntBeHere"`
}
type testDocBasicLists struct {
Bools []bool `toml:"bools"`
Dates []time.Time `toml:"dates"`
Floats []*float32 `toml:"floats"`
Ints []int `toml:"ints"`
Strings []string `toml:"strings"`
UInts []uint `toml:"uints"`
}
type testDocSubs struct {
First testSubDoc `toml:"first"`
Second *testSubDoc `toml:"second"`
}
type testSubDoc struct {
Name string `toml:"name"`
unexported int `toml:"shouldntBeHere"`
}
var biteMe = "Bite me"
var float1 float32 = 12.3
var float2 float32 = 45.6
var float3 float32 = 78.9
var subdoc = testSubDoc{"Second", 0}
var docData = testDoc{
Title: "TOML Marshal Testing",
unexported: 0,
Unexported2: 0,
Basics: testDocBasics{
Bool: true,
Date: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
Float: 123.4,
Int: 5000,
Uint: 5001,
String: &biteMe,
unexported: 0,
},
BasicLists: testDocBasicLists{
Bools: []bool{true, false, true},
Dates: []time.Time{
time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
time.Date(1980, 5, 27, 7, 32, 0, 0, time.UTC),
},
Floats: []*float32{&float1, &float2, &float3},
Ints: []int{8001, 8001, 8002},
Strings: []string{"One", "Two", "Three"},
UInts: []uint{5002, 5003},
},
BasicMap: map[string]string{
"one": "one",
"two": "two",
},
Subdocs: testDocSubs{
First: testSubDoc{"First", 0},
Second: &subdoc,
},
SubDocList: []testSubDoc{
testSubDoc{"List.First", 0},
testSubDoc{"List.Second", 0},
},
SubDocPtrs: []*testSubDoc{&subdoc},
}
func TestDocMarshal(t *testing.T) {
result, err := Marshal(docData)
if err != nil {
t.Fatal(err)
}
expected, _ := ioutil.ReadFile("marshal_test.toml")
if !bytes.Equal(result, expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
func TestDocUnmarshal(t *testing.T) {
result := testDoc{}
tomlData, _ := ioutil.ReadFile("marshal_test.toml")
err := Unmarshal(tomlData, &result)
expected := docData
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
resStr, _ := json.MarshalIndent(result, "", " ")
expStr, _ := json.MarshalIndent(expected, "", " ")
t.Errorf("Bad unmarshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expStr, resStr)
}
}
func ExampleUnmarshal() {
type Postgres struct {
User string
Password string
}
type Config struct {
Postgres Postgres
}
doc := []byte(`
[postgres]
user = "pelletier"
password = "mypassword"`)
config := Config{}
Unmarshal(doc, &config)
fmt.Println("user=", config.Postgres.User)
}
func TestDocPartialUnmarshal(t *testing.T) {
result := testDocSubs{}
tree, _ := LoadFile("marshal_test.toml")
subTree := tree.Get("subdoc").(*Tree)
err := subTree.Unmarshal(&result)
expected := docData.Subdocs
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
resStr, _ := json.MarshalIndent(result, "", " ")
expStr, _ := json.MarshalIndent(expected, "", " ")
t.Errorf("Bad partial unmartial: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expStr, resStr)
}
}
type tomlTypeCheckTest struct {
name string
item interface{}
typ int //0=primitive, 1=otherslice, 2=treeslice, 3=tree
}
func TestTypeChecks(t *testing.T) {
tests := []tomlTypeCheckTest{
{"integer", 2, 0},
{"time", time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC), 0},
{"stringlist", []string{"hello", "hi"}, 1},
{"timelist", []time.Time{time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)}, 1},
{"objectlist", []tomlTypeCheckTest{}, 2},
{"object", tomlTypeCheckTest{}, 3},
}
for _, test := range tests {
expected := []bool{false, false, false, false}
expected[test.typ] = true
result := []bool{
isPrimitive(reflect.TypeOf(test.item)),
isOtherSlice(reflect.TypeOf(test.item)),
isTreeSlice(reflect.TypeOf(test.item)),
isTree(reflect.TypeOf(test.item)),
}
if !reflect.DeepEqual(expected, result) {
t.Errorf("Bad type check on %q: expected %v, got %v", test.name, expected, result)
}
}
}
type unexportedMarshalTestStruct struct {
String string `toml:"string"`
StringList []string `toml:"strlist"`
Sub basicMarshalTestSubStruct `toml:"subdoc"`
SubList []basicMarshalTestSubStruct `toml:"sublist"`
unexported int `toml:"shouldntBeHere"`
Unexported2 int `toml:"-"`
}
var unexportedTestData = unexportedMarshalTestStruct{
String: "Hello",
StringList: []string{"Howdy", "Hey There"},
Sub: basicMarshalTestSubStruct{"One"},
SubList: []basicMarshalTestSubStruct{{"Two"}, {"Three"}},
unexported: 0,
Unexported2: 0,
}
var unexportedTestToml = []byte(`string = "Hello"
strlist = ["Howdy","Hey There"]
unexported = 1
shouldntBeHere = 2
[subdoc]
String2 = "One"
[[sublist]]
String2 = "Two"
[[sublist]]
String2 = "Three"
`)
func TestUnexportedUnmarshal(t *testing.T) {
result := unexportedMarshalTestStruct{}
err := Unmarshal(unexportedTestToml, &result)
expected := unexportedTestData
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Bad unexported unmarshal: expected %v, got %v", expected, result)
}
}
type errStruct struct {
Bool bool `toml:"bool"`
Date time.Time `toml:"date"`
Float float64 `toml:"float"`
Int int16 `toml:"int"`
String *string `toml:"string"`
}
var errTomls = []string{
"bool = truly\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:3200Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123a4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = j000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = Bite me",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = Bite me",
"bool = 1\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\n\"sorry\"\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = \"sorry\"\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = 1",
}
type mapErr struct {
Vals map[string]float64
}
type intErr struct {
Int1 int
Int2 int8
Int3 int16
Int4 int32
Int5 int64
UInt1 uint
UInt2 uint8
UInt3 uint16
UInt4 uint32
UInt5 uint64
Flt1 float32
Flt2 float64
}
var intErrTomls = []string{
"Int1 = []\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = []\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = []\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = []\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = []\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = []\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = []\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = []\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = []\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = []\nFlt1 = 1.0\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = []\nFlt2 = 2.0",
"Int1 = 1\nInt2 = 2\nInt3 = 3\nInt4 = 4\nInt5 = 5\nUInt1 = 1\nUInt2 = 2\nUInt3 = 3\nUInt4 = 4\nUInt5 = 5\nFlt1 = 1.0\nFlt2 = []",
}
func TestErrUnmarshal(t *testing.T) {
for ind, toml := range errTomls {
result := errStruct{}
err := Unmarshal([]byte(toml), &result)
if err == nil {
t.Errorf("Expected err from case %d\n", ind)
}
}
result2 := mapErr{}
err := Unmarshal([]byte("[Vals]\nfred=\"1.2\""), &result2)
if err == nil {
t.Errorf("Expected err from map")
}
for ind, toml := range intErrTomls {
result3 := intErr{}
err := Unmarshal([]byte(toml), &result3)
if err == nil {
t.Errorf("Expected int err from case %d\n", ind)
}
}
}
type emptyMarshalTestStruct struct {
Title string `toml:"title"`
Bool bool `toml:"bool"`
Int int `toml:"int"`
String string `toml:"string"`
StringList []string `toml:"stringlist"`
Ptr *basicMarshalTestStruct `toml:"ptr"`
Map map[string]string `toml:"map"`
}
var emptyTestData = emptyMarshalTestStruct{
Title: "Placeholder",
Bool: false,
Int: 0,
String: "",
StringList: []string{},
Ptr: nil,
Map: map[string]string{},
}
var emptyTestToml = []byte(`bool = false
int = 0
string = ""
stringlist = []
title = "Placeholder"
[map]
`)
type emptyMarshalTestStruct2 struct {
Title string `toml:"title"`
Bool bool `toml:"bool,omitempty"`
Int int `toml:"int, omitempty"`
String string `toml:"string,omitempty "`
StringList []string `toml:"stringlist,omitempty"`
Ptr *basicMarshalTestStruct `toml:"ptr,omitempty"`
Map map[string]string `toml:"map,omitempty"`
}
var emptyTestData2 = emptyMarshalTestStruct2{
Title: "Placeholder",
Bool: false,
Int: 0,
String: "",
StringList: []string{},
Ptr: nil,
Map: map[string]string{},
}
var emptyTestToml2 = []byte(`title = "Placeholder"
`)
func TestEmptyMarshal(t *testing.T) {
result, err := Marshal(emptyTestData)
if err != nil {
t.Fatal(err)
}
expected := emptyTestToml
if !bytes.Equal(result, expected) {
t.Errorf("Bad empty marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
func TestEmptyMarshalOmit(t *testing.T) {
result, err := Marshal(emptyTestData2)
if err != nil {
t.Fatal(err)
}
expected := emptyTestToml2
if !bytes.Equal(result, expected) {
t.Errorf("Bad empty omit marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
func TestEmptyUnmarshal(t *testing.T) {
result := emptyMarshalTestStruct{}
err := Unmarshal(emptyTestToml, &result)
expected := emptyTestData
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Bad empty unmarshal: expected %v, got %v", expected, result)
}
}
func TestEmptyUnmarshalOmit(t *testing.T) {
result := emptyMarshalTestStruct2{}
err := Unmarshal(emptyTestToml, &result)
expected := emptyTestData2
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Bad empty omit unmarshal: expected %v, got %v", expected, result)
}
}
type pointerMarshalTestStruct struct {
Str *string
List *[]string
ListPtr *[]*string
Map *map[string]string
MapPtr *map[string]*string
EmptyStr *string
EmptyList *[]string
EmptyMap *map[string]string
DblPtr *[]*[]*string
}
var pointerStr = "Hello"
var pointerList = []string{"Hello back"}
var pointerListPtr = []*string{&pointerStr}
var pointerMap = map[string]string{"response": "Goodbye"}
var pointerMapPtr = map[string]*string{"alternate": &pointerStr}
var pointerTestData = pointerMarshalTestStruct{
Str: &pointerStr,
List: &pointerList,
ListPtr: &pointerListPtr,
Map: &pointerMap,
MapPtr: &pointerMapPtr,
EmptyStr: nil,
EmptyList: nil,
EmptyMap: nil,
}
var pointerTestToml = []byte(`List = ["Hello back"]
ListPtr = ["Hello"]
Str = "Hello"
[Map]
response = "Goodbye"
[MapPtr]
alternate = "Hello"
`)
func TestPointerMarshal(t *testing.T) {
result, err := Marshal(pointerTestData)
if err != nil {
t.Fatal(err)
}
expected := pointerTestToml
if !bytes.Equal(result, expected) {
t.Errorf("Bad pointer marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
func TestPointerUnmarshal(t *testing.T) {
result := pointerMarshalTestStruct{}
err := Unmarshal(pointerTestToml, &result)
expected := pointerTestData
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Bad pointer unmarshal: expected %v, got %v", expected, result)
}
}
type nestedMarshalTestStruct struct {
String [][]string
//Struct [][]basicMarshalTestSubStruct
StringPtr *[]*[]*string
// StructPtr *[]*[]*basicMarshalTestSubStruct
}
var str1 = "Three"
var str2 = "Four"
var strPtr = []*string{&str1, &str2}
var strPtr2 = []*[]*string{&strPtr}
var nestedTestData = nestedMarshalTestStruct{
String: [][]string{[]string{"Five", "Six"}, []string{"One", "Two"}},
StringPtr: &strPtr2,
}
var nestedTestToml = []byte(`String = [["Five","Six"],["One","Two"]]
StringPtr = [["Three","Four"]]
`)
func TestNestedMarshal(t *testing.T) {
result, err := Marshal(nestedTestData)
if err != nil {
t.Fatal(err)
}
expected := nestedTestToml
if !bytes.Equal(result, expected) {
t.Errorf("Bad nested marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
func TestNestedUnmarshal(t *testing.T) {
result := nestedMarshalTestStruct{}
err := Unmarshal(nestedTestToml, &result)
expected := nestedTestData
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(result, expected) {
t.Errorf("Bad nested unmarshal: expected %v, got %v", expected, result)
}
}
type customMarshalerParent struct {
Self customMarshaler `toml:"me"`
Friends []customMarshaler `toml:"friends"`
}
type customMarshaler struct {
FirsName string
LastName string
}
func (c customMarshaler) MarshalTOML() ([]byte, error) {
fullName := fmt.Sprintf("%s %s", c.FirsName, c.LastName)
return []byte(fullName), nil
}
var customMarshalerData = customMarshaler{FirsName: "Sally", LastName: "Fields"}
var customMarshalerToml = []byte(`Sally Fields`)
var nestedCustomMarshalerData = customMarshalerParent{
Self: customMarshaler{FirsName: "Maiku", LastName: "Suteda"},
Friends: []customMarshaler{customMarshalerData},
}
var nestedCustomMarshalerToml = []byte(`friends = ["Sally Fields"]
me = "Maiku Suteda"
`)
func TestCustomMarshaler(t *testing.T) {
result, err := Marshal(customMarshalerData)
if err != nil {
t.Fatal(err)
}
expected := customMarshalerToml
if !bytes.Equal(result, expected) {
t.Errorf("Bad custom marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
func TestNestedCustomMarshaler(t *testing.T) {
result, err := Marshal(nestedCustomMarshalerData)
if err != nil {
t.Fatal(err)
}
expected := nestedCustomMarshalerToml
if !bytes.Equal(result, expected) {
t.Errorf("Bad nested custom marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
+38
View File
@@ -0,0 +1,38 @@
title = "TOML Marshal Testing"
[basic]
bool = true
date = 1979-05-27T07:32:00Z
float = 123.4
int = 5000
string = "Bite me"
uint = 5001
[basic_lists]
bools = [true,false,true]
dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z]
floats = [12.3,45.6,78.9]
ints = [8001,8001,8002]
strings = ["One","Two","Three"]
uints = [5002,5003]
[basic_map]
one = "one"
two = "two"
[subdoc]
[subdoc.first]
name = "First"
[subdoc.second]
name = "Second"
[[subdoclist]]
name = "List.First"
[[subdoclist]]
name = "List.Second"
[[subdocptrs]]
name = "Second"
+19 -19
View File
@@ -14,7 +14,7 @@ import (
type tomlParser struct { type tomlParser struct {
flow chan token flow chan token
tree *TomlTree tree *Tree
tokensBuffer []token tokensBuffer []token
currentTable []string currentTable []string
seenTableKeys []string seenTableKeys []string
@@ -106,18 +106,18 @@ func (p *tomlParser) parseGroupArray() tomlParserStateFn {
} }
p.tree.createSubTree(keys[:len(keys)-1], startToken.Position) // create parent entries p.tree.createSubTree(keys[:len(keys)-1], startToken.Position) // create parent entries
destTree := p.tree.GetPath(keys) destTree := p.tree.GetPath(keys)
var array []*TomlTree var array []*Tree
if destTree == nil { if destTree == nil {
array = make([]*TomlTree, 0) array = make([]*Tree, 0)
} else if target, ok := destTree.([]*TomlTree); ok && target != nil { } else if target, ok := destTree.([]*Tree); ok && target != nil {
array = destTree.([]*TomlTree) array = destTree.([]*Tree)
} else { } else {
p.raiseError(key, "key %s is already assigned and not of type table array", key) p.raiseError(key, "key %s is already assigned and not of type table array", key)
} }
p.currentTable = keys p.currentTable = keys
// add a new tree to the end of the table array // add a new tree to the end of the table array
newTree := newTomlTree() newTree := newTree()
newTree.position = startToken.Position newTree.position = startToken.Position
array = append(array, newTree) array = append(array, newTree)
p.tree.SetPath(p.currentTable, array) p.tree.SetPath(p.currentTable, array)
@@ -183,11 +183,11 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
} }
// find the table to assign, looking out for arrays of tables // find the table to assign, looking out for arrays of tables
var targetNode *TomlTree var targetNode *Tree
switch node := p.tree.GetPath(tableKey).(type) { switch node := p.tree.GetPath(tableKey).(type) {
case []*TomlTree: case []*Tree:
targetNode = node[len(node)-1] targetNode = node[len(node)-1]
case *TomlTree: case *Tree:
targetNode = node targetNode = node
default: default:
p.raiseError(key, "Unknown table type for path: %s", p.raiseError(key, "Unknown table type for path: %s",
@@ -212,7 +212,7 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
var toInsert interface{} var toInsert interface{}
switch value.(type) { switch value.(type) {
case *TomlTree, []*TomlTree: case *Tree, []*Tree:
toInsert = value toInsert = value
default: default:
toInsert = &tomlValue{value, key.Position} toInsert = &tomlValue{value, key.Position}
@@ -289,8 +289,8 @@ func tokenIsComma(t *token) bool {
return t != nil && t.typ == tokenComma return t != nil && t.typ == tokenComma
} }
func (p *tomlParser) parseInlineTable() *TomlTree { func (p *tomlParser) parseInlineTable() *Tree {
tree := newTomlTree() tree := newTree()
var previous *token var previous *token
Loop: Loop:
for { for {
@@ -360,22 +360,22 @@ func (p *tomlParser) parseArray() interface{} {
p.getToken() p.getToken()
} }
} }
// An array of TomlTrees is actually an array of inline // An array of Trees is actually an array of inline
// tables, which is a shorthand for a table array. If the // tables, which is a shorthand for a table array. If the
// array was not converted from []interface{} to []*TomlTree, // array was not converted from []interface{} to []*Tree,
// the two notations would not be equivalent. // the two notations would not be equivalent.
if arrayType == reflect.TypeOf(newTomlTree()) { if arrayType == reflect.TypeOf(newTree()) {
tomlArray := make([]*TomlTree, len(array)) tomlArray := make([]*Tree, len(array))
for i, v := range array { for i, v := range array {
tomlArray[i] = v.(*TomlTree) tomlArray[i] = v.(*Tree)
} }
return tomlArray return tomlArray
} }
return array return array
} }
func parseToml(flow chan token) *TomlTree { func parseToml(flow chan token) *Tree {
result := newTomlTree() result := newTree()
result.position = Position{1, 1} result.position = Position{1, 1}
parser := &tomlParser{ parser := &tomlParser{
flow: flow, flow: flow,
+11 -9
View File
@@ -9,7 +9,7 @@ import (
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
) )
func assertSubTree(t *testing.T, path []string, tree *TomlTree, err error, ref map[string]interface{}) { func assertSubTree(t *testing.T, path []string, tree *Tree, err error, ref map[string]interface{}) {
if err != nil { if err != nil {
t.Error("Non-nil error:", err.Error()) t.Error("Non-nil error:", err.Error())
return return
@@ -20,12 +20,12 @@ func assertSubTree(t *testing.T, path []string, tree *TomlTree, err error, ref m
// NOTE: directly access key instead of resolve by path // NOTE: directly access key instead of resolve by path
// NOTE: see TestSpecialKV // NOTE: see TestSpecialKV
switch node := tree.GetPath([]string{k}).(type) { switch node := tree.GetPath([]string{k}).(type) {
case []*TomlTree: case []*Tree:
t.Log("\tcomparing key", nextPath, "by array iteration") t.Log("\tcomparing key", nextPath, "by array iteration")
for idx, item := range node { for idx, item := range node {
assertSubTree(t, nextPath, item, err, v.([]map[string]interface{})[idx]) assertSubTree(t, nextPath, item, err, v.([]map[string]interface{})[idx])
} }
case *TomlTree: case *Tree:
t.Log("\tcomparing key", nextPath, "by subtree assestion") t.Log("\tcomparing key", nextPath, "by subtree assestion")
assertSubTree(t, nextPath, node, err, v.(map[string]interface{})) assertSubTree(t, nextPath, node, err, v.(map[string]interface{}))
default: default:
@@ -37,14 +37,14 @@ func assertSubTree(t *testing.T, path []string, tree *TomlTree, err error, ref m
} }
} }
func assertTree(t *testing.T, tree *TomlTree, err error, ref map[string]interface{}) { func assertTree(t *testing.T, tree *Tree, err error, ref map[string]interface{}) {
t.Log("Asserting tree:\n", spew.Sdump(tree)) t.Log("Asserting tree:\n", spew.Sdump(tree))
assertSubTree(t, []string{}, tree, err, ref) assertSubTree(t, []string{}, tree, err, ref)
t.Log("Finished tree assertion.") t.Log("Finished tree assertion.")
} }
func TestCreateSubTree(t *testing.T) { func TestCreateSubTree(t *testing.T) {
tree := newTomlTree() tree := newTree()
tree.createSubTree([]string{"a", "b", "c"}, Position{}) tree.createSubTree([]string{"a", "b", "c"}, Position{})
tree.Set("a.b.c", 42) tree.Set("a.b.c", 42)
if tree.Get("a.b.c") != 42 { if tree.Get("a.b.c") != 42 {
@@ -500,7 +500,8 @@ func TestFloatsWithoutLeadingZeros(t *testing.T) {
func TestMissingFile(t *testing.T) { func TestMissingFile(t *testing.T) {
_, err := LoadFile("foo.toml") _, err := LoadFile("foo.toml")
if err.Error() != "open foo.toml: no such file or directory" { if err.Error() != "open foo.toml: no such file or directory" &&
err.Error() != "open foo.toml: The system cannot find the file specified." {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
@@ -662,10 +663,11 @@ func TestTomlValueStringRepresentation(t *testing.T) {
} }
func TestToStringMapStringString(t *testing.T) { func TestToStringMapStringString(t *testing.T) {
in := map[string]interface{}{"m": TreeFromMap(map[string]interface{}{ tree, err := TreeFromMap(map[string]interface{}{"m": map[string]interface{}{"v": "abc"}})
"v": &tomlValue{"abc", Position{0, 0}}})} if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := "\n[m]\n v = \"abc\"\n" want := "\n[m]\n v = \"abc\"\n"
tree := TreeFromMap(in)
got := tree.String() got := tree.String()
if got != want { if got != want {
+175
View File
@@ -0,0 +1,175 @@
// Package query performs JSONPath-like queries on a TOML document.
//
// The query path implementation is based loosely on the JSONPath specification:
// http://goessner.net/articles/JsonPath/.
//
// The idea behind a query path is to allow quick access to any element, or set
// of elements within TOML document, with a single expression.
//
// result, err := query.CompileAndExecute("$.foo.bar.baz", tree)
//
// This is roughly equivalent to:
//
// next := tree.Get("foo")
// if next != nil {
// next = next.Get("bar")
// if next != nil {
// next = next.Get("baz")
// }
// }
// result := next
//
// err is nil if any parsing exception occurs.
//
// If no node in the tree matches the query, result will simply contain an empty list of
// items.
//
// As illustrated above, the query path is much more efficient, especially since
// the structure of the TOML file can vary. Rather than making assumptions about
// a document's structure, a query allows the programmer to make structured
// requests into the document, and get zero or more values as a result.
//
// Query syntax
//
// The syntax of a query begins with a root token, followed by any number
// sub-expressions:
//
// $
// Root of the TOML tree. This must always come first.
// .name
// Selects child of this node, where 'name' is a TOML key
// name.
// ['name']
// Selects child of this node, where 'name' is a string
// containing a TOML key name.
// [index]
// Selcts child array element at 'index'.
// ..expr
// Recursively selects all children, filtered by an a union,
// index, or slice expression.
// ..*
// Recursive selection of all nodes at this point in the
// tree.
// .*
// Selects all children of the current node.
// [expr,expr]
// Union operator - a logical 'or' grouping of two or more
// sub-expressions: index, key name, or filter.
// [start:end:step]
// Slice operator - selects array elements from start to
// end-1, at the given step. All three arguments are
// optional.
// [?(filter)]
// Named filter expression - the function 'filter' is
// used to filter children at this node.
//
// Query Indexes And Slices
//
// Index expressions perform no bounds checking, and will contribute no
// values to the result set if the provided index or index range is invalid.
// Negative indexes represent values from the end of the array, counting backwards.
//
// // select the last index of the array named 'foo'
// query.CompileAndExecute("$.foo[-1]", tree)
//
// Slice expressions are supported, by using ':' to separate a start/end index pair.
//
// // select up to the first five elements in the array
// query.CompileAndExecute("$.foo[0:5]", tree)
//
// Slice expressions also allow negative indexes for the start and stop
// arguments.
//
// // select all array elements.
// query.CompileAndExecute("$.foo[0:-1]", tree)
//
// Slice expressions may have an optional stride/step parameter:
//
// // select every other element
// query.CompileAndExecute("$.foo[0:-1:2]", tree)
//
// Slice start and end parameters are also optional:
//
// // these are all equivalent and select all the values in the array
// query.CompileAndExecute("$.foo[:]", tree)
// query.CompileAndExecute("$.foo[0:]", tree)
// query.CompileAndExecute("$.foo[:-1]", tree)
// query.CompileAndExecute("$.foo[0:-1:]", tree)
// query.CompileAndExecute("$.foo[::1]", tree)
// query.CompileAndExecute("$.foo[0::1]", tree)
// query.CompileAndExecute("$.foo[:-1:1]", tree)
// query.CompileAndExecute("$.foo[0:-1:1]", tree)
//
// Query Filters
//
// Query filters 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.
// query.CompileAndExecute("$.foo[?(bar)]", tree)
//
// There are several filters provided with the library:
//
// tree
// Allows nodes of type Tree.
// int
// Allows nodes of type int64.
// float
// Allows nodes of type float64.
// string
// Allows nodes of type string.
// time
// Allows nodes of type time.Time.
// bool
// Allows nodes of type bool.
//
// Query Results
//
// An executed query returns a Result object. This contains the nodes
// in the TOML tree that qualify the query expression. Position information
// is also available for each value in the set.
//
// // display the results of a query
// results := query.CompileAndExecute("$.foo.bar.baz", tree)
// for idx, value := results.Values() {
// fmt.Println("%v: %v", results.Positions()[idx], value)
// }
//
// Compiled Queries
//
// Queries may be executed directly on a Tree object, or compiled ahead
// of time and executed discretely. The former is more convienent, but has the
// penalty of having to recompile the query expression each time.
//
// // basic query
// results := query.CompileAndExecute("$.foo.bar.baz", tree)
//
// // compiled query
// query, err := toml.Compile("$.foo.bar.baz")
// results := query.Execute(tree)
//
// // run the compiled query again on a different tree
// moreResults := query.Execute(anotherTree)
//
// User Defined Query Filters
//
// Filter expressions may also be user defined by using the SetFilter()
// function on the Query object. The function must return true/false, which
// signifies if the passed node is kept or discarded, respectively.
//
// // create a query that references a user-defined filter
// query, _ := query.Compile("$[?(bazOnly)]")
//
// // define the filter, and assign it to the query
// query.SetFilter("bazOnly", func(node interface{}) bool{
// if tree, ok := node.(*Tree); ok {
// return tree.Has("baz")
// }
// return false // reject all other node types
// })
//
// // run the query
// query.Execute(tree)
//
package query
+5 -4
View File
@@ -3,13 +3,14 @@
// Written using the principles developed by Rob Pike in // Written using the principles developed by Rob Pike in
// http://www.youtube.com/watch?v=HxaD_trXwRE // http://www.youtube.com/watch?v=HxaD_trXwRE
package toml package query
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/pelletier/go-toml"
) )
// Lexer state function // Lexer state function
@@ -54,7 +55,7 @@ func (l *queryLexer) nextStart() {
func (l *queryLexer) emit(t tokenType) { func (l *queryLexer) emit(t tokenType) {
l.tokens <- token{ l.tokens <- token{
Position: Position{l.line, l.col}, Position: toml.Position{Line:l.line, Col:l.col},
typ: t, typ: t,
val: l.input[l.start:l.pos], val: l.input[l.start:l.pos],
} }
@@ -63,7 +64,7 @@ func (l *queryLexer) emit(t tokenType) {
func (l *queryLexer) emitWithValue(t tokenType, value string) { func (l *queryLexer) emitWithValue(t tokenType, value string) {
l.tokens <- token{ l.tokens <- token{
Position: Position{l.line, l.col}, Position: toml.Position{Line:l.line, Col:l.col},
typ: t, typ: t,
val: value, val: value,
} }
@@ -91,7 +92,7 @@ func (l *queryLexer) backup() {
func (l *queryLexer) errorf(format string, args ...interface{}) queryLexStateFn { func (l *queryLexer) errorf(format string, args ...interface{}) queryLexStateFn {
l.tokens <- token{ l.tokens <- token{
Position: Position{l.line, l.col}, Position: toml.Position{Line:l.line, Col:l.col},
typ: tokenError, typ: tokenError,
val: fmt.Sprintf(format, args...), val: fmt.Sprintf(format, args...),
} }
+50 -49
View File
@@ -1,7 +1,8 @@
package toml package query
import ( import (
"testing" "testing"
"github.com/pelletier/go-toml"
) )
func testQLFlow(t *testing.T, input string, expectedFlow []token) { func testQLFlow(t *testing.T, input string, expectedFlow []token) {
@@ -36,143 +37,143 @@ func testQLFlow(t *testing.T, input string, expectedFlow []token) {
func TestLexSpecialChars(t *testing.T) { func TestLexSpecialChars(t *testing.T) {
testQLFlow(t, " .$[]..()?*", []token{ testQLFlow(t, " .$[]..()?*", []token{
{Position{1, 2}, tokenDot, "."}, {toml.Position{1, 2}, tokenDot, "."},
{Position{1, 3}, tokenDollar, "$"}, {toml.Position{1, 3}, tokenDollar, "$"},
{Position{1, 4}, tokenLeftBracket, "["}, {toml.Position{1, 4}, tokenLeftBracket, "["},
{Position{1, 5}, tokenRightBracket, "]"}, {toml.Position{1, 5}, tokenRightBracket, "]"},
{Position{1, 6}, tokenDotDot, ".."}, {toml.Position{1, 6}, tokenDotDot, ".."},
{Position{1, 8}, tokenLeftParen, "("}, {toml.Position{1, 8}, tokenLeftParen, "("},
{Position{1, 9}, tokenRightParen, ")"}, {toml.Position{1, 9}, tokenRightParen, ")"},
{Position{1, 10}, tokenQuestion, "?"}, {toml.Position{1, 10}, tokenQuestion, "?"},
{Position{1, 11}, tokenStar, "*"}, {toml.Position{1, 11}, tokenStar, "*"},
{Position{1, 12}, tokenEOF, ""}, {toml.Position{1, 12}, tokenEOF, ""},
}) })
} }
func TestLexString(t *testing.T) { func TestLexString(t *testing.T) {
testQLFlow(t, "'foo\n'", []token{ testQLFlow(t, "'foo\n'", []token{
{Position{1, 2}, tokenString, "foo\n"}, {toml.Position{1, 2}, tokenString, "foo\n"},
{Position{2, 2}, tokenEOF, ""}, {toml.Position{2, 2}, tokenEOF, ""},
}) })
} }
func TestLexDoubleString(t *testing.T) { func TestLexDoubleString(t *testing.T) {
testQLFlow(t, `"bar"`, []token{ testQLFlow(t, `"bar"`, []token{
{Position{1, 2}, tokenString, "bar"}, {toml.Position{1, 2}, tokenString, "bar"},
{Position{1, 6}, tokenEOF, ""}, {toml.Position{1, 6}, tokenEOF, ""},
}) })
} }
func TestLexStringEscapes(t *testing.T) { func TestLexStringEscapes(t *testing.T) {
testQLFlow(t, `"foo \" \' \b \f \/ \t \r \\ \u03A9 \U00012345 \n bar"`, []token{ testQLFlow(t, `"foo \" \' \b \f \/ \t \r \\ \u03A9 \U00012345 \n bar"`, []token{
{Position{1, 2}, tokenString, "foo \" ' \b \f / \t \r \\ \u03A9 \U00012345 \n bar"}, {toml.Position{1, 2}, tokenString, "foo \" ' \b \f / \t \r \\ \u03A9 \U00012345 \n bar"},
{Position{1, 55}, tokenEOF, ""}, {toml.Position{1, 55}, tokenEOF, ""},
}) })
} }
func TestLexStringUnfinishedUnicode4(t *testing.T) { func TestLexStringUnfinishedUnicode4(t *testing.T) {
testQLFlow(t, `"\u000"`, []token{ testQLFlow(t, `"\u000"`, []token{
{Position{1, 2}, tokenError, "unfinished unicode escape"}, {toml.Position{1, 2}, tokenError, "unfinished unicode escape"},
}) })
} }
func TestLexStringUnfinishedUnicode8(t *testing.T) { func TestLexStringUnfinishedUnicode8(t *testing.T) {
testQLFlow(t, `"\U0000"`, []token{ testQLFlow(t, `"\U0000"`, []token{
{Position{1, 2}, tokenError, "unfinished unicode escape"}, {toml.Position{1, 2}, tokenError, "unfinished unicode escape"},
}) })
} }
func TestLexStringInvalidEscape(t *testing.T) { func TestLexStringInvalidEscape(t *testing.T) {
testQLFlow(t, `"\x"`, []token{ testQLFlow(t, `"\x"`, []token{
{Position{1, 2}, tokenError, "invalid escape sequence: \\x"}, {toml.Position{1, 2}, tokenError, "invalid escape sequence: \\x"},
}) })
} }
func TestLexStringUnfinished(t *testing.T) { func TestLexStringUnfinished(t *testing.T) {
testQLFlow(t, `"bar`, []token{ testQLFlow(t, `"bar`, []token{
{Position{1, 2}, tokenError, "unclosed string"}, {toml.Position{1, 2}, tokenError, "unclosed string"},
}) })
} }
func TestLexKey(t *testing.T) { func TestLexKey(t *testing.T) {
testQLFlow(t, "foo", []token{ testQLFlow(t, "foo", []token{
{Position{1, 1}, tokenKey, "foo"}, {toml.Position{1, 1}, tokenKey, "foo"},
{Position{1, 4}, tokenEOF, ""}, {toml.Position{1, 4}, tokenEOF, ""},
}) })
} }
func TestLexRecurse(t *testing.T) { func TestLexRecurse(t *testing.T) {
testQLFlow(t, "$..*", []token{ testQLFlow(t, "$..*", []token{
{Position{1, 1}, tokenDollar, "$"}, {toml.Position{1, 1}, tokenDollar, "$"},
{Position{1, 2}, tokenDotDot, ".."}, {toml.Position{1, 2}, tokenDotDot, ".."},
{Position{1, 4}, tokenStar, "*"}, {toml.Position{1, 4}, tokenStar, "*"},
{Position{1, 5}, tokenEOF, ""}, {toml.Position{1, 5}, tokenEOF, ""},
}) })
} }
func TestLexBracketKey(t *testing.T) { func TestLexBracketKey(t *testing.T) {
testQLFlow(t, "$[foo]", []token{ testQLFlow(t, "$[foo]", []token{
{Position{1, 1}, tokenDollar, "$"}, {toml.Position{1, 1}, tokenDollar, "$"},
{Position{1, 2}, tokenLeftBracket, "["}, {toml.Position{1, 2}, tokenLeftBracket, "["},
{Position{1, 3}, tokenKey, "foo"}, {toml.Position{1, 3}, tokenKey, "foo"},
{Position{1, 6}, tokenRightBracket, "]"}, {toml.Position{1, 6}, tokenRightBracket, "]"},
{Position{1, 7}, tokenEOF, ""}, {toml.Position{1, 7}, tokenEOF, ""},
}) })
} }
func TestLexSpace(t *testing.T) { func TestLexSpace(t *testing.T) {
testQLFlow(t, "foo bar baz", []token{ testQLFlow(t, "foo bar baz", []token{
{Position{1, 1}, tokenKey, "foo"}, {toml.Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenKey, "bar"}, {toml.Position{1, 5}, tokenKey, "bar"},
{Position{1, 9}, tokenKey, "baz"}, {toml.Position{1, 9}, tokenKey, "baz"},
{Position{1, 12}, tokenEOF, ""}, {toml.Position{1, 12}, tokenEOF, ""},
}) })
} }
func TestLexInteger(t *testing.T) { func TestLexInteger(t *testing.T) {
testQLFlow(t, "100 +200 -300", []token{ testQLFlow(t, "100 +200 -300", []token{
{Position{1, 1}, tokenInteger, "100"}, {toml.Position{1, 1}, tokenInteger, "100"},
{Position{1, 5}, tokenInteger, "+200"}, {toml.Position{1, 5}, tokenInteger, "+200"},
{Position{1, 10}, tokenInteger, "-300"}, {toml.Position{1, 10}, tokenInteger, "-300"},
{Position{1, 14}, tokenEOF, ""}, {toml.Position{1, 14}, tokenEOF, ""},
}) })
} }
func TestLexFloat(t *testing.T) { func TestLexFloat(t *testing.T) {
testQLFlow(t, "100.0 +200.0 -300.0", []token{ testQLFlow(t, "100.0 +200.0 -300.0", []token{
{Position{1, 1}, tokenFloat, "100.0"}, {toml.Position{1, 1}, tokenFloat, "100.0"},
{Position{1, 7}, tokenFloat, "+200.0"}, {toml.Position{1, 7}, tokenFloat, "+200.0"},
{Position{1, 14}, tokenFloat, "-300.0"}, {toml.Position{1, 14}, tokenFloat, "-300.0"},
{Position{1, 20}, tokenEOF, ""}, {toml.Position{1, 20}, tokenEOF, ""},
}) })
} }
func TestLexFloatWithMultipleDots(t *testing.T) { func TestLexFloatWithMultipleDots(t *testing.T) {
testQLFlow(t, "4.2.", []token{ testQLFlow(t, "4.2.", []token{
{Position{1, 1}, tokenError, "cannot have two dots in one float"}, {toml.Position{1, 1}, tokenError, "cannot have two dots in one float"},
}) })
} }
func TestLexFloatLeadingDot(t *testing.T) { func TestLexFloatLeadingDot(t *testing.T) {
testQLFlow(t, "+.1", []token{ testQLFlow(t, "+.1", []token{
{Position{1, 1}, tokenError, "cannot start float with a dot"}, {toml.Position{1, 1}, tokenError, "cannot start float with a dot"},
}) })
} }
func TestLexFloatWithTrailingDot(t *testing.T) { func TestLexFloatWithTrailingDot(t *testing.T) {
testQLFlow(t, "42.", []token{ testQLFlow(t, "42.", []token{
{Position{1, 1}, tokenError, "float cannot end with a dot"}, {toml.Position{1, 1}, tokenError, "float cannot end with a dot"},
}) })
} }
func TestLexNumberWithoutDigit(t *testing.T) { func TestLexNumberWithoutDigit(t *testing.T) {
testQLFlow(t, "+", []token{ testQLFlow(t, "+", []token{
{Position{1, 1}, tokenError, "no digit in that number"}, {toml.Position{1, 1}, tokenError, "no digit in that number"},
}) })
} }
func TestLexUnknown(t *testing.T) { func TestLexUnknown(t *testing.T) {
testQLFlow(t, "^", []token{ testQLFlow(t, "^", []token{
{Position{1, 1}, tokenError, "unexpected char: '94'"}, {toml.Position{1, 1}, tokenError, "unexpected char: '94'"},
}) })
} }
+52 -54
View File
@@ -1,27 +1,10 @@
package toml package query
import ( import (
"fmt" "fmt"
"github.com/pelletier/go-toml"
) )
// support function to set positions for tomlValues
// NOTE: this is done to allow ctx.lastPosition to indicate the start of any
// values returned by the query engines
func tomlValueCheck(node interface{}, ctx *queryContext) interface{} {
switch castNode := node.(type) {
case *tomlValue:
ctx.lastPosition = castNode.position
return castNode.value
case []*TomlTree:
if len(castNode) > 0 {
ctx.lastPosition = castNode[0].position
}
return node
default:
return node
}
}
// base match // base match
type matchBase struct { type matchBase struct {
next pathFn next pathFn
@@ -45,15 +28,7 @@ func (f *terminatingFn) setNext(next pathFn) {
} }
func (f *terminatingFn) call(node interface{}, ctx *queryContext) { func (f *terminatingFn) call(node interface{}, ctx *queryContext) {
switch castNode := node.(type) { ctx.result.appendResult(node, ctx.lastPosition)
case *TomlTree:
ctx.result.appendResult(node, castNode.position)
case *tomlValue:
ctx.result.appendResult(node, castNode.position)
default:
// use last position for scalars
ctx.result.appendResult(node, ctx.lastPosition)
}
} }
// match single key // match single key
@@ -67,16 +42,18 @@ func newMatchKeyFn(name string) *matchKeyFn {
} }
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) { func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
if array, ok := node.([]*TomlTree); ok { if array, ok := node.([]*toml.Tree); ok {
for _, tree := range array { for _, tree := range array {
item := tree.values[f.Name] item := tree.Get(f.Name)
if item != nil { if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name)
f.next.call(item, ctx) f.next.call(item, ctx)
} }
} }
} else if tree, ok := node.(*TomlTree); ok { } else if tree, ok := node.(*toml.Tree); ok {
item := tree.values[f.Name] item := tree.Get(f.Name)
if item != nil { if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name)
f.next.call(item, ctx) f.next.call(item, ctx)
} }
} }
@@ -93,8 +70,13 @@ func newMatchIndexFn(idx int) *matchIndexFn {
} }
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 := node.([]interface{}); ok {
if f.Idx < len(arr) && f.Idx >= 0 { if f.Idx < len(arr) && f.Idx >= 0 {
if treesArray, ok := node.([]*toml.Tree); ok {
if len(treesArray) > 0 {
ctx.lastPosition = treesArray[0].Position()
}
}
f.next.call(arr[f.Idx], ctx) f.next.call(arr[f.Idx], ctx)
} }
} }
@@ -111,7 +93,7 @@ func newMatchSliceFn(start, end, step int) *matchSliceFn {
} }
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 := node.([]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
if realStart < 0 { if realStart < 0 {
@@ -125,6 +107,11 @@ 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 {
if treesArray, ok := node.([]*toml.Tree); ok {
if len(treesArray) > 0 {
ctx.lastPosition = treesArray[0].Position()
}
}
f.next.call(arr[idx], ctx) f.next.call(arr[idx], ctx)
} }
} }
@@ -140,8 +127,10 @@ func newMatchAnyFn() *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.(*toml.Tree); ok {
for _, v := range tree.values { for _, k := range tree.Keys() {
v := tree.Get(k)
ctx.lastPosition = tree.GetPosition(k)
f.next.call(v, ctx) f.next.call(v, ctx)
} }
} }
@@ -174,21 +163,25 @@ func newMatchRecursiveFn() *matchRecursiveFn {
} }
func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) { func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*TomlTree); ok { originalPosition := ctx.lastPosition
var visit func(tree *TomlTree) if tree, ok := node.(*toml.Tree); ok {
visit = func(tree *TomlTree) { var visit func(tree *toml.Tree)
for _, v := range tree.values { visit = func(tree *toml.Tree) {
for _, k := range tree.Keys() {
v := tree.Get(k)
ctx.lastPosition = tree.GetPosition(k)
f.next.call(v, ctx) f.next.call(v, ctx)
switch node := v.(type) { switch node := v.(type) {
case *TomlTree: case *toml.Tree:
visit(node) visit(node)
case []*TomlTree: case []*toml.Tree:
for _, subtree := range node { for _, subtree := range node {
visit(subtree) visit(subtree)
} }
} }
} }
} }
ctx.lastPosition = originalPosition
f.next.call(tree, ctx) f.next.call(tree, ctx)
visit(tree) visit(tree)
} }
@@ -197,11 +190,11 @@ func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
// match based on an externally provided functional filter // match based on an externally provided functional filter
type matchFilterFn struct { type matchFilterFn struct {
matchBase matchBase
Pos Position Pos toml.Position
Name string Name string
} }
func newMatchFilterFn(name string, pos Position) *matchFilterFn { func newMatchFilterFn(name string, pos toml.Position) *matchFilterFn {
return &matchFilterFn{Name: name, Pos: pos} return &matchFilterFn{Name: name, Pos: pos}
} }
@@ -211,17 +204,22 @@ func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
panic(fmt.Sprintf("%s: query context does not have filter '%s'", panic(fmt.Sprintf("%s: query context does not have filter '%s'",
f.Pos.String(), f.Name)) f.Pos.String(), f.Name))
} }
switch castNode := tomlValueCheck(node, ctx).(type) { switch castNode := node.(type) {
case *TomlTree: case *toml.Tree:
for _, v := range castNode.values { for _, k := range castNode.Keys() {
if tv, ok := v.(*tomlValue); ok { v := castNode.Get(k)
if fn(tv.value) { if fn(v) {
f.next.call(v, ctx) ctx.lastPosition = castNode.GetPosition(k)
} f.next.call(v, ctx)
} else { }
if fn(v) { }
f.next.call(v, ctx) case []*toml.Tree:
for _, v := range castNode {
if fn(v) {
if len(castNode) > 0 {
ctx.lastPosition = castNode[0].Position()
} }
f.next.call(v, ctx)
} }
} }
case []interface{}: case []interface{}:
+4 -3
View File
@@ -1,8 +1,9 @@
package toml package query
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/pelletier/go-toml"
) )
// dump path tree to a string // dump path tree to a string
@@ -194,8 +195,8 @@ func TestPathFilterExpr(t *testing.T) {
"$[?('foo'),?(bar)]", "$[?('foo'),?(bar)]",
buildPath( buildPath(
&matchUnionFn{[]pathFn{ &matchUnionFn{[]pathFn{
newMatchFilterFn("foo", Position{}), newMatchFilterFn("foo", toml.Position{}),
newMatchFilterFn("bar", Position{}), newMatchFilterFn("bar", toml.Position{}),
}}, }},
)) ))
} }
+1 -1
View File
@@ -5,7 +5,7 @@
https://code.google.com/p/json-path/ https://code.google.com/p/json-path/
*/ */
package toml package query
import ( import (
"fmt" "fmt"
+64 -65
View File
@@ -1,4 +1,4 @@
package toml package query
import ( import (
"fmt" "fmt"
@@ -7,19 +7,18 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/pelletier/go-toml"
) )
type queryTestNode struct { type queryTestNode struct {
value interface{} value interface{}
position Position position toml.Position
} }
func valueString(root interface{}) string { func valueString(root interface{}) string {
result := "" //fmt.Sprintf("%T:", root) result := "" //fmt.Sprintf("%T:", root)
switch node := root.(type) { switch node := root.(type) {
case *tomlValue: case *Result:
return valueString(node.value)
case *QueryResult:
items := []string{} items := []string{}
for i, v := range node.Values() { for i, v := range node.Values() {
items = append(items, fmt.Sprintf("%s:%s", items = append(items, fmt.Sprintf("%s:%s",
@@ -37,7 +36,7 @@ func valueString(root interface{}) string {
} }
sort.Strings(items) sort.Strings(items)
result = "[" + strings.Join(items, ", ") + "]" result = "[" + strings.Join(items, ", ") + "]"
case *TomlTree: case *toml.Tree:
// workaround for unreliable map key ordering // workaround for unreliable map key ordering
items := []string{} items := []string{}
for _, k := range node.Keys() { for _, k := range node.Keys() {
@@ -78,13 +77,13 @@ func assertValue(t *testing.T, result, ref interface{}) {
} }
} }
func assertQueryPositions(t *testing.T, toml, query string, ref []interface{}) { func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) {
tree, err := Load(toml) tree, err := toml.Load(tomlDoc)
if err != nil { if err != nil {
t.Errorf("Non-nil toml parse error: %v", err) t.Errorf("Non-nil toml parse error: %v", err)
return return
} }
q, err := CompileQuery(query) q, err := Compile(query)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@@ -101,7 +100,7 @@ func TestQueryRoot(t *testing.T) {
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(42), "a": int64(42),
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
}) })
} }
@@ -112,7 +111,7 @@ func TestQueryKey(t *testing.T) {
"$.foo.a", "$.foo.a",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(42), Position{2, 1}, int64(42), toml.Position{2, 1},
}, },
}) })
} }
@@ -123,7 +122,7 @@ func TestQueryKeyString(t *testing.T) {
"$.foo['a']", "$.foo['a']",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(42), Position{2, 1}, int64(42), toml.Position{2, 1},
}, },
}) })
} }
@@ -134,7 +133,7 @@ func TestQueryIndex(t *testing.T) {
"$.foo.a[5]", "$.foo.a[5]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(6), Position{2, 1}, int64(6), toml.Position{2, 1},
}, },
}) })
} }
@@ -145,19 +144,19 @@ func TestQuerySliceRange(t *testing.T) {
"$.foo.a[0:5]", "$.foo.a[0:5]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(1), Position{2, 1}, int64(1), toml.Position{2, 1},
}, },
queryTestNode{ queryTestNode{
int64(2), Position{2, 1}, int64(2), toml.Position{2, 1},
}, },
queryTestNode{ queryTestNode{
int64(3), Position{2, 1}, int64(3), toml.Position{2, 1},
}, },
queryTestNode{ queryTestNode{
int64(4), Position{2, 1}, int64(4), toml.Position{2, 1},
}, },
queryTestNode{ queryTestNode{
int64(5), Position{2, 1}, int64(5), toml.Position{2, 1},
}, },
}) })
} }
@@ -168,13 +167,13 @@ func TestQuerySliceStep(t *testing.T) {
"$.foo.a[0:5:2]", "$.foo.a[0:5:2]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(1), Position{2, 1}, int64(1), toml.Position{2, 1},
}, },
queryTestNode{ queryTestNode{
int64(3), Position{2, 1}, int64(3), toml.Position{2, 1},
}, },
queryTestNode{ queryTestNode{
int64(5), Position{2, 1}, int64(5), toml.Position{2, 1},
}, },
}) })
} }
@@ -188,13 +187,13 @@ func TestQueryAny(t *testing.T) {
map[string]interface{}{ map[string]interface{}{
"a": int64(1), "a": int64(1),
"b": int64(2), "b": int64(2),
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(3), "a": int64(3),
"b": int64(4), "b": int64(4),
}, Position{4, 1}, }, toml.Position{4, 1},
}, },
}) })
} }
@@ -207,19 +206,19 @@ func TestQueryUnionSimple(t *testing.T) {
map[string]interface{}{ map[string]interface{}{
"a": int64(1), "a": int64(1),
"b": int64(2), "b": int64(2),
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(3), "a": int64(3),
"b": int64(4), "b": int64(4),
}, Position{4, 1}, }, toml.Position{4, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(5), "a": int64(5),
"b": int64(6), "b": int64(6),
}, Position{7, 1}, }, toml.Position{7, 1},
}, },
}) })
} }
@@ -249,7 +248,7 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(6), "b": int64(6),
}, },
}, },
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
@@ -257,19 +256,19 @@ func TestQueryRecursionAll(t *testing.T) {
"a": int64(1), "a": int64(1),
"b": int64(2), "b": int64(2),
}, },
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(1), "a": int64(1),
"b": int64(2), "b": int64(2),
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{
int64(1), Position{2, 1}, int64(1), toml.Position{2, 1},
}, },
queryTestNode{ queryTestNode{
int64(2), Position{3, 1}, int64(2), toml.Position{3, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
@@ -277,19 +276,19 @@ func TestQueryRecursionAll(t *testing.T) {
"a": int64(3), "a": int64(3),
"b": int64(4), "b": int64(4),
}, },
}, Position{4, 1}, }, toml.Position{4, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(3), "a": int64(3),
"b": int64(4), "b": int64(4),
}, Position{4, 1}, }, toml.Position{4, 1},
}, },
queryTestNode{ queryTestNode{
int64(3), Position{5, 1}, int64(3), toml.Position{5, 1},
}, },
queryTestNode{ queryTestNode{
int64(4), Position{6, 1}, int64(4), toml.Position{6, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
@@ -297,19 +296,19 @@ func TestQueryRecursionAll(t *testing.T) {
"a": int64(5), "a": int64(5),
"b": int64(6), "b": int64(6),
}, },
}, Position{7, 1}, }, toml.Position{7, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(5), "a": int64(5),
"b": int64(6), "b": int64(6),
}, Position{7, 1}, }, toml.Position{7, 1},
}, },
queryTestNode{ queryTestNode{
int64(5), Position{8, 1}, int64(5), toml.Position{8, 1},
}, },
queryTestNode{ queryTestNode{
int64(6), Position{9, 1}, int64(6), toml.Position{9, 1},
}, },
}) })
} }
@@ -325,31 +324,31 @@ func TestQueryRecursionUnionSimple(t *testing.T) {
"a": int64(1), "a": int64(1),
"b": int64(2), "b": int64(2),
}, },
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(3), "a": int64(3),
"b": int64(4), "b": int64(4),
}, Position{4, 1}, }, toml.Position{4, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(1), "a": int64(1),
"b": int64(2), "b": int64(2),
}, Position{1, 1}, }, toml.Position{1, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"a": int64(5), "a": int64(5),
"b": int64(6), "b": int64(6),
}, Position{7, 1}, }, toml.Position{7, 1},
}, },
}) })
} }
func TestQueryFilterFn(t *testing.T) { func TestQueryFilterFn(t *testing.T) {
buff, err := ioutil.ReadFile("example.toml") buff, err := ioutil.ReadFile("../example.toml")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@@ -359,16 +358,16 @@ func TestQueryFilterFn(t *testing.T) {
"$..[?(int)]", "$..[?(int)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
int64(8001), Position{13, 1}, int64(8001), toml.Position{13, 1},
}, },
queryTestNode{ queryTestNode{
int64(8001), Position{13, 1}, int64(8001), toml.Position{13, 1},
}, },
queryTestNode{ queryTestNode{
int64(8002), Position{13, 1}, int64(8002), toml.Position{13, 1},
}, },
queryTestNode{ queryTestNode{
int64(5000), Position{14, 1}, int64(5000), toml.Position{14, 1},
}, },
}) })
@@ -376,32 +375,32 @@ func TestQueryFilterFn(t *testing.T) {
"$..[?(string)]", "$..[?(string)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
"TOML Example", Position{3, 1}, "TOML Example", toml.Position{3, 1},
}, },
queryTestNode{ queryTestNode{
"Tom Preston-Werner", Position{6, 1}, "Tom Preston-Werner", toml.Position{6, 1},
}, },
queryTestNode{ queryTestNode{
"GitHub", Position{7, 1}, "GitHub", toml.Position{7, 1},
}, },
queryTestNode{ queryTestNode{
"GitHub Cofounder & CEO\nLikes tater tots and beer.", "GitHub Cofounder & CEO\nLikes tater tots and beer.",
Position{8, 1}, toml.Position{8, 1},
}, },
queryTestNode{ queryTestNode{
"192.168.1.1", Position{12, 1}, "192.168.1.1", toml.Position{12, 1},
}, },
queryTestNode{ queryTestNode{
"10.0.0.1", Position{21, 3}, "10.0.0.1", toml.Position{21, 3},
}, },
queryTestNode{ queryTestNode{
"eqdc10", Position{22, 3}, "eqdc10", toml.Position{22, 3},
}, },
queryTestNode{ queryTestNode{
"10.0.0.2", Position{25, 3}, "10.0.0.2", toml.Position{25, 3},
}, },
queryTestNode{ queryTestNode{
"eqdc10", Position{26, 3}, "eqdc10", toml.Position{26, 3},
}, },
}) })
@@ -421,7 +420,7 @@ func TestQueryFilterFn(t *testing.T) {
"organization": "GitHub", "organization": "GitHub",
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.", "bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
"dob": tv, "dob": tv,
}, Position{5, 1}, }, toml.Position{5, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
@@ -429,7 +428,7 @@ func TestQueryFilterFn(t *testing.T) {
"ports": []interface{}{int64(8001), int64(8001), int64(8002)}, "ports": []interface{}{int64(8001), int64(8001), int64(8002)},
"connection_max": int64(5000), "connection_max": int64(5000),
"enabled": true, "enabled": true,
}, Position{11, 1}, }, toml.Position{11, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
@@ -441,19 +440,19 @@ func TestQueryFilterFn(t *testing.T) {
"ip": "10.0.0.2", "ip": "10.0.0.2",
"dc": "eqdc10", "dc": "eqdc10",
}, },
}, Position{17, 1}, }, toml.Position{17, 1},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"ip": "10.0.0.1", "ip": "10.0.0.1",
"dc": "eqdc10", "dc": "eqdc10",
}, Position{20, 3}, }, toml.Position{20, 3},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
"ip": "10.0.0.2", "ip": "10.0.0.2",
"dc": "eqdc10", "dc": "eqdc10",
}, Position{24, 3}, }, toml.Position{24, 3},
}, },
queryTestNode{ queryTestNode{
map[string]interface{}{ map[string]interface{}{
@@ -461,7 +460,7 @@ func TestQueryFilterFn(t *testing.T) {
[]interface{}{"gamma", "delta"}, []interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)}, []interface{}{int64(1), int64(2)},
}, },
}, Position{28, 1}, }, toml.Position{28, 1},
}, },
}) })
@@ -469,7 +468,7 @@ func TestQueryFilterFn(t *testing.T) {
"$..[?(time)]", "$..[?(time)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
tv, Position{9, 1}, tv, toml.Position{9, 1},
}, },
}) })
@@ -477,7 +476,7 @@ func TestQueryFilterFn(t *testing.T) {
"$..[?(bool)]", "$..[?(bool)]",
[]interface{}{ []interface{}{
queryTestNode{ queryTestNode{
true, Position{15, 1}, true, toml.Position{15, 1},
}, },
}) })
} }
+34 -29
View File
@@ -1,7 +1,9 @@
package toml package query
import ( import (
"time" "time"
"github.com/pelletier/go-toml"
) )
// NodeFilterFn represents a user-defined filter function, for use with // NodeFilterFn represents a user-defined filter function, for use with
@@ -15,50 +17,43 @@ import (
// to use from multiple goroutines. // to use from multiple goroutines.
type NodeFilterFn func(node interface{}) bool type NodeFilterFn func(node interface{}) bool
// QueryResult is the result of Executing a Query. // Result is the result of Executing a Query.
type QueryResult struct { type Result struct {
items []interface{} items []interface{}
positions []Position positions []toml.Position
} }
// appends a value/position pair to the result set. // appends a value/position pair to the result set.
func (r *QueryResult) appendResult(node interface{}, pos Position) { func (r *Result) appendResult(node interface{}, pos toml.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)
} }
// Values is a set of values within a QueryResult. The order of values is not // Values is a set of values within a Result. The order of values is not
// guaranteed to be in document order, and may be different each time a query is // guaranteed to be in document order, and may be different each time a query is
// executed. // executed.
func (r QueryResult) Values() []interface{} { func (r Result) Values() []interface{} {
values := make([]interface{}, len(r.items)) return r.items
for i, v := range r.items {
o, ok := v.(*tomlValue)
if ok {
values[i] = o.value
} else {
values[i] = v
}
}
return values
} }
// Positions is a set of positions for values within a QueryResult. Each index // Positions is a set of positions for values within a Result. Each index
// in Positions() corresponds to the entry in Value() of the same index. // in Positions() corresponds to the entry in Value() of the same index.
func (r QueryResult) Positions() []Position { func (r Result) Positions() []toml.Position {
return r.positions return r.positions
} }
// runtime context for executing query paths // runtime context for executing query paths
type queryContext struct { type queryContext struct {
result *QueryResult result *Result
filters *map[string]NodeFilterFn filters *map[string]NodeFilterFn
lastPosition Position lastPosition toml.Position
} }
// generic path functor interface // generic path functor interface
type pathFn interface { type pathFn interface {
setNext(next pathFn) setNext(next pathFn)
// it is the caller's responsibility to set the ctx.lastPosition before invoking call()
// node can be one of: *toml.Tree, []*toml.Tree, or a scalar
call(node interface{}, ctx *queryContext) call(node interface{}, ctx *queryContext)
} }
@@ -88,17 +83,17 @@ func (q *Query) appendPath(next pathFn) {
next.setNext(newTerminatingFn()) // init the next functor next.setNext(newTerminatingFn()) // init the next functor
} }
// CompileQuery compiles a TOML path expression. The returned Query can be used // Compile compiles a TOML path expression. The returned Query can be used
// to match elements within a TomlTree and its descendants. // to match elements within a Tree and its descendants. See Execute.
func CompileQuery(path string) (*Query, error) { func Compile(path string) (*Query, error) {
return parseQuery(lexQuery(path)) return parseQuery(lexQuery(path))
} }
// Execute executes a query against a TomlTree, and returns the result of the query. // Execute executes a query against a Tree, and returns the result of the query.
func (q *Query) Execute(tree *TomlTree) *QueryResult { func (q *Query) Execute(tree *toml.Tree) *Result {
result := &QueryResult{ result := &Result{
items: []interface{}{}, items: []interface{}{},
positions: []Position{}, positions: []toml.Position{},
} }
if q.root == nil { if q.root == nil {
result.appendResult(tree, tree.GetPosition("")) result.appendResult(tree, tree.GetPosition(""))
@@ -107,11 +102,21 @@ func (q *Query) Execute(tree *TomlTree) *QueryResult {
result: result, result: result,
filters: q.filters, filters: q.filters,
} }
ctx.lastPosition = tree.Position()
q.root.call(tree, ctx) q.root.call(tree, ctx)
} }
return result return result
} }
// CompileAndExecute is a shorthand for Compile(path) followed by Execute(tree).
func CompileAndExecute(path string, tree *toml.Tree) (*Result, error) {
query, err := Compile(path)
if err != nil {
return nil, err
}
return query.Execute(tree), nil
}
// SetFilter sets a user-defined filter function. These may be used inside // SetFilter sets a user-defined filter function. These may be used inside
// "?(..)" query expressions to filter TOML document elements within a query. // "?(..)" query expressions to filter TOML document elements within a query.
func (q *Query) SetFilter(name string, fn NodeFilterFn) { func (q *Query) SetFilter(name string, fn NodeFilterFn) {
@@ -127,7 +132,7 @@ func (q *Query) SetFilter(name string, fn NodeFilterFn) {
var defaultFilterFunctions = map[string]NodeFilterFn{ var defaultFilterFunctions = map[string]NodeFilterFn{
"tree": func(node interface{}) bool { "tree": func(node interface{}) bool {
_, ok := node.(*TomlTree) _, ok := node.(*toml.Tree)
return ok return ok
}, },
"int": func(node interface{}) bool { "int": func(node interface{}) bool {
+157
View File
@@ -0,0 +1,157 @@
package query
import (
"fmt"
"testing"
"github.com/pelletier/go-toml"
)
func assertArrayContainsInAnyOrder(t *testing.T, array []interface{}, objects ...interface{}) {
if len(array) != len(objects) {
t.Fatalf("array contains %d objects but %d are expected", len(array), len(objects))
}
for _, o := range objects {
found := false
for _, a := range array {
if a == o {
found = true
break
}
}
if !found {
t.Fatal(o, "not found in array", array)
}
}
}
func TestQueryExample(t *testing.T) {
config, _ := toml.Load(`
[[book]]
title = "The Stand"
author = "Stephen King"
[[book]]
title = "For Whom the Bell Tolls"
author = "Ernest Hemmingway"
[[book]]
title = "Neuromancer"
author = "William Gibson"
`)
authors, err := CompileAndExecute("$.book.author", config)
if err != nil {
t.Fatal("unexpected error:", err)
}
names := authors.Values()
if len(names) != 3 {
t.Fatalf("query should return 3 names but returned %d", len(names))
}
assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson")
}
func TestQueryReadmeExample(t *testing.T) {
config, _ := toml.Load(`
[postgres]
user = "pelletier"
password = "mypassword"
`)
query, err := Compile("$..[user,password]")
if err != nil {
t.Fatal("unexpected error:", err)
}
results := query.Execute(config)
values := results.Values()
if len(values) != 2 {
t.Fatalf("query should return 2 values but returned %d", len(values))
}
assertArrayContainsInAnyOrder(t, values, "pelletier", "mypassword")
}
func TestQueryPathNotPresent(t *testing.T) {
config, _ := toml.Load(`a = "hello"`)
query, err := Compile("$.foo.bar")
if err != nil {
t.Fatal("unexpected error:", err)
}
results := query.Execute(config)
if err != nil {
t.Fatalf("err should be nil. got %s instead", err)
}
if len(results.items) != 0 {
t.Fatalf("no items should be matched. %d matched instead", len(results.items))
}
}
func ExampleNodeFilterFn_filterExample() {
tree, _ := toml.Load(`
[struct_one]
foo = "foo"
bar = "bar"
[struct_two]
baz = "baz"
gorf = "gorf"
`)
// create a query that references a user-defined-filter
query, _ := Compile("$[?(bazOnly)]")
// define the filter, and assign it to the query
query.SetFilter("bazOnly", func(node interface{}) bool {
if tree, ok := node.(*toml.Tree); ok {
return tree.Has("baz")
}
return false // reject all other node types
})
// results contain only the 'struct_two' Tree
query.Execute(tree)
}
func ExampleQuery_queryExample() {
config, _ := toml.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
query, _ := Compile("$.book.author")
authors := query.Execute(config)
for _, name := range authors.Values() {
fmt.Println(name)
}
}
func TestTomlQuery(t *testing.T) {
tree, err := toml.Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6")
if err != nil {
t.Error(err)
return
}
query, err := Compile("$.foo.bar")
if err != nil {
t.Error(err)
return
}
result := query.Execute(tree)
values := result.Values()
if len(values) != 1 {
t.Errorf("Expected resultset of 1, got %d instead: %v", len(values), values)
}
if tt, ok := values[0].(*toml.Tree); !ok {
t.Errorf("Expected type of Tree: %T", values[0])
} else if tt.Get("a") != int64(1) {
t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a"))
} else if tt.Get("b") != int64(2) {
t.Errorf("Expected 'b' with a value 2: %v", tt.Get("b"))
}
}
+107
View File
@@ -0,0 +1,107 @@
package query
import (
"fmt"
"strconv"
"unicode"
"github.com/pelletier/go-toml"
)
// Define tokens
type tokenType int
const (
eof = -(iota + 1)
)
const (
tokenError tokenType = iota
tokenEOF
tokenKey
tokenString
tokenInteger
tokenFloat
tokenLeftBracket
tokenRightBracket
tokenLeftParen
tokenRightParen
tokenComma
tokenColon
tokenDollar
tokenStar
tokenQuestion
tokenDot
tokenDotDot
)
var tokenTypeNames = []string{
"Error",
"EOF",
"Key",
"String",
"Integer",
"Float",
"[",
"]",
"(",
")",
",",
":",
"$",
"*",
"?",
".",
"..",
}
type token struct {
toml.Position
typ tokenType
val string
}
func (tt tokenType) String() string {
idx := int(tt)
if idx < len(tokenTypeNames) {
return tokenTypeNames[idx]
}
return "Unknown"
}
func (t token) Int() int {
if result, err := strconv.Atoi(t.val); err != nil {
panic(err)
} else {
return result
}
}
func (t token) String() string {
switch t.typ {
case tokenEOF:
return "EOF"
case tokenError:
return t.val
}
return fmt.Sprintf("%q", t.val)
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_'
}
func isDigit(r rune) bool {
return unicode.IsNumber(r)
}
func isHexDigit(r rune) bool {
return isDigit(r) ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
-70
View File
@@ -1,70 +0,0 @@
package toml
import (
"testing"
)
func assertArrayContainsInAnyOrder(t *testing.T, array []interface{}, objects ...interface{}) {
if len(array) != len(objects) {
t.Fatalf("array contains %d objects but %d are expected", len(array), len(objects))
}
for _, o := range objects {
found := false
for _, a := range array {
if a == o {
found = true
break
}
}
if !found {
t.Fatal(o, "not found in array", array)
}
}
}
func TestQueryExample(t *testing.T) {
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"
`)
authors, _ := config.Query("$.book.author")
names := authors.Values()
if len(names) != 3 {
t.Fatalf("query should return 3 names but returned %d", len(names))
}
assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson")
}
func TestQueryReadmeExample(t *testing.T) {
config, _ := Load(`
[postgres]
user = "pelletier"
password = "mypassword"
`)
results, _ := config.Query("$..[user,password]")
values := results.Values()
if len(values) != 2 {
t.Fatalf("query should return 2 values but returned %d", len(values))
}
assertArrayContainsInAnyOrder(t, values, "pelletier", "mypassword")
}
func TestQueryPathNotPresent(t *testing.T) {
config, _ := Load(`a = "hello"`)
results, err := config.Query("$.foo.bar")
if err != nil {
t.Fatalf("err should be nil. got %s instead", err)
}
if len(results.items) != 0 {
t.Fatalf("no items should be matched. %d matched instead", len(results.items))
}
}
+7 -1
View File
@@ -19,6 +19,9 @@ function git_clone() {
popd popd
} }
# Remove potential previous runs
rm -rf src test_program_bin toml-test
# Run go vet # Run go vet
go vet ./... go vet ./...
@@ -36,13 +39,16 @@ go build -o toml-test github.com/BurntSushi/toml-test
# vendorize the current lib for testing # vendorize the current lib for testing
# NOTE: this basically mocks an install without having to go back out to github for code # NOTE: this basically mocks an install without having to go back out to github for code
mkdir -p src/github.com/pelletier/go-toml/cmd mkdir -p src/github.com/pelletier/go-toml/cmd
mkdir -p src/github.com/pelletier/go-toml/query
cp *.go *.toml src/github.com/pelletier/go-toml cp *.go *.toml src/github.com/pelletier/go-toml
cp -R cmd/* src/github.com/pelletier/go-toml/cmd cp -R cmd/* src/github.com/pelletier/go-toml/cmd
cp -R query/* src/github.com/pelletier/go-toml/query
go build -o test_program_bin src/github.com/pelletier/go-toml/cmd/test_program.go go build -o test_program_bin src/github.com/pelletier/go-toml/cmd/test_program.go
# Run basic unit tests # Run basic unit tests
go test github.com/pelletier/go-toml -v -covermode=count -coverprofile=coverage.out go test github.com/pelletier/go-toml -covermode=count -coverprofile=coverage.out
go test github.com/pelletier/go-toml/cmd/tomljson go test github.com/pelletier/go-toml/cmd/tomljson
go test github.com/pelletier/go-toml/query
# run the entire BurntSushi test suite # run the entire BurntSushi test suite
if [[ $# -eq 0 ]] ; then if [[ $# -eq 0 ]] ; then
+2 -1
View File
@@ -135,5 +135,6 @@ func isDigit(r rune) bool {
func isHexDigit(r rune) bool { func isHexDigit(r rune) bool {
return isDigit(r) || return isDigit(r) ||
r == 'A' || r == 'B' || r == 'C' || r == 'D' || r == 'E' || r == 'F' (r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
} }
+56 -57
View File
@@ -14,28 +14,35 @@ type tomlValue struct {
position Position position Position
} }
// TomlTree is the result of the parsing of a TOML file. // Tree is the result of the parsing of a TOML file.
type TomlTree struct { type Tree struct {
values map[string]interface{} // string -> *tomlValue, *TomlTree, []*TomlTree values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree
position Position position Position
} }
func newTomlTree() *TomlTree { func newTree() *Tree {
return &TomlTree{ return &Tree{
values: make(map[string]interface{}), values: make(map[string]interface{}),
position: Position{}, position: Position{},
} }
} }
// TreeFromMap initializes a new TomlTree object using the given map. // TreeFromMap initializes a new Tree object using the given map.
func TreeFromMap(m map[string]interface{}) *TomlTree { func TreeFromMap(m map[string]interface{}) (*Tree, error) {
return &TomlTree{ result, err := toTree(m)
values: m, if err != nil {
return nil, err
} }
return result.(*Tree), nil
}
// Position returns the position of the tree.
func (t *Tree) Position() Position {
return t.position
} }
// Has returns a boolean indicating if the given key exists. // Has returns a boolean indicating if the given key exists.
func (t *TomlTree) Has(key string) bool { func (t *Tree) Has(key string) bool {
if key == "" { if key == "" {
return false return false
} }
@@ -43,25 +50,26 @@ func (t *TomlTree) Has(key string) bool {
} }
// HasPath returns true if the given path of keys exists, false otherwise. // HasPath returns true if the given path of keys exists, false otherwise.
func (t *TomlTree) HasPath(keys []string) bool { func (t *Tree) HasPath(keys []string) bool {
return t.GetPath(keys) != nil return t.GetPath(keys) != nil
} }
// Keys returns the keys of the toplevel tree. // Keys returns the keys of the toplevel tree (does not recurse).
// Warning: this is a costly operation. func (t *Tree) Keys() []string {
func (t *TomlTree) Keys() []string { keys := make([]string, len(t.values))
var keys []string i := 0
for k := range t.values { for k := range t.values {
keys = append(keys, k) keys[i] = k
i++
} }
return keys return keys
} }
// Get the value at key in the TomlTree. // Get the value at key in the Tree.
// Key is a dot-separated path (e.g. a.b.c). // Key is a dot-separated path (e.g. a.b.c).
// Returns nil if the path does not exist in the tree. // Returns nil if the path does not exist in the tree.
// If keys is of length zero, the current tree is returned. // If keys is of length zero, the current tree is returned.
func (t *TomlTree) Get(key string) interface{} { func (t *Tree) Get(key string) interface{} {
if key == "" { if key == "" {
return t return t
} }
@@ -74,7 +82,7 @@ func (t *TomlTree) Get(key string) interface{} {
// GetPath returns the element in the tree indicated by 'keys'. // GetPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned. // If keys is of length zero, the current tree is returned.
func (t *TomlTree) GetPath(keys []string) interface{} { func (t *Tree) GetPath(keys []string) interface{} {
if len(keys) == 0 { if len(keys) == 0 {
return t return t
} }
@@ -85,9 +93,9 @@ func (t *TomlTree) GetPath(keys []string) interface{} {
return nil return nil
} }
switch node := value.(type) { switch node := value.(type) {
case *TomlTree: case *Tree:
subtree = node subtree = node
case []*TomlTree: case []*Tree:
// go to most recent element // go to most recent element
if len(node) == 0 { if len(node) == 0 {
return nil return nil
@@ -107,7 +115,7 @@ func (t *TomlTree) GetPath(keys []string) interface{} {
} }
// GetPosition returns the position of the given key. // GetPosition returns the position of the given key.
func (t *TomlTree) GetPosition(key string) Position { func (t *Tree) GetPosition(key string) Position {
if key == "" { if key == "" {
return t.position return t.position
} }
@@ -116,7 +124,7 @@ func (t *TomlTree) GetPosition(key string) Position {
// GetPositionPath returns the element in the tree indicated by 'keys'. // GetPositionPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned. // If keys is of length zero, the current tree is returned.
func (t *TomlTree) GetPositionPath(keys []string) Position { func (t *Tree) GetPositionPath(keys []string) Position {
if len(keys) == 0 { if len(keys) == 0 {
return t.position return t.position
} }
@@ -127,9 +135,9 @@ func (t *TomlTree) GetPositionPath(keys []string) Position {
return Position{0, 0} return Position{0, 0}
} }
switch node := value.(type) { switch node := value.(type) {
case *TomlTree: case *Tree:
subtree = node subtree = node
case []*TomlTree: case []*Tree:
// go to most recent element // go to most recent element
if len(node) == 0 { if len(node) == 0 {
return Position{0, 0} return Position{0, 0}
@@ -143,9 +151,9 @@ func (t *TomlTree) GetPositionPath(keys []string) Position {
switch node := subtree.values[keys[len(keys)-1]].(type) { switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue: case *tomlValue:
return node.position return node.position
case *TomlTree: case *Tree:
return node.position return node.position
case []*TomlTree: case []*Tree:
// go to most recent element // go to most recent element
if len(node) == 0 { if len(node) == 0 {
return Position{0, 0} return Position{0, 0}
@@ -157,7 +165,7 @@ func (t *TomlTree) GetPositionPath(keys []string) Position {
} }
// GetDefault works like Get but with a default value // GetDefault works like Get but with a default value
func (t *TomlTree) GetDefault(key string, def interface{}) interface{} { func (t *Tree) GetDefault(key string, def interface{}) interface{} {
val := t.Get(key) val := t.Get(key)
if val == nil { if val == nil {
return def return def
@@ -167,30 +175,30 @@ func (t *TomlTree) GetDefault(key string, def interface{}) interface{} {
// Set an element in the tree. // Set an element in the tree.
// Key is a dot-separated path (e.g. a.b.c). // Key is a dot-separated path (e.g. a.b.c).
// Creates all necessary intermediates trees, if needed. // Creates all necessary intermediate trees, if needed.
func (t *TomlTree) Set(key string, value interface{}) { func (t *Tree) Set(key string, value interface{}) {
t.SetPath(strings.Split(key, "."), value) t.SetPath(strings.Split(key, "."), value)
} }
// SetPath sets an element in the tree. // SetPath sets an element in the tree.
// Keys is an array of path elements (e.g. {"a","b","c"}). // Keys is an array of path elements (e.g. {"a","b","c"}).
// Creates all necessary intermediates trees, if needed. // Creates all necessary intermediate trees, if needed.
func (t *TomlTree) SetPath(keys []string, value interface{}) { func (t *Tree) SetPath(keys []string, value interface{}) {
subtree := t subtree := t
for _, intermediateKey := range keys[:len(keys)-1] { for _, intermediateKey := range keys[:len(keys)-1] {
nextTree, exists := subtree.values[intermediateKey] nextTree, exists := subtree.values[intermediateKey]
if !exists { if !exists {
nextTree = newTomlTree() nextTree = newTree()
subtree.values[intermediateKey] = nextTree // add new element here subtree.values[intermediateKey] = nextTree // add new element here
} }
switch node := nextTree.(type) { switch node := nextTree.(type) {
case *TomlTree: case *Tree:
subtree = node subtree = node
case []*TomlTree: case []*Tree:
// go to most recent element // go to most recent element
if len(node) == 0 { if len(node) == 0 {
// create element if it does not exist // create element if it does not exist
subtree.values[intermediateKey] = append(node, newTomlTree()) subtree.values[intermediateKey] = append(node, newTree())
} }
subtree = node[len(node)-1] subtree = node[len(node)-1]
} }
@@ -199,9 +207,9 @@ func (t *TomlTree) SetPath(keys []string, value interface{}) {
var toInsert interface{} var toInsert interface{}
switch value.(type) { switch value.(type) {
case *TomlTree: case *Tree:
toInsert = value toInsert = value
case []*TomlTree: case []*Tree:
toInsert = value toInsert = value
case *tomlValue: case *tomlValue:
toInsert = value toInsert = value
@@ -219,21 +227,21 @@ func (t *TomlTree) SetPath(keys []string, value interface{}) {
// and tree[a][b][c] // and tree[a][b][c]
// //
// Returns nil on success, error object on failure // Returns nil on success, error object on failure
func (t *TomlTree) createSubTree(keys []string, pos Position) error { func (t *Tree) createSubTree(keys []string, pos Position) error {
subtree := t subtree := t
for _, intermediateKey := range keys { for _, intermediateKey := range keys {
nextTree, exists := subtree.values[intermediateKey] nextTree, exists := subtree.values[intermediateKey]
if !exists { if !exists {
tree := newTomlTree() tree := newTree()
tree.position = pos tree.position = pos
subtree.values[intermediateKey] = tree subtree.values[intermediateKey] = tree
nextTree = tree nextTree = tree
} }
switch node := nextTree.(type) { switch node := nextTree.(type) {
case []*TomlTree: case []*Tree:
subtree = node[len(node)-1] subtree = node[len(node)-1]
case *TomlTree: case *Tree:
subtree = node subtree = node
default: default:
return fmt.Errorf("unknown type for path %s (%s): %T (%#v)", return fmt.Errorf("unknown type for path %s (%s): %T (%#v)",
@@ -243,17 +251,8 @@ func (t *TomlTree) createSubTree(keys []string, pos Position) error {
return nil return nil
} }
// Query compiles and executes a query on a tree and returns the query result. // LoadReader creates a Tree from any io.Reader.
func (t *TomlTree) Query(query string) (*QueryResult, error) { func LoadReader(reader io.Reader) (tree *Tree, err error) {
q, err := CompileQuery(query)
if err != nil {
return nil, err
}
return q.Execute(t), nil
}
// LoadReader creates a TomlTree from any io.Reader.
func LoadReader(reader io.Reader) (tree *TomlTree, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok { if _, ok := r.(runtime.Error); ok {
@@ -266,13 +265,13 @@ func LoadReader(reader io.Reader) (tree *TomlTree, err error) {
return return
} }
// Load creates a TomlTree from a string. // Load creates a Tree from a string.
func Load(content string) (tree *TomlTree, err error) { func Load(content string) (tree *Tree, err error) {
return LoadReader(strings.NewReader(content)) return LoadReader(strings.NewReader(content))
} }
// LoadFile creates a TomlTree from a file. // LoadFile creates a Tree from a file.
func LoadFile(path string) (tree *TomlTree, err error) { func LoadFile(path string) (tree *Tree, err error) {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return nil, err return nil, err
+7 -29
View File
@@ -70,12 +70,12 @@ func TestTomlHasPath(t *testing.T) {
} }
func TestTomlGetPath(t *testing.T) { func TestTomlGetPath(t *testing.T) {
node := newTomlTree() node := newTree()
//TODO: set other node data //TODO: set other node data
for idx, item := range []struct { for idx, item := range []struct {
Path []string Path []string
Expected *TomlTree Expected *Tree
}{ }{
{ // empty path test { // empty path test
[]string{}, []string{},
@@ -94,35 +94,13 @@ func TestTomlGetPath(t *testing.T) {
} }
} }
func TestTomlQuery(t *testing.T) {
tree, err := Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6")
if err != nil {
t.Error(err)
return
}
result, err := tree.Query("$.foo.bar")
if err != nil {
t.Error(err)
return
}
values := result.Values()
if len(values) != 1 {
t.Errorf("Expected resultset of 1, got %d instead: %v", len(values), values)
}
if tt, ok := values[0].(*TomlTree); !ok {
t.Errorf("Expected type of TomlTree: %T", values[0])
} else if tt.Get("a") != int64(1) {
t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a"))
} else if tt.Get("b") != int64(2) {
t.Errorf("Expected 'b' with a value 2: %v", tt.Get("b"))
}
}
func TestTomlFromMap(t *testing.T) { func TestTomlFromMap(t *testing.T) {
simpleMap := map[string]interface{}{"hello": 42} simpleMap := map[string]interface{}{"hello": 42}
tree := TreeFromMap(simpleMap) tree, err := TreeFromMap(simpleMap)
if tree.Get("hello") != 42 { if err != nil {
t.Fatal("unexpected error:", err)
}
if tree.Get("hello") != int64(42) {
t.Fatal("hello should be 42, not", tree.Get("hello")) t.Fatal("hello should be 42, not", tree.Get("hello"))
} }
} }
+142
View File
@@ -0,0 +1,142 @@
package toml
import (
"fmt"
"reflect"
"time"
)
var kindToType = [reflect.String + 1]reflect.Type{
reflect.Bool: reflect.TypeOf(true),
reflect.String: reflect.TypeOf(""),
reflect.Float32: reflect.TypeOf(float64(1)),
reflect.Float64: reflect.TypeOf(float64(1)),
reflect.Int: reflect.TypeOf(int64(1)),
reflect.Int8: reflect.TypeOf(int64(1)),
reflect.Int16: reflect.TypeOf(int64(1)),
reflect.Int32: reflect.TypeOf(int64(1)),
reflect.Int64: reflect.TypeOf(int64(1)),
reflect.Uint: reflect.TypeOf(uint64(1)),
reflect.Uint8: reflect.TypeOf(uint64(1)),
reflect.Uint16: reflect.TypeOf(uint64(1)),
reflect.Uint32: reflect.TypeOf(uint64(1)),
reflect.Uint64: reflect.TypeOf(uint64(1)),
}
// typeFor returns a reflect.Type for a reflect.Kind, or nil if none is found.
// supported values:
// string, bool, int64, uint64, float64, time.Time, int, int8, int16, int32, uint, uint8, uint16, uint32, float32
func typeFor(k reflect.Kind) reflect.Type {
if k > 0 && int(k) < len(kindToType) {
return kindToType[k]
}
return nil
}
func simpleValueCoercion(object interface{}) (interface{}, error) {
switch original := object.(type) {
case string, bool, int64, uint64, float64, time.Time:
return original, nil
case int:
return int64(original), nil
case int8:
return int64(original), nil
case int16:
return int64(original), nil
case int32:
return int64(original), nil
case uint:
return uint64(original), nil
case uint8:
return uint64(original), nil
case uint16:
return uint64(original), nil
case uint32:
return uint64(original), nil
case float32:
return float64(original), nil
case fmt.Stringer:
return original.String(), nil
default:
return nil, fmt.Errorf("cannot convert type %T to Tree", object)
}
}
func sliceToTree(object interface{}) (interface{}, error) {
// arrays are a bit tricky, since they can represent either a
// collection of simple values, which is represented by one
// *tomlValue, or an array of tables, which is represented by an
// array of *Tree.
// holding the assumption that this function is called from toTree only when value.Kind() is Array or Slice
value := reflect.ValueOf(object)
insideType := value.Type().Elem()
length := value.Len()
if length > 0 {
insideType = reflect.ValueOf(value.Index(0).Interface()).Type()
}
if insideType.Kind() == reflect.Map {
// this is considered as an array of tables
tablesArray := make([]*Tree, 0, length)
for i := 0; i < length; i++ {
table := value.Index(i)
tree, err := toTree(table.Interface())
if err != nil {
return nil, err
}
tablesArray = append(tablesArray, tree.(*Tree))
}
return tablesArray, nil
}
sliceType := typeFor(insideType.Kind())
if sliceType == nil {
sliceType = insideType
}
arrayValue := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, length)
for i := 0; i < length; i++ {
val := value.Index(i).Interface()
simpleValue, err := simpleValueCoercion(val)
if err != nil {
return nil, err
}
arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue))
}
return &tomlValue{arrayValue.Interface(), Position{}}, nil
}
func toTree(object interface{}) (interface{}, error) {
value := reflect.ValueOf(object)
if value.Kind() == reflect.Map {
values := map[string]interface{}{}
keys := value.MapKeys()
for _, key := range keys {
if key.Kind() != reflect.String {
if _, ok := key.Interface().(string); !ok {
return nil, fmt.Errorf("map key needs to be a string, not %T (%v)", key.Interface(), key.Kind())
}
}
v := value.MapIndex(key)
newValue, err := toTree(v.Interface())
if err != nil {
return nil, err
}
values[key.String()] = newValue
}
return &Tree{values, Position{}}, nil
}
if value.Kind() == reflect.Array || value.Kind() == reflect.Slice {
return sliceToTree(object)
}
simpleValue, err := simpleValueCoercion(object)
if err != nil {
return nil, err
}
return &tomlValue{simpleValue, Position{}}, nil
}
+126
View File
@@ -0,0 +1,126 @@
package toml
import (
"strconv"
"testing"
"time"
)
type customString string
type stringer struct{}
func (s stringer) String() string {
return "stringer"
}
func validate(t *testing.T, path string, object interface{}) {
switch o := object.(type) {
case *Tree:
for key, tree := range o.values {
validate(t, path+"."+key, tree)
}
case []*Tree:
for index, tree := range o {
validate(t, path+"."+strconv.Itoa(index), tree)
}
case *tomlValue:
switch o.value.(type) {
case int64, uint64, bool, string, float64, time.Time,
[]int64, []uint64, []bool, []string, []float64, []time.Time:
default:
t.Fatalf("tomlValue at key %s containing incorrect type %T", path, o.value)
}
default:
t.Fatalf("value at key %s is of incorrect type %T", path, object)
}
t.Logf("validation ok %s as %T", path, object)
}
func validateTree(t *testing.T, tree *Tree) {
validate(t, "", tree)
}
func TestTreeCreateToTree(t *testing.T) {
data := map[string]interface{}{
"a_string": "bar",
"an_int": 42,
"time": time.Now(),
"int8": int8(2),
"int16": int16(2),
"int32": int32(2),
"uint8": uint8(2),
"uint16": uint16(2),
"uint32": uint32(2),
"float32": float32(2),
"a_bool": false,
"stringer": stringer{},
"nested": map[string]interface{}{
"foo": "bar",
},
"array": []string{"a", "b", "c"},
"array_uint": []uint{uint(1), uint(2)},
"array_table": []map[string]interface{}{map[string]interface{}{"sub_map": 52}},
"array_times": []time.Time{time.Now(), time.Now()},
"map_times": map[string]time.Time{"now": time.Now()},
"custom_string_map_key": map[customString]interface{}{customString("custom"): "custom"},
}
tree, err := TreeFromMap(data)
if err != nil {
t.Fatal("unexpected error:", err)
}
validateTree(t, tree)
}
func TestTreeCreateToTreeInvalidLeafType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": t})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidMapKeyType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": map[int]interface{}{2: 1}})
expected := "map key needs to be a string, not int (int)"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidArrayMemberType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []*testing.T{t}})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidTableGroupType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"hello": t}}})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestRoundTripArrayOfTables(t *testing.T) {
orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\",\"b\"]\n"
tree, err := Load(orig)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := orig
got := tree.String()
if got != want {
t.Errorf("want:\n%s\ngot:\n%s", want, got)
}
}
+42 -37
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"reflect"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -51,6 +52,9 @@ func tomlValueStringRepresentation(v interface{}) (string, error) {
return strconv.FormatFloat(value, 'f', -1, 32), nil return strconv.FormatFloat(value, 'f', -1, 32), nil
case string: case string:
return "\"" + encodeTomlString(value) + "\"", nil return "\"" + encodeTomlString(value) + "\"", nil
case []byte:
b, _ := v.([]byte)
return tomlValueStringRepresentation(string(b))
case bool: case bool:
if value { if value {
return "true", nil return "true", nil
@@ -60,9 +64,14 @@ func tomlValueStringRepresentation(v interface{}) (string, error) {
return value.Format(time.RFC3339), nil return value.Format(time.RFC3339), nil
case nil: case nil:
return "", nil return "", nil
case []interface{}: }
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice {
values := []string{} values := []string{}
for _, item := range value { for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface()
itemRepr, err := tomlValueStringRepresentation(item) itemRepr, err := tomlValueStringRepresentation(item)
if err != nil { if err != nil {
return "", err return "", err
@@ -70,19 +79,18 @@ func tomlValueStringRepresentation(v interface{}) (string, error) {
values = append(values, itemRepr) values = append(values, itemRepr)
} }
return "[" + strings.Join(values, ",") + "]", nil return "[" + strings.Join(values, ",") + "]", nil
default:
return "", fmt.Errorf("unsupported value type %T: %v", value, value)
} }
return "", fmt.Errorf("unsupported value type %T: %v", v, v)
} }
func (t *TomlTree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64) (int64, error) { func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64) (int64, error) {
simpleValuesKeys := make([]string, 0) simpleValuesKeys := make([]string, 0)
complexValuesKeys := make([]string, 0) complexValuesKeys := make([]string, 0)
for k := range t.values { for k := range t.values {
v := t.values[k] v := t.values[k]
switch v.(type) { switch v.(type) {
case *TomlTree, []*TomlTree: case *Tree, []*Tree:
complexValuesKeys = append(complexValuesKeys, k) complexValuesKeys = append(complexValuesKeys, k)
default: default:
simpleValuesKeys = append(simpleValuesKeys, k) simpleValuesKeys = append(simpleValuesKeys, k)
@@ -95,7 +103,7 @@ func (t *TomlTree) writeTo(w io.Writer, indent, keyspace string, bytesCount int6
for _, k := range simpleValuesKeys { for _, k := range simpleValuesKeys {
v, ok := t.values[k].(*tomlValue) v, ok := t.values[k].(*tomlValue)
if !ok { if !ok {
return bytesCount, fmt.Errorf("invalid key type at %s: %T", k, t.values[k]) return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
} }
repr, err := tomlValueStringRepresentation(v.value) repr, err := tomlValueStringRepresentation(v.value)
@@ -121,7 +129,7 @@ func (t *TomlTree) writeTo(w io.Writer, indent, keyspace string, bytesCount int6
switch node := v.(type) { switch node := v.(type) {
// node has to be of those two types given how keys are sorted above // node has to be of those two types given how keys are sorted above
case *TomlTree: case *Tree:
tableName := fmt.Sprintf("\n%s[%s]\n", indent, combinedKey) tableName := fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
writtenBytesCount, err := w.Write([]byte(tableName)) writtenBytesCount, err := w.Write([]byte(tableName))
bytesCount += int64(writtenBytesCount) bytesCount += int64(writtenBytesCount)
@@ -132,20 +140,18 @@ func (t *TomlTree) writeTo(w io.Writer, indent, keyspace string, bytesCount int6
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
case []*TomlTree: case []*Tree:
for _, subTree := range node { for _, subTree := range node {
if len(subTree.values) > 0 { tableArrayName := fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey)
tableArrayName := fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey) writtenBytesCount, err := w.Write([]byte(tableArrayName))
writtenBytesCount, err := w.Write([]byte(tableArrayName)) bytesCount += int64(writtenBytesCount)
bytesCount += int64(writtenBytesCount) if err != nil {
if err != nil { return bytesCount, err
return bytesCount, err }
}
bytesCount, err = subTree.writeTo(w, indent+" ", combinedKey, bytesCount) bytesCount, err = subTree.writeTo(w, indent+" ", combinedKey, bytesCount)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
}
} }
} }
} }
@@ -154,16 +160,16 @@ func (t *TomlTree) writeTo(w io.Writer, indent, keyspace string, bytesCount int6
return bytesCount, nil return bytesCount, nil
} }
// WriteTo encode the TomlTree as Toml and writes it to the writer w. // WriteTo encode the Tree as Toml and writes it to the writer w.
// Returns the number of bytes written in case of success, or an error if anything happened. // Returns the number of bytes written in case of success, or an error if anything happened.
func (t *TomlTree) WriteTo(w io.Writer) (int64, error) { func (t *Tree) WriteTo(w io.Writer) (int64, error) {
return t.writeTo(w, "", "", 0) return t.writeTo(w, "", "", 0)
} }
// ToTomlString generates a human-readable representation of the current tree. // ToTomlString generates a human-readable representation of the current tree.
// Output spans multiple lines, and is suitable for ingest by a TOML parser. // Output spans multiple lines, and is suitable for ingest by a TOML parser.
// If the conversion cannot be performed, ToString returns a non-nil error. // If the conversion cannot be performed, ToString returns a non-nil error.
func (t *TomlTree) ToTomlString() (string, error) { func (t *Tree) ToTomlString() (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
_, err := t.WriteTo(&buf) _, err := t.WriteTo(&buf)
if err != nil { if err != nil {
@@ -174,36 +180,35 @@ func (t *TomlTree) ToTomlString() (string, error) {
// String generates a human-readable representation of the current tree. // String generates a human-readable representation of the current tree.
// Alias of ToString. Present to implement the fmt.Stringer interface. // Alias of ToString. Present to implement the fmt.Stringer interface.
func (t *TomlTree) String() string { func (t *Tree) String() string {
result, _ := t.ToTomlString() result, _ := t.ToTomlString()
return result return result
} }
// ToMap recursively generates a representation of the tree using Go built-in structures. // ToMap recursively generates a representation of the tree using Go built-in structures.
// The following types are used: // The following types are used:
// * uint64 //
// * int64 // * bool
// * bool // * float64
// * string // * int64
// * time.Time // * string
// * map[string]interface{} (where interface{} is any of this list) // * uint64
// * []interface{} (where interface{} is any of this list) // * time.Time
func (t *TomlTree) ToMap() map[string]interface{} { // * map[string]interface{} (where interface{} is any of this list)
// * []interface{} (where interface{} is any of this list)
func (t *Tree) ToMap() map[string]interface{} {
result := map[string]interface{}{} result := map[string]interface{}{}
for k, v := range t.values { for k, v := range t.values {
switch node := v.(type) { switch node := v.(type) {
case []*TomlTree: case []*Tree:
var array []interface{} var array []interface{}
for _, item := range node { for _, item := range node {
array = append(array, item.ToMap()) array = append(array, item.ToMap())
} }
result[k] = array result[k] = array
case *TomlTree: case *Tree:
result[k] = node.ToMap() result[k] = node.ToMap()
case map[string]interface{}:
sub := TreeFromMap(node)
result[k] = sub.ToMap()
case *tomlValue: case *tomlValue:
result[k] = node.value result[k] = node.value
} }
+41 -30
View File
@@ -40,7 +40,31 @@ func assertErrorString(t *testing.T, expected string, err error) {
} }
} }
func TestTomlTreeWriteToTomlString(t *testing.T) { func TestTreeWriteToEmptyTable(t *testing.T) {
doc := `[[empty-tables]]
[[empty-tables]]`
toml, err := Load(doc)
if err != nil {
t.Fatal("Unexpected Load error:", err)
}
tomlString, err := toml.ToTomlString()
if err != nil {
t.Fatal("Unexpected ToTomlString error:", err)
}
expected := `
[[empty-tables]]
[[empty-tables]]
`
if tomlString != expected {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, tomlString)
}
}
func TestTreeWriteToTomlString(t *testing.T) {
toml, err := Load(`name = { first = "Tom", last = "Preston-Werner" } toml, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
points = { x = 1, y = 2 }`) points = { x = 1, y = 2 }`)
@@ -63,7 +87,7 @@ points = { x = 1, y = 2 }`)
}) })
} }
func TestTomlTreeWriteToTomlStringSimple(t *testing.T) { func TestTreeWriteToTomlStringSimple(t *testing.T) {
tree, err := Load("[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n") tree, err := Load("[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n")
if err != nil { if err != nil {
t.Errorf("Test failed to parse: %v", err) t.Errorf("Test failed to parse: %v", err)
@@ -79,7 +103,7 @@ func TestTomlTreeWriteToTomlStringSimple(t *testing.T) {
} }
} }
func TestTomlTreeWriteToTomlStringKeysOrders(t *testing.T) { func TestTreeWriteToTomlStringKeysOrders(t *testing.T) {
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
tree, _ := Load(` tree, _ := Load(`
foobar = true foobar = true
@@ -119,20 +143,7 @@ func testMaps(t *testing.T, actual, expected map[string]interface{}) {
} }
} }
func TestToTomlStringTypeConversionError(t *testing.T) { func TestTreeWriteToMapSimple(t *testing.T) {
tree := TomlTree{
values: map[string]interface{}{
"thing": &tomlValue{[]string{"unsupported"}, Position{}},
},
}
_, err := tree.ToTomlString()
expected := errors.New("unsupported value type []string: [unsupported]")
if err.Error() != expected.Error() {
t.Errorf("expecting error %s, but got %s instead", expected, err)
}
}
func TestTomlTreeWriteToMapSimple(t *testing.T) {
tree, _ := Load("a = 42\nb = 17") tree, _ := Load("a = 42\nb = 17")
expected := map[string]interface{}{ expected := map[string]interface{}{
@@ -143,32 +154,32 @@ func TestTomlTreeWriteToMapSimple(t *testing.T) {
testMaps(t, tree.ToMap(), expected) testMaps(t, tree.ToMap(), expected)
} }
func TestTomlTreeWriteToInvalidTreeSimpleValue(t *testing.T) { func TestTreeWriteToInvalidTreeSimpleValue(t *testing.T) {
tree := TomlTree{values: map[string]interface{}{"foo": int8(1)}} tree := Tree{values: map[string]interface{}{"foo": int8(1)}}
_, err := tree.ToTomlString() _, err := tree.ToTomlString()
assertErrorString(t, "invalid key type at foo: int8", err) assertErrorString(t, "invalid value type at foo: int8", err)
} }
func TestTomlTreeWriteToInvalidTreeTomlValue(t *testing.T) { func TestTreeWriteToInvalidTreeTomlValue(t *testing.T) {
tree := TomlTree{values: map[string]interface{}{"foo": &tomlValue{int8(1), Position{}}}} tree := Tree{values: map[string]interface{}{"foo": &tomlValue{int8(1), Position{}}}}
_, err := tree.ToTomlString() _, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err) assertErrorString(t, "unsupported value type int8: 1", err)
} }
func TestTomlTreeWriteToInvalidTreeTomlValueArray(t *testing.T) { func TestTreeWriteToInvalidTreeTomlValueArray(t *testing.T) {
tree := TomlTree{values: map[string]interface{}{"foo": &tomlValue{[]interface{}{int8(1)}, Position{}}}} tree := Tree{values: map[string]interface{}{"foo": &tomlValue{[]interface{}{int8(1)}, Position{}}}}
_, err := tree.ToTomlString() _, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err) assertErrorString(t, "unsupported value type int8: 1", err)
} }
func TestTomlTreeWriteToFailingWriterInSimpleValue(t *testing.T) { func TestTreeWriteToFailingWriterInSimpleValue(t *testing.T) {
toml, _ := Load(`a = 2`) toml, _ := Load(`a = 2`)
writer := failingWriter{failAt: 0, written: 0} writer := failingWriter{failAt: 0, written: 0}
_, err := toml.WriteTo(writer) _, err := toml.WriteTo(writer)
assertErrorString(t, "failingWriter failed after writting 0 bytes", err) assertErrorString(t, "failingWriter failed after writting 0 bytes", err)
} }
func TestTomlTreeWriteToFailingWriterInTable(t *testing.T) { func TestTreeWriteToFailingWriterInTable(t *testing.T) {
toml, _ := Load(` toml, _ := Load(`
[b] [b]
a = 2`) a = 2`)
@@ -181,7 +192,7 @@ a = 2`)
assertErrorString(t, "failingWriter failed after writting 13 bytes", err) assertErrorString(t, "failingWriter failed after writting 13 bytes", err)
} }
func TestTomlTreeWriteToFailingWriterInArray(t *testing.T) { func TestTreeWriteToFailingWriterInArray(t *testing.T) {
toml, _ := Load(` toml, _ := Load(`
[[b]] [[b]]
a = 2`) a = 2`)
@@ -194,7 +205,7 @@ a = 2`)
assertErrorString(t, "failingWriter failed after writting 15 bytes", err) assertErrorString(t, "failingWriter failed after writting 15 bytes", err)
} }
func TestTomlTreeWriteToMapExampleFile(t *testing.T) { func TestTreeWriteToMapExampleFile(t *testing.T) {
tree, _ := LoadFile("example.toml") tree, _ := LoadFile("example.toml")
expected := map[string]interface{}{ expected := map[string]interface{}{
"title": "TOML Example", "title": "TOML Example",
@@ -230,7 +241,7 @@ func TestTomlTreeWriteToMapExampleFile(t *testing.T) {
testMaps(t, tree.ToMap(), expected) testMaps(t, tree.ToMap(), expected)
} }
func TestTomlTreeWriteToMapWithTablesInMultipleChunks(t *testing.T) { func TestTreeWriteToMapWithTablesInMultipleChunks(t *testing.T) {
tree, _ := Load(` tree, _ := Load(`
[[menu.main]] [[menu.main]]
a = "menu 1" a = "menu 1"
@@ -251,7 +262,7 @@ func TestTomlTreeWriteToMapWithTablesInMultipleChunks(t *testing.T) {
testMaps(t, treeMap, expected) testMaps(t, treeMap, expected)
} }
func TestTomlTreeWriteToMapWithArrayOfInlineTables(t *testing.T) { func TestTreeWriteToMapWithArrayOfInlineTables(t *testing.T) {
tree, _ := Load(` tree, _ := Load(`
[params] [params]
language_tabs = [ language_tabs = [