Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64ff1ea4d5 | |||
| b39f6ef1f9 | |||
| c187221f01 | |||
| 8e6ab94eec | |||
| 8d9c606c69 | |||
| 288bc57940 | |||
| e3b2497729 | |||
| 1a8565204c | |||
| e58cfd32d4 | |||
| a2ae216b47 | |||
| 8645be8dc7 | |||
| 99b9371c53 | |||
| 92c565e02b | |||
| 6e26017b00 | |||
| 9d93af61de | |||
| 4d8fb95ffe | |||
| 0e41db2176 | |||
| afca7f3334 | |||
| d6a90e60ed | |||
| fe63e9f76d | |||
| 7f50e4c339 | |||
| a402e618c3 | |||
| 2df083520a | |||
| 8176e30b38 | |||
| 14c964fc02 | |||
| f963bc320f | |||
| 0488b850c6 | |||
| 346e676fa2 | |||
| 6d743bb19f | |||
| fa1c2ab68c | |||
| a6c6ad1f5f | |||
| ab7a652912 | |||
| 3102b98900 | |||
| f0cae62430 | |||
| 56c6106477 | |||
| 7d69e5a5c5 | |||
| 07d0c2e4d3 | |||
| 6b9002d8f9 | |||
| 5753e884d0 | |||
| d467309bdd | |||
| 821a80e635 | |||
| dd4c4ffc2b | |||
| da703daafe | |||
| f58048cec0 | |||
| 440592fa85 | |||
| f4f2456dcd | |||
| a77f30ea80 | |||
| d61c80733b | |||
| 894e775e38 | |||
| 8e75093380 | |||
| cf5ad6a245 | |||
| 8fc7451ffc | |||
| 9defd66d3c | |||
| 6adf8057ed | |||
| 36e1197190 | |||
| 6dd2de38a9 | |||
| 209315c2af | |||
| 41a8959f14 | |||
| 16a681db2a | |||
| 9f36448571 | |||
| 222e90a7d3 | |||
| a8327d781a | |||
| 61449e9d32 | |||
| 48c977fb58 | |||
| 42e7853ef6 | |||
| 1f3d0e03c3 | |||
| 36d65b681a | |||
| a56707c85f | |||
| 4b47f52cb0 | |||
| 2f2f28631b | |||
| 543444f747 | |||
| b814e1a94f | |||
| 1fe62f3000 | |||
| 709382e9c1 | |||
| 71e7762db5 | |||
| 34da10d880 | |||
| db15f8a481 | |||
| 8ef71920bd | |||
| fa055bcbba | |||
| 7337a63f5a |
+9
-3
@@ -1,7 +1,13 @@
|
|||||||
language: go
|
language: go
|
||||||
script: "./test.sh"
|
script: "./test.sh"
|
||||||
go:
|
go:
|
||||||
- 1.1
|
- 1.4.3
|
||||||
- 1.2
|
- 1.5.4
|
||||||
- 1.3
|
- 1.6.2
|
||||||
- tip
|
- tip
|
||||||
|
before_install:
|
||||||
|
- go get github.com/axw/gocov/gocov
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
|
||||||
|
after_success:
|
||||||
|
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
Go library for the [TOML](https://github.com/mojombo/toml) format.
|
Go library for the [TOML](https://github.com/mojombo/toml) format.
|
||||||
|
|
||||||
This library supports TOML version
|
This library supports TOML version
|
||||||
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
|
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
|
||||||
|
|
||||||
[](http://godoc.org/github.com/pelletier/go-toml)
|
[](http://godoc.org/github.com/pelletier/go-toml)
|
||||||
[](https://travis-ci.org/pelletier/go-toml)
|
[](https://travis-ci.org/pelletier/go-toml)
|
||||||
|
[](https://coveralls.io/github/pelletier/go-toml?branch=master)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ You can run both of them using `./test.sh`.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2013, 2014 Thomas Pelletier, Eric Anderton
|
Copyright (c) 2013 - 2016 Thomas Pelletier, Eric Anderton
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
bytes, err := ioutil.ReadAll(os.Stdin)
|
bytes, err := ioutil.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Fatalf("Error during TOML read: %s", err)
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
tree, err := toml.Load(string(bytes))
|
tree, err := toml.Load(string(bytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Fatalf("Error during TOML load: %s", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ func main() {
|
|||||||
|
|
||||||
if err := json.NewEncoder(os.Stdout).Encode(typedTree); err != nil {
|
if err := json.NewEncoder(os.Stdout).Encode(typedTree); err != nil {
|
||||||
log.Fatalf("Error encoding JSON: %s", err)
|
log.Fatalf("Error encoding JSON: %s", err)
|
||||||
|
os.Exit(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Fprintln(os.Stderr, `tomll can be used in two ways:
|
||||||
|
Writing to STDIN and reading from STDOUT:
|
||||||
|
cat file.toml | tomll > file.toml
|
||||||
|
|
||||||
|
Reading and updating a list of files:
|
||||||
|
tomll a.toml b.toml c.toml
|
||||||
|
|
||||||
|
When given a list of files, tomll will modify all files in place without asking.
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
// read from stdin and print to stdout
|
||||||
|
if flag.NArg() == 0 {
|
||||||
|
s, err := lintReader(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
io.WriteString(os.Stderr, err.Error())
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
io.WriteString(os.Stdout, s)
|
||||||
|
} else {
|
||||||
|
// otherwise modify a list of files
|
||||||
|
for _, filename := range flag.Args() {
|
||||||
|
s, err := lintFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
io.WriteString(os.Stderr, err.Error())
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
ioutil.WriteFile(filename, []byte(s), 0644)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lintFile(filename string) (string, error) {
|
||||||
|
tree, err := toml.LoadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tree.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lintReader(r io.Reader) (string, error) {
|
||||||
|
tree, err := toml.LoadReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tree.String(), nil
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Package toml is a TOML markup language parser.
|
// Package toml is a TOML markup language parser.
|
||||||
//
|
//
|
||||||
// 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/toml-v0.2.0.md
|
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md
|
||||||
//
|
//
|
||||||
// TOML Parsing
|
// TOML Parsing
|
||||||
//
|
//
|
||||||
@@ -83,9 +83,9 @@
|
|||||||
// The idea behind a query path is to allow quick access to any element, or set
|
// 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.
|
// of elements within TOML document, with a single expression.
|
||||||
//
|
//
|
||||||
// result := tree.Query("$.foo.bar.baz") // result is 'nil' if the path is not present
|
// result, err := tree.Query("$.foo.bar.baz")
|
||||||
//
|
//
|
||||||
// This is equivalent to:
|
// This is roughly equivalent to:
|
||||||
//
|
//
|
||||||
// next := tree.Get("foo")
|
// next := tree.Get("foo")
|
||||||
// if next != nil {
|
// if next != nil {
|
||||||
@@ -96,6 +96,11 @@
|
|||||||
// }
|
// }
|
||||||
// result := next
|
// 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
|
// 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
|
// 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
|
// a document's structure, a query allows the programmer to make structured
|
||||||
|
|||||||
+3
-3
@@ -69,13 +69,13 @@ func Example_comprehensiveExample() {
|
|||||||
fmt.Println("User is ", user, ". Password is ", password)
|
fmt.Println("User is ", user, ". Password is ", password)
|
||||||
|
|
||||||
// show where elements are in the file
|
// show where elements are in the file
|
||||||
fmt.Println("User position: %v", configTree.GetPosition("user"))
|
fmt.Printf("User position: %v\n", configTree.GetPosition("user"))
|
||||||
fmt.Println("Password position: %v", configTree.GetPosition("password"))
|
fmt.Printf("Password position: %v\n", configTree.GetPosition("password"))
|
||||||
|
|
||||||
// use a query to gather elements without walking the tree
|
// use a query to gather elements without walking the tree
|
||||||
results, _ := config.Query("$..[user,password]")
|
results, _ := config.Query("$..[user,password]")
|
||||||
for ii, item := range results.Values() {
|
for ii, item := range results.Values() {
|
||||||
fmt.Println("Query result %d: %v", ii, item)
|
fmt.Printf("Query result %d: %v\n", ii, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# This is a TOML document. Boom.
|
||||||
|
|
||||||
|
title = "TOML Example"
|
||||||
|
|
||||||
|
[owner]
|
||||||
|
name = "Tom Preston-Werner"
|
||||||
|
organization = "GitHub"
|
||||||
|
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
||||||
|
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
||||||
|
|
||||||
|
[database]
|
||||||
|
server = "192.168.1.1"
|
||||||
|
ports = [ 8001, 8001, 8002 ]
|
||||||
|
connection_max = 5000
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[servers]
|
||||||
|
|
||||||
|
# You can indent as you please. Tabs or spaces. TOML don't care.
|
||||||
|
[servers.alpha]
|
||||||
|
ip = "10.0.0.1"
|
||||||
|
dc = "eqdc10"
|
||||||
|
|
||||||
|
[servers.beta]
|
||||||
|
ip = "10.0.0.2"
|
||||||
|
dc = "eqdc10"
|
||||||
|
|
||||||
|
[clients]
|
||||||
|
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Parsing keys handling both bare and quoted keys.
|
||||||
|
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseKey(key string) ([]string, error) {
|
||||||
|
groups := []string{}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
inQuotes := false
|
||||||
|
escapeNext := false
|
||||||
|
ignoreSpace := true
|
||||||
|
expectDot := false
|
||||||
|
|
||||||
|
for _, char := range key {
|
||||||
|
if ignoreSpace {
|
||||||
|
if char == ' ' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ignoreSpace = false
|
||||||
|
}
|
||||||
|
if escapeNext {
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
escapeNext = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch char {
|
||||||
|
case '\\':
|
||||||
|
escapeNext = true
|
||||||
|
continue
|
||||||
|
case '"':
|
||||||
|
inQuotes = !inQuotes
|
||||||
|
expectDot = false
|
||||||
|
case '.':
|
||||||
|
if inQuotes {
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
} else {
|
||||||
|
groups = append(groups, buffer.String())
|
||||||
|
buffer.Reset()
|
||||||
|
ignoreSpace = true
|
||||||
|
expectDot = false
|
||||||
|
}
|
||||||
|
case ' ':
|
||||||
|
if inQuotes {
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
} else {
|
||||||
|
expectDot = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if !inQuotes && !isValidBareChar(char) {
|
||||||
|
return nil, fmt.Errorf("invalid bare character: %c", char)
|
||||||
|
}
|
||||||
|
if !inQuotes && expectDot {
|
||||||
|
return nil, fmt.Errorf("what?")
|
||||||
|
}
|
||||||
|
buffer.WriteRune(char)
|
||||||
|
expectDot = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inQuotes {
|
||||||
|
return nil, fmt.Errorf("mismatched quotes")
|
||||||
|
}
|
||||||
|
if escapeNext {
|
||||||
|
return nil, fmt.Errorf("unfinished escape sequence")
|
||||||
|
}
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
groups = append(groups, buffer.String())
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty key")
|
||||||
|
}
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidBareChar(r rune) bool {
|
||||||
|
return isAlphanumeric(r) || r == '-' || unicode.IsNumber(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testResult(t *testing.T, key string, expected []string) {
|
||||||
|
parsed, err := parseKey(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
if len(expected) != len(parsed) {
|
||||||
|
t.Fatal("Expected length", len(expected), "but", len(parsed), "parsed")
|
||||||
|
}
|
||||||
|
for index, expectedKey := range expected {
|
||||||
|
if expectedKey != parsed[index] {
|
||||||
|
t.Fatal("Expected", expectedKey, "at index", index, "but found", parsed[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testError(t *testing.T, key string, expectedError string) {
|
||||||
|
_, err := parseKey(key)
|
||||||
|
if fmt.Sprintf("%s", err) != expectedError {
|
||||||
|
t.Fatalf("Expected error \"%s\", but got \"%s\".", expectedError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBareKeyBasic(t *testing.T) {
|
||||||
|
testResult(t, "test", []string{"test"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBareKeyDotted(t *testing.T) {
|
||||||
|
testResult(t, "this.is.a.key", []string{"this", "is", "a", "key"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDottedKeyBasic(t *testing.T) {
|
||||||
|
testResult(t, "\"a.dotted.key\"", []string{"a.dotted.key"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseKeyPound(t *testing.T) {
|
||||||
|
testError(t, "hello#world", "invalid bare character: #")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyKey(t *testing.T) {
|
||||||
|
testError(t, "", "empty key")
|
||||||
|
testError(t, " ", "empty key")
|
||||||
|
}
|
||||||
@@ -6,11 +6,14 @@
|
|||||||
package toml
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
|
||||||
|
"github.com/pelletier/go-buffruneio"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dateRegexp *regexp.Regexp
|
var dateRegexp *regexp.Regexp
|
||||||
@@ -20,47 +23,56 @@ type tomlLexStateFn func() tomlLexStateFn
|
|||||||
|
|
||||||
// Define lexer
|
// Define lexer
|
||||||
type tomlLexer struct {
|
type tomlLexer struct {
|
||||||
input string
|
input *buffruneio.Reader // Textual source
|
||||||
start int
|
buffer []rune // Runes composing the current token
|
||||||
pos int
|
tokens chan token
|
||||||
width int
|
depth int
|
||||||
tokens chan token
|
line int
|
||||||
depth int
|
col int
|
||||||
line int
|
endbufferLine int
|
||||||
col int
|
endbufferCol int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) run() {
|
// Basic read operations on input
|
||||||
for state := l.lexVoid; state != nil; {
|
|
||||||
state = state()
|
func (l *tomlLexer) read() rune {
|
||||||
|
r, err := l.input.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
close(l.tokens)
|
if r == '\n' {
|
||||||
|
l.endbufferLine++
|
||||||
|
l.endbufferCol = 1
|
||||||
|
} else {
|
||||||
|
l.endbufferCol++
|
||||||
|
}
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) nextStart() {
|
func (l *tomlLexer) next() rune {
|
||||||
// iterate by runes (utf8 characters)
|
r := l.read()
|
||||||
// search for newlines and advance line/col counts
|
|
||||||
for i := l.start; i < l.pos; {
|
if r != eof {
|
||||||
r, width := utf8.DecodeRuneInString(l.input[i:])
|
l.buffer = append(l.buffer, r)
|
||||||
if r == '\n' {
|
|
||||||
l.line++
|
|
||||||
l.col = 1
|
|
||||||
} else {
|
|
||||||
l.col++
|
|
||||||
}
|
|
||||||
i += width
|
|
||||||
}
|
}
|
||||||
// advance start position to next token
|
return r
|
||||||
l.start = l.pos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) emit(t tokenType) {
|
func (l *tomlLexer) ignore() {
|
||||||
l.tokens <- token{
|
l.buffer = make([]rune, 0)
|
||||||
Position: Position{l.line, l.col},
|
l.line = l.endbufferLine
|
||||||
typ: t,
|
l.col = l.endbufferCol
|
||||||
val: l.input[l.start:l.pos],
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) skip() {
|
||||||
|
l.next()
|
||||||
|
l.ignore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) fastForward(n int) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
l.next()
|
||||||
}
|
}
|
||||||
l.nextStart()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) emitWithValue(t tokenType, value string) {
|
func (l *tomlLexer) emitWithValue(t tokenType, value string) {
|
||||||
@@ -69,27 +81,37 @@ func (l *tomlLexer) emitWithValue(t tokenType, value string) {
|
|||||||
typ: t,
|
typ: t,
|
||||||
val: value,
|
val: value,
|
||||||
}
|
}
|
||||||
l.nextStart()
|
l.ignore()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) next() rune {
|
func (l *tomlLexer) emit(t tokenType) {
|
||||||
if l.pos >= len(l.input) {
|
l.emitWithValue(t, string(l.buffer))
|
||||||
l.width = 0
|
}
|
||||||
return eof
|
|
||||||
|
func (l *tomlLexer) peek() rune {
|
||||||
|
r, err := l.input.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
var r rune
|
l.input.UnreadRune()
|
||||||
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
|
|
||||||
l.pos += l.width
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) ignore() {
|
func (l *tomlLexer) follow(next string) bool {
|
||||||
l.nextStart()
|
for _, expectedRune := range next {
|
||||||
|
r, err := l.input.ReadRune()
|
||||||
|
defer l.input.UnreadRune()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if expectedRune != r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) backup() {
|
// Error management
|
||||||
l.pos -= l.width
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
|
func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
|
||||||
l.tokens <- token{
|
l.tokens <- token{
|
||||||
@@ -100,23 +122,7 @@ func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) peek() rune {
|
// State functions
|
||||||
r := l.next()
|
|
||||||
l.backup()
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *tomlLexer) accept(valid string) bool {
|
|
||||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
l.backup()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *tomlLexer) follow(next string) bool {
|
|
||||||
return strings.HasPrefix(l.input[l.pos:], next)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *tomlLexer) lexVoid() tomlLexStateFn {
|
func (l *tomlLexer) lexVoid() tomlLexStateFn {
|
||||||
for {
|
for {
|
||||||
@@ -128,21 +134,27 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
|
|||||||
return l.lexComment
|
return l.lexComment
|
||||||
case '=':
|
case '=':
|
||||||
return l.lexEqual
|
return l.lexEqual
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
|
case '\n':
|
||||||
|
l.skip()
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSpace(next) {
|
if isSpace(next) {
|
||||||
l.ignore()
|
l.skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.depth > 0 {
|
if l.depth > 0 {
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
if isKeyChar(next) {
|
if isKeyStartChar(next) {
|
||||||
return l.lexKey
|
return l.lexKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.next() == eof {
|
if next == eof {
|
||||||
|
l.next()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,26 +170,35 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
|
|||||||
case '.':
|
case '.':
|
||||||
return l.errorf("cannot start float with a dot")
|
return l.errorf("cannot start float with a dot")
|
||||||
case '=':
|
case '=':
|
||||||
return l.errorf("cannot have multiple equals for the same key")
|
return l.lexEqual
|
||||||
case '[':
|
case '[':
|
||||||
l.depth++
|
l.depth++
|
||||||
return l.lexLeftBracket
|
return l.lexLeftBracket
|
||||||
case ']':
|
case ']':
|
||||||
l.depth--
|
l.depth--
|
||||||
return l.lexRightBracket
|
return l.lexRightBracket
|
||||||
|
case '{':
|
||||||
|
return l.lexLeftCurlyBrace
|
||||||
|
case '}':
|
||||||
|
return l.lexRightCurlyBrace
|
||||||
case '#':
|
case '#':
|
||||||
return l.lexComment
|
return l.lexComment
|
||||||
case '"':
|
case '"':
|
||||||
return l.lexString
|
return l.lexString
|
||||||
|
case '\'':
|
||||||
|
return l.lexLiteralString
|
||||||
case ',':
|
case ',':
|
||||||
return l.lexComma
|
return l.lexComma
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
case '\n':
|
case '\n':
|
||||||
l.ignore()
|
l.skip()
|
||||||
l.pos++
|
|
||||||
if l.depth == 0 {
|
if l.depth == 0 {
|
||||||
return l.lexVoid
|
return l.lexVoid
|
||||||
}
|
}
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
|
case '_':
|
||||||
|
return l.errorf("cannot start number with underscore")
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.follow("true") {
|
if l.follow("true") {
|
||||||
@@ -188,11 +209,20 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
|
|||||||
return l.lexFalse
|
return l.lexFalse
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAlphanumeric(next) {
|
if isSpace(next) {
|
||||||
return l.lexKey
|
l.skip()
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if dateRegexp.FindString(l.input[l.pos:]) != "" {
|
if next == eof {
|
||||||
|
l.next()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
possibleDate := string(l.input.Peek(35))
|
||||||
|
dateMatch := dateRegexp.FindString(possibleDate)
|
||||||
|
if dateMatch != "" {
|
||||||
|
l.fastForward(len(dateMatch))
|
||||||
return l.lexDate
|
return l.lexDate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,157 +230,298 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
|
|||||||
return l.lexNumber
|
return l.lexNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSpace(next) {
|
if isAlphanumeric(next) {
|
||||||
l.ignore()
|
return l.lexKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.next() == eof {
|
return l.errorf("no value can start with %c", next)
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.emit(tokenEOF)
|
l.emit(tokenEOF)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenLeftCurlyBrace)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
|
||||||
|
l.next()
|
||||||
|
l.emit(tokenRightCurlyBrace)
|
||||||
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexDate() tomlLexStateFn {
|
func (l *tomlLexer) lexDate() tomlLexStateFn {
|
||||||
l.ignore()
|
|
||||||
l.pos += 20 // Fixed size of a date in TOML
|
|
||||||
l.emit(tokenDate)
|
l.emit(tokenDate)
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexTrue() tomlLexStateFn {
|
func (l *tomlLexer) lexTrue() tomlLexStateFn {
|
||||||
l.ignore()
|
l.fastForward(4)
|
||||||
l.pos += 4
|
|
||||||
l.emit(tokenTrue)
|
l.emit(tokenTrue)
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexFalse() tomlLexStateFn {
|
func (l *tomlLexer) lexFalse() tomlLexStateFn {
|
||||||
l.ignore()
|
l.fastForward(5)
|
||||||
l.pos += 5
|
|
||||||
l.emit(tokenFalse)
|
l.emit(tokenFalse)
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexEqual() tomlLexStateFn {
|
func (l *tomlLexer) lexEqual() tomlLexStateFn {
|
||||||
l.ignore()
|
l.next()
|
||||||
l.accept("=")
|
|
||||||
l.emit(tokenEqual)
|
l.emit(tokenEqual)
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexComma() tomlLexStateFn {
|
func (l *tomlLexer) lexComma() tomlLexStateFn {
|
||||||
l.ignore()
|
l.next()
|
||||||
l.accept(",")
|
|
||||||
l.emit(tokenComma)
|
l.emit(tokenComma)
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexKey() tomlLexStateFn {
|
func (l *tomlLexer) lexKey() tomlLexStateFn {
|
||||||
l.ignore()
|
growingString := ""
|
||||||
for isKeyChar(l.next()) {
|
|
||||||
|
for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
|
||||||
|
if r == '"' {
|
||||||
|
l.next()
|
||||||
|
str, err := l.lexStringAsString(`"`, false, true)
|
||||||
|
if err != nil {
|
||||||
|
return l.errorf(err.Error())
|
||||||
|
}
|
||||||
|
growingString += `"` + str + `"`
|
||||||
|
l.next()
|
||||||
|
continue
|
||||||
|
} else if r == '\n' {
|
||||||
|
return l.errorf("keys cannot contain new lines")
|
||||||
|
} else if isSpace(r) {
|
||||||
|
break
|
||||||
|
} else if !isValidBareChar(r) {
|
||||||
|
return l.errorf("keys cannot contain %c character", r)
|
||||||
|
}
|
||||||
|
growingString += string(r)
|
||||||
|
l.next()
|
||||||
}
|
}
|
||||||
l.backup()
|
l.emitWithValue(tokenKey, growingString)
|
||||||
l.emit(tokenKey)
|
|
||||||
return l.lexVoid
|
return l.lexVoid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexComment() tomlLexStateFn {
|
func (l *tomlLexer) lexComment() tomlLexStateFn {
|
||||||
for {
|
for next := l.peek(); next != '\n' && next != eof; next = l.peek() {
|
||||||
next := l.next()
|
if next == '\r' && l.follow("\r\n") {
|
||||||
if next == '\n' || next == eof {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
l.next()
|
||||||
}
|
}
|
||||||
l.ignore()
|
l.ignore()
|
||||||
return l.lexVoid
|
return l.lexVoid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
|
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
|
||||||
l.ignore()
|
l.next()
|
||||||
l.pos++
|
|
||||||
l.emit(tokenLeftBracket)
|
l.emit(tokenLeftBracket)
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexString() tomlLexStateFn {
|
func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) {
|
||||||
l.pos++
|
|
||||||
l.ignore()
|
|
||||||
growingString := ""
|
growingString := ""
|
||||||
|
|
||||||
|
if discardLeadingNewLine {
|
||||||
|
if l.follow("\r\n") {
|
||||||
|
l.skip()
|
||||||
|
l.skip()
|
||||||
|
} else if l.peek() == '\n' {
|
||||||
|
l.skip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find end of string
|
||||||
for {
|
for {
|
||||||
if l.peek() == '"' {
|
if l.follow(terminator) {
|
||||||
l.emitWithValue(tokenString, growingString)
|
return growingString, nil
|
||||||
l.pos++
|
|
||||||
l.ignore()
|
|
||||||
return l.lexRvalue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.follow("\\\"") {
|
next := l.peek()
|
||||||
l.pos++
|
if next == eof {
|
||||||
growingString += "\""
|
break
|
||||||
} else if l.follow("\\n") {
|
}
|
||||||
l.pos++
|
growingString += string(l.next())
|
||||||
growingString += "\n"
|
}
|
||||||
} else if l.follow("\\b") {
|
|
||||||
l.pos++
|
return "", errors.New("unclosed string")
|
||||||
growingString += "\b"
|
}
|
||||||
} else if l.follow("\\f") {
|
|
||||||
l.pos++
|
func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
|
||||||
growingString += "\f"
|
l.skip()
|
||||||
} else if l.follow("\\/") {
|
|
||||||
l.pos++
|
// handle special case for triple-quote
|
||||||
growingString += "/"
|
terminator := "'"
|
||||||
} else if l.follow("\\t") {
|
discardLeadingNewLine := false
|
||||||
l.pos++
|
if l.follow("''") {
|
||||||
growingString += "\t"
|
l.skip()
|
||||||
} else if l.follow("\\r") {
|
l.skip()
|
||||||
l.pos++
|
terminator = "'''"
|
||||||
growingString += "\r"
|
discardLeadingNewLine = true
|
||||||
} else if l.follow("\\\\") {
|
}
|
||||||
l.pos++
|
|
||||||
growingString += "\\"
|
str, err := l.lexLiteralStringAsString(terminator, discardLeadingNewLine)
|
||||||
} else if l.follow("\\u") {
|
if err != nil {
|
||||||
l.pos += 2
|
return l.errorf(err.Error())
|
||||||
code := ""
|
}
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
c := l.peek()
|
l.emitWithValue(tokenString, str)
|
||||||
l.pos++
|
l.fastForward(len(terminator))
|
||||||
if !isHexDigit(c) {
|
l.ignore()
|
||||||
return l.errorf("unfinished unicode escape")
|
return l.lexRvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lex a string and return the results as a string.
|
||||||
|
// Terminator is the substring indicating the end of the token.
|
||||||
|
// The resulting string does not include the terminator.
|
||||||
|
func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) {
|
||||||
|
growingString := ""
|
||||||
|
|
||||||
|
if discardLeadingNewLine {
|
||||||
|
if l.follow("\r\n") {
|
||||||
|
l.skip()
|
||||||
|
l.skip()
|
||||||
|
} else if l.peek() == '\n' {
|
||||||
|
l.skip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if l.follow(terminator) {
|
||||||
|
return growingString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.follow("\\") {
|
||||||
|
l.next()
|
||||||
|
switch l.peek() {
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
|
case '\n':
|
||||||
|
fallthrough
|
||||||
|
case '\t':
|
||||||
|
fallthrough
|
||||||
|
case ' ':
|
||||||
|
// skip all whitespace chars following backslash
|
||||||
|
for strings.ContainsRune("\r\n\t ", l.peek()) {
|
||||||
|
l.next()
|
||||||
}
|
}
|
||||||
code = code + string(c)
|
case '"':
|
||||||
|
growingString += "\""
|
||||||
|
l.next()
|
||||||
|
case 'n':
|
||||||
|
growingString += "\n"
|
||||||
|
l.next()
|
||||||
|
case 'b':
|
||||||
|
growingString += "\b"
|
||||||
|
l.next()
|
||||||
|
case 'f':
|
||||||
|
growingString += "\f"
|
||||||
|
l.next()
|
||||||
|
case '/':
|
||||||
|
growingString += "/"
|
||||||
|
l.next()
|
||||||
|
case 't':
|
||||||
|
growingString += "\t"
|
||||||
|
l.next()
|
||||||
|
case 'r':
|
||||||
|
growingString += "\r"
|
||||||
|
l.next()
|
||||||
|
case '\\':
|
||||||
|
growingString += "\\"
|
||||||
|
l.next()
|
||||||
|
case 'u':
|
||||||
|
l.next()
|
||||||
|
code := ""
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
c := l.peek()
|
||||||
|
if !isHexDigit(c) {
|
||||||
|
return "", errors.New("unfinished unicode escape")
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
code = code + string(c)
|
||||||
|
}
|
||||||
|
intcode, err := strconv.ParseInt(code, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("invalid unicode escape: \\u" + code)
|
||||||
|
}
|
||||||
|
growingString += string(rune(intcode))
|
||||||
|
case 'U':
|
||||||
|
l.next()
|
||||||
|
code := ""
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
c := l.peek()
|
||||||
|
if !isHexDigit(c) {
|
||||||
|
return "", errors.New("unfinished unicode escape")
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
code = code + string(c)
|
||||||
|
}
|
||||||
|
intcode, err := strconv.ParseInt(code, 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("invalid unicode escape: \\U" + code)
|
||||||
|
}
|
||||||
|
growingString += string(rune(intcode))
|
||||||
|
default:
|
||||||
|
return "", errors.New("invalid escape sequence: \\" + string(l.peek()))
|
||||||
}
|
}
|
||||||
l.pos--
|
|
||||||
intcode, err := strconv.ParseInt(code, 16, 32)
|
|
||||||
if err != nil {
|
|
||||||
return l.errorf("invalid unicode escape: \\u" + code)
|
|
||||||
}
|
|
||||||
growingString += string(rune(intcode))
|
|
||||||
} else if l.follow("\\") {
|
|
||||||
l.pos++
|
|
||||||
return l.errorf("invalid escape sequence: \\" + string(l.peek()))
|
|
||||||
} else {
|
} else {
|
||||||
growingString += string(l.peek())
|
r := l.peek()
|
||||||
|
|
||||||
|
if 0x00 <= r && r <= 0x1F && !(acceptNewLines && (r == '\n' || r == '\r')) {
|
||||||
|
return "", fmt.Errorf("unescaped control character %U", r)
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
growingString += string(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.next() == eof {
|
if l.peek() == eof {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return l.errorf("unclosed string")
|
return "", errors.New("unclosed string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) lexString() tomlLexStateFn {
|
||||||
|
l.skip()
|
||||||
|
|
||||||
|
// handle special case for triple-quote
|
||||||
|
terminator := `"`
|
||||||
|
discardLeadingNewLine := false
|
||||||
|
acceptNewLines := false
|
||||||
|
if l.follow(`""`) {
|
||||||
|
l.skip()
|
||||||
|
l.skip()
|
||||||
|
terminator = `"""`
|
||||||
|
discardLeadingNewLine = true
|
||||||
|
acceptNewLines = true
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return l.errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
l.emitWithValue(tokenString, str)
|
||||||
|
l.fastForward(len(terminator))
|
||||||
|
l.ignore()
|
||||||
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexKeyGroup() tomlLexStateFn {
|
func (l *tomlLexer) lexKeyGroup() tomlLexStateFn {
|
||||||
l.ignore()
|
l.next()
|
||||||
l.pos++
|
|
||||||
|
|
||||||
if l.peek() == '[' {
|
if l.peek() == '[' {
|
||||||
// token '[[' signifies an array of anonymous key groups
|
// token '[[' signifies an array of anonymous key groups
|
||||||
l.pos++
|
l.next()
|
||||||
l.emit(tokenDoubleLeftBracket)
|
l.emit(tokenDoubleLeftBracket)
|
||||||
return l.lexInsideKeyGroupArray
|
return l.lexInsideKeyGroupArray
|
||||||
}
|
}
|
||||||
@@ -360,79 +531,85 @@ func (l *tomlLexer) lexKeyGroup() tomlLexStateFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexInsideKeyGroupArray() tomlLexStateFn {
|
func (l *tomlLexer) lexInsideKeyGroupArray() tomlLexStateFn {
|
||||||
for {
|
for r := l.peek(); r != eof; r = l.peek() {
|
||||||
if l.peek() == ']' {
|
switch r {
|
||||||
if l.pos > l.start {
|
case ']':
|
||||||
|
if len(l.buffer) > 0 {
|
||||||
l.emit(tokenKeyGroupArray)
|
l.emit(tokenKeyGroupArray)
|
||||||
}
|
}
|
||||||
l.ignore()
|
l.next()
|
||||||
l.pos++
|
|
||||||
if l.peek() != ']' {
|
if l.peek() != ']' {
|
||||||
break // error
|
break
|
||||||
}
|
}
|
||||||
l.pos++
|
l.next()
|
||||||
l.emit(tokenDoubleRightBracket)
|
l.emit(tokenDoubleRightBracket)
|
||||||
return l.lexVoid
|
return l.lexVoid
|
||||||
} else if l.peek() == '[' {
|
case '[':
|
||||||
return l.errorf("group name cannot contain ']'")
|
return l.errorf("group name cannot contain ']'")
|
||||||
}
|
default:
|
||||||
|
l.next()
|
||||||
if l.next() == eof {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return l.errorf("unclosed key group array")
|
return l.errorf("unclosed key group array")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexInsideKeyGroup() tomlLexStateFn {
|
func (l *tomlLexer) lexInsideKeyGroup() tomlLexStateFn {
|
||||||
for {
|
for r := l.peek(); r != eof; r = l.peek() {
|
||||||
if l.peek() == ']' {
|
switch r {
|
||||||
if l.pos > l.start {
|
case ']':
|
||||||
|
if len(l.buffer) > 0 {
|
||||||
l.emit(tokenKeyGroup)
|
l.emit(tokenKeyGroup)
|
||||||
}
|
}
|
||||||
l.ignore()
|
l.next()
|
||||||
l.pos++
|
|
||||||
l.emit(tokenRightBracket)
|
l.emit(tokenRightBracket)
|
||||||
return l.lexVoid
|
return l.lexVoid
|
||||||
} else if l.peek() == '[' {
|
case '[':
|
||||||
return l.errorf("group name cannot contain ']'")
|
return l.errorf("group name cannot contain ']'")
|
||||||
}
|
default:
|
||||||
|
l.next()
|
||||||
if l.next() == eof {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return l.errorf("unclosed key group")
|
return l.errorf("unclosed key group")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
|
func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
|
||||||
l.ignore()
|
l.next()
|
||||||
l.pos++
|
|
||||||
l.emit(tokenRightBracket)
|
l.emit(tokenRightBracket)
|
||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *tomlLexer) lexNumber() tomlLexStateFn {
|
func (l *tomlLexer) lexNumber() tomlLexStateFn {
|
||||||
l.ignore()
|
r := l.peek()
|
||||||
if !l.accept("+") {
|
if r == '+' || r == '-' {
|
||||||
l.accept("-")
|
l.next()
|
||||||
}
|
}
|
||||||
pointSeen := false
|
pointSeen := false
|
||||||
|
expSeen := false
|
||||||
digitSeen := false
|
digitSeen := false
|
||||||
for {
|
for {
|
||||||
next := l.next()
|
next := l.peek()
|
||||||
if next == '.' {
|
if next == '.' {
|
||||||
if pointSeen {
|
if pointSeen {
|
||||||
return l.errorf("cannot have two dots in one float")
|
return l.errorf("cannot have two dots in one float")
|
||||||
}
|
}
|
||||||
|
l.next()
|
||||||
if !isDigit(l.peek()) {
|
if !isDigit(l.peek()) {
|
||||||
return l.errorf("float cannot end with a dot")
|
return l.errorf("float cannot end with a dot")
|
||||||
}
|
}
|
||||||
pointSeen = true
|
pointSeen = true
|
||||||
|
} else if next == 'e' || next == 'E' {
|
||||||
|
expSeen = true
|
||||||
|
l.next()
|
||||||
|
r := l.peek()
|
||||||
|
if r == '+' || r == '-' {
|
||||||
|
l.next()
|
||||||
|
}
|
||||||
} else if isDigit(next) {
|
} else if isDigit(next) {
|
||||||
digitSeen = true
|
digitSeen = true
|
||||||
|
l.next()
|
||||||
|
} else if next == '_' {
|
||||||
|
l.next()
|
||||||
} else {
|
} else {
|
||||||
l.backup()
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if pointSeen && !digitSeen {
|
if pointSeen && !digitSeen {
|
||||||
@@ -443,7 +620,7 @@ func (l *tomlLexer) lexNumber() tomlLexStateFn {
|
|||||||
if !digitSeen {
|
if !digitSeen {
|
||||||
return l.errorf("no digit in that number")
|
return l.errorf("no digit in that number")
|
||||||
}
|
}
|
||||||
if pointSeen {
|
if pointSeen || expSeen {
|
||||||
l.emit(tokenFloat)
|
l.emit(tokenFloat)
|
||||||
} else {
|
} else {
|
||||||
l.emit(tokenInteger)
|
l.emit(tokenInteger)
|
||||||
@@ -451,17 +628,27 @@ func (l *tomlLexer) lexNumber() tomlLexStateFn {
|
|||||||
return l.lexRvalue
|
return l.lexRvalue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *tomlLexer) run() {
|
||||||
|
for state := l.lexVoid; state != nil; {
|
||||||
|
state = state()
|
||||||
|
}
|
||||||
|
close(l.tokens)
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
dateRegexp = regexp.MustCompile("^\\d{1,4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")
|
dateRegexp = regexp.MustCompile(`^\d{1,4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry point
|
// Entry point
|
||||||
func lexToml(input string) chan token {
|
func lexToml(input io.Reader) chan token {
|
||||||
|
bufferedInput := buffruneio.NewReader(input)
|
||||||
l := &tomlLexer{
|
l := &tomlLexer{
|
||||||
input: input,
|
input: bufferedInput,
|
||||||
tokens: make(chan token),
|
tokens: make(chan token),
|
||||||
line: 1,
|
line: 1,
|
||||||
col: 1,
|
col: 1,
|
||||||
|
endbufferLine: 1,
|
||||||
|
endbufferCol: 1,
|
||||||
}
|
}
|
||||||
go l.run()
|
go l.run()
|
||||||
return l.tokens
|
return l.tokens
|
||||||
|
|||||||
+310
-21
@@ -1,18 +1,23 @@
|
|||||||
package toml
|
package toml
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func testFlow(t *testing.T, input string, expectedFlow []token) {
|
func testFlow(t *testing.T, input string, expectedFlow []token) {
|
||||||
ch := lexToml(input)
|
ch := lexToml(strings.NewReader(input))
|
||||||
for _, expected := range expectedFlow {
|
for _, expected := range expectedFlow {
|
||||||
token := <-ch
|
token := <-ch
|
||||||
if token != expected {
|
if token != expected {
|
||||||
t.Log("While testing: ", input)
|
t.Log("While testing: ", input)
|
||||||
|
t.Log("compared (got)", token, "to (expected)", expected)
|
||||||
|
t.Log("\tvalue:", token.val, "<->", expected.val)
|
||||||
|
t.Log("\tvalue as bytes:", []byte(token.val), "<->", []byte(expected.val))
|
||||||
|
t.Log("\ttype:", token.typ.String(), "<->", expected.typ.String())
|
||||||
|
t.Log("\tline:", token.Line, "<->", expected.Line)
|
||||||
|
t.Log("\tcolumn:", token.Col, "<->", expected.Col)
|
||||||
t.Log("compared", token, "to", expected)
|
t.Log("compared", token, "to", expected)
|
||||||
t.Log(token.val, "<->", expected.val)
|
|
||||||
t.Log(token.typ, "<->", expected.typ)
|
|
||||||
t.Log(token.Line, "<->", expected.Line)
|
|
||||||
t.Log(token.Col, "<->", expected.Col)
|
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +44,15 @@ func TestValidKeyGroup(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNestedQuotedUnicodeKeyGroup(t *testing.T) {
|
||||||
|
testFlow(t, `[ j . "ʞ" . l ]`, []token{
|
||||||
|
token{Position{1, 1}, tokenLeftBracket, "["},
|
||||||
|
token{Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l `},
|
||||||
|
token{Position{1, 15}, tokenRightBracket, "]"},
|
||||||
|
token{Position{1, 16}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnclosedKeyGroup(t *testing.T) {
|
func TestUnclosedKeyGroup(t *testing.T) {
|
||||||
testFlow(t, "[hello world", []token{
|
testFlow(t, "[hello world", []token{
|
||||||
token{Position{1, 1}, tokenLeftBracket, "["},
|
token{Position{1, 1}, tokenLeftBracket, "["},
|
||||||
@@ -73,6 +87,18 @@ func TestMultipleKeyGroupsComment(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSimpleWindowsCRLF(t *testing.T) {
|
||||||
|
testFlow(t, "a=4\r\nb=2", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 2}, tokenEqual, "="},
|
||||||
|
token{Position{1, 3}, tokenInteger, "4"},
|
||||||
|
token{Position{2, 1}, tokenKey, "b"},
|
||||||
|
token{Position{2, 2}, tokenEqual, "="},
|
||||||
|
token{Position{2, 3}, tokenInteger, "2"},
|
||||||
|
token{Position{2, 4}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestBasicKey(t *testing.T) {
|
func TestBasicKey(t *testing.T) {
|
||||||
testFlow(t, "hello", []token{
|
testFlow(t, "hello", []token{
|
||||||
token{Position{1, 1}, tokenKey, "hello"},
|
token{Position{1, 1}, tokenKey, "hello"},
|
||||||
@@ -118,19 +144,13 @@ func TestBasicKeyAndEqual(t *testing.T) {
|
|||||||
|
|
||||||
func TestKeyWithSharpAndEqual(t *testing.T) {
|
func TestKeyWithSharpAndEqual(t *testing.T) {
|
||||||
testFlow(t, "key#name = 5", []token{
|
testFlow(t, "key#name = 5", []token{
|
||||||
token{Position{1, 1}, tokenKey, "key#name"},
|
token{Position{1, 1}, tokenError, "keys cannot contain # character"},
|
||||||
token{Position{1, 10}, tokenEqual, "="},
|
|
||||||
token{Position{1, 12}, tokenInteger, "5"},
|
|
||||||
token{Position{1, 13}, tokenEOF, ""},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeyWithSymbolsAndEqual(t *testing.T) {
|
func TestKeyWithSymbolsAndEqual(t *testing.T) {
|
||||||
testFlow(t, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:' = 5", []token{
|
testFlow(t, "~!@$^&*()_+-`1234567890[]\\|/?><.,;:' = 5", []token{
|
||||||
token{Position{1, 1}, tokenKey, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:'"},
|
token{Position{1, 1}, tokenError, "keys cannot contain ~ character"},
|
||||||
token{Position{1, 39}, tokenEqual, "="},
|
|
||||||
token{Position{1, 41}, tokenInteger, "5"},
|
|
||||||
token{Position{1, 42}, tokenEOF, ""},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +296,13 @@ func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
|
|||||||
|
|
||||||
func TestDateRegexp(t *testing.T) {
|
func TestDateRegexp(t *testing.T) {
|
||||||
if dateRegexp.FindString("1979-05-27T07:32:00Z") == "" {
|
if dateRegexp.FindString("1979-05-27T07:32:00Z") == "" {
|
||||||
t.Fail()
|
t.Error("basic lexing")
|
||||||
|
}
|
||||||
|
if dateRegexp.FindString("1979-05-27T00:32:00-07:00") == "" {
|
||||||
|
t.Error("offset lexing")
|
||||||
|
}
|
||||||
|
if dateRegexp.FindString("1979-05-27T00:32:00.999999-07:00") == "" {
|
||||||
|
t.Error("nano precision lexing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +313,18 @@ func TestKeyEqualDate(t *testing.T) {
|
|||||||
token{Position{1, 7}, tokenDate, "1979-05-27T07:32:00Z"},
|
token{Position{1, 7}, tokenDate, "1979-05-27T07:32:00Z"},
|
||||||
token{Position{1, 27}, tokenEOF, ""},
|
token{Position{1, 27}, tokenEOF, ""},
|
||||||
})
|
})
|
||||||
|
testFlow(t, "foo = 1979-05-27T00:32:00-07:00", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 7}, tokenDate, "1979-05-27T00:32:00-07:00"},
|
||||||
|
token{Position{1, 32}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
testFlow(t, "foo = 1979-05-27T00:32:00.999999-07:00", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 7}, tokenDate, "1979-05-27T00:32:00.999999-07:00"},
|
||||||
|
token{Position{1, 39}, tokenEOF, ""},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFloatEndingWithDot(t *testing.T) {
|
func TestFloatEndingWithDot(t *testing.T) {
|
||||||
@@ -305,11 +343,48 @@ func TestFloatWithTwoDots(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoubleEqualKey(t *testing.T) {
|
func TestFloatWithExponent1(t *testing.T) {
|
||||||
testFlow(t, "foo= = 2", []token{
|
testFlow(t, "a = 5e+22", []token{
|
||||||
token{Position{1, 1}, tokenKey, "foo"},
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
token{Position{1, 4}, tokenEqual, "="},
|
token{Position{1, 3}, tokenEqual, "="},
|
||||||
token{Position{1, 5}, tokenError, "cannot have multiple equals for the same key"},
|
token{Position{1, 5}, tokenFloat, "5e+22"},
|
||||||
|
token{Position{1, 10}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloatWithExponent2(t *testing.T) {
|
||||||
|
testFlow(t, "a = 5E+22", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 3}, tokenEqual, "="},
|
||||||
|
token{Position{1, 5}, tokenFloat, "5E+22"},
|
||||||
|
token{Position{1, 10}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloatWithExponent3(t *testing.T) {
|
||||||
|
testFlow(t, "a = -5e+22", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 3}, tokenEqual, "="},
|
||||||
|
token{Position{1, 5}, tokenFloat, "-5e+22"},
|
||||||
|
token{Position{1, 11}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloatWithExponent4(t *testing.T) {
|
||||||
|
testFlow(t, "a = -5e-22", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 3}, tokenEqual, "="},
|
||||||
|
token{Position{1, 5}, tokenFloat, "-5e-22"},
|
||||||
|
token{Position{1, 11}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloatWithExponent5(t *testing.T) {
|
||||||
|
testFlow(t, "a = 6.626e-34", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 3}, tokenEqual, "="},
|
||||||
|
token{Position{1, 5}, tokenFloat, "6.626e-34"},
|
||||||
|
token{Position{1, 14}, tokenEOF, ""},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +452,40 @@ func TestKeyEqualNumber(t *testing.T) {
|
|||||||
token{Position{1, 7}, tokenFloat, "-4.2"},
|
token{Position{1, 7}, tokenFloat, "-4.2"},
|
||||||
token{Position{1, 11}, tokenEOF, ""},
|
token{Position{1, 11}, tokenEOF, ""},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
testFlow(t, "foo = 1_000", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 7}, tokenInteger, "1_000"},
|
||||||
|
token{Position{1, 12}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "foo = 5_349_221", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 7}, tokenInteger, "5_349_221"},
|
||||||
|
token{Position{1, 16}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "foo = 1_2_3_4_5", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 7}, tokenInteger, "1_2_3_4_5"},
|
||||||
|
token{Position{1, 16}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "flt8 = 9_224_617.445_991_228_313", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "flt8"},
|
||||||
|
token{Position{1, 6}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenFloat, "9_224_617.445_991_228_313"},
|
||||||
|
token{Position{1, 33}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "foo = +", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 7}, tokenError, "no digit in that number"},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiline(t *testing.T) {
|
func TestMultiline(t *testing.T) {
|
||||||
@@ -398,6 +507,140 @@ func TestKeyEqualStringUnicodeEscape(t *testing.T) {
|
|||||||
token{Position{1, 8}, tokenString, "hello ♥"},
|
token{Position{1, 8}, tokenString, "hello ♥"},
|
||||||
token{Position{1, 21}, tokenEOF, ""},
|
token{Position{1, 21}, tokenEOF, ""},
|
||||||
})
|
})
|
||||||
|
testFlow(t, `foo = "hello \U000003B4"`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenString, "hello δ"},
|
||||||
|
token{Position{1, 25}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
testFlow(t, `foo = "\u2"`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenError, "unfinished unicode escape"},
|
||||||
|
})
|
||||||
|
testFlow(t, `foo = "\U2"`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenError, "unfinished unicode escape"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyEqualStringNoEscape(t *testing.T) {
|
||||||
|
testFlow(t, "foo = \"hello \u0002\"", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenError, "unescaped control character U+0002"},
|
||||||
|
})
|
||||||
|
testFlow(t, "foo = \"hello \u001F\"", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenError, "unescaped control character U+001F"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiteralString(t *testing.T) {
|
||||||
|
testFlow(t, `foo = 'C:\Users\nodejs\templates'`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenString, `C:\Users\nodejs\templates`},
|
||||||
|
token{Position{1, 34}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
testFlow(t, `foo = '\\ServerX\admin$\system32\'`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenString, `\\ServerX\admin$\system32\`},
|
||||||
|
token{Position{1, 35}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
testFlow(t, `foo = 'Tom "Dubs" Preston-Werner'`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenString, `Tom "Dubs" Preston-Werner`},
|
||||||
|
token{Position{1, 34}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
testFlow(t, `foo = '<\i\c*\s*>'`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenString, `<\i\c*\s*>`},
|
||||||
|
token{Position{1, 19}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
testFlow(t, `foo = 'C:\Users\nodejs\unfinis`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenError, "unclosed string"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultilineLiteralString(t *testing.T) {
|
||||||
|
testFlow(t, `foo = '''hello 'literal' world'''`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 10}, tokenString, `hello 'literal' world`},
|
||||||
|
token{Position{1, 34}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "foo = '''\nhello\n'literal'\nworld'''", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{2, 1}, tokenString, "hello\n'literal'\nworld"},
|
||||||
|
token{Position{4, 9}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
testFlow(t, "foo = '''\r\nhello\r\n'literal'\r\nworld'''", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{2, 1}, tokenString, "hello\r\n'literal'\r\nworld"},
|
||||||
|
token{Position{4, 9}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultilineString(t *testing.T) {
|
||||||
|
testFlow(t, `foo = """hello "literal" world"""`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 10}, tokenString, `hello "literal" world`},
|
||||||
|
token{Position{1, 34}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "foo = \"\"\"\r\nhello\\\r\n\"literal\"\\\nworld\"\"\"", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{2, 1}, tokenString, "hello\"literal\"world"},
|
||||||
|
token{Position{4, 9}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "foo = \"\"\"\\\n \\\n \\\n hello\\\nmultiline\\\nworld\"\"\"", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 10}, tokenString, "hellomultilineworld"},
|
||||||
|
token{Position{6, 9}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "key2 = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "key2"},
|
||||||
|
token{Position{1, 6}, tokenEqual, "="},
|
||||||
|
token{Position{2, 1}, tokenString, "The quick brown fox jumps over the lazy dog."},
|
||||||
|
token{Position{6, 21}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "key2 = \"\"\"\\\n The quick brown \\\n fox jumps over \\\n the lazy dog.\\\n \"\"\"", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "key2"},
|
||||||
|
token{Position{1, 6}, tokenEqual, "="},
|
||||||
|
token{Position{1, 11}, tokenString, "The quick brown fox jumps over the lazy dog."},
|
||||||
|
token{Position{5, 11}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, `key2 = "Roses are red\nViolets are blue"`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "key2"},
|
||||||
|
token{Position{1, 6}, tokenEqual, "="},
|
||||||
|
token{Position{1, 9}, tokenString, "Roses are red\nViolets are blue"},
|
||||||
|
token{Position{1, 41}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, "key2 = \"\"\"\nRoses are red\nViolets are blue\"\"\"", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "key2"},
|
||||||
|
token{Position{1, 6}, tokenEqual, "="},
|
||||||
|
token{Position{2, 1}, tokenString, "Roses are red\nViolets are blue"},
|
||||||
|
token{Position{3, 20}, tokenEOF, ""},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnicodeString(t *testing.T) {
|
func TestUnicodeString(t *testing.T) {
|
||||||
@@ -408,6 +651,14 @@ func TestUnicodeString(t *testing.T) {
|
|||||||
token{Position{1, 22}, tokenEOF, ""},
|
token{Position{1, 22}, tokenEOF, ""},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
func TestEscapeInString(t *testing.T) {
|
||||||
|
testFlow(t, `foo = "\b\f\/"`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "foo"},
|
||||||
|
token{Position{1, 5}, tokenEqual, "="},
|
||||||
|
token{Position{1, 8}, tokenString, "\b\f/"},
|
||||||
|
token{Position{1, 15}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestKeyGroupArray(t *testing.T) {
|
func TestKeyGroupArray(t *testing.T) {
|
||||||
testFlow(t, "[[foo]]", []token{
|
testFlow(t, "[[foo]]", []token{
|
||||||
@@ -417,3 +668,41 @@ func TestKeyGroupArray(t *testing.T) {
|
|||||||
token{Position{1, 8}, tokenEOF, ""},
|
token{Position{1, 8}, tokenEOF, ""},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQuotedKey(t *testing.T) {
|
||||||
|
testFlow(t, "\"a b\" = 42", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "\"a b\""},
|
||||||
|
token{Position{1, 7}, tokenEqual, "="},
|
||||||
|
token{Position{1, 9}, tokenInteger, "42"},
|
||||||
|
token{Position{1, 11}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyNewline(t *testing.T) {
|
||||||
|
testFlow(t, "a\n= 4", []token{
|
||||||
|
token{Position{1, 1}, tokenError, "keys cannot contain new lines"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidFloat(t *testing.T) {
|
||||||
|
testFlow(t, "a=7e1_", []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 2}, tokenEqual, "="},
|
||||||
|
token{Position{1, 3}, tokenFloat, "7e1_"},
|
||||||
|
token{Position{1, 7}, tokenEOF, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLexUnknownRvalue(t *testing.T) {
|
||||||
|
testFlow(t, `a = !b`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 3}, tokenEqual, "="},
|
||||||
|
token{Position{1, 5}, tokenError, "no value can start with !"},
|
||||||
|
})
|
||||||
|
|
||||||
|
testFlow(t, `a = \b`, []token{
|
||||||
|
token{Position{1, 1}, tokenKey, "a"},
|
||||||
|
token{Position{1, 3}, tokenEqual, "="},
|
||||||
|
token{Position{1, 5}, tokenError, `no value can start with \`},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,7 +67,14 @@ func newMatchKeyFn(name string) *matchKeyFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
|
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
|
||||||
if tree, ok := node.(*TomlTree); ok {
|
if array, ok := node.([]*TomlTree); ok {
|
||||||
|
for _, tree := range array {
|
||||||
|
item := tree.values[f.Name]
|
||||||
|
if item != nil {
|
||||||
|
f.next.call(item, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if tree, ok := node.(*TomlTree); ok {
|
||||||
item := tree.values[f.Name]
|
item := tree.values[f.Name]
|
||||||
if item != nil {
|
if item != nil {
|
||||||
f.next.call(item, ctx)
|
f.next.call(item, ctx)
|
||||||
@@ -202,7 +209,7 @@ func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
|
|||||||
fn, ok := (*ctx.filters)[f.Name]
|
fn, ok := (*ctx.filters)[f.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("%s: query context does not have filter '%s'",
|
panic(fmt.Sprintf("%s: query context does not have filter '%s'",
|
||||||
f.Pos, f.Name))
|
f.Pos.String(), f.Name))
|
||||||
}
|
}
|
||||||
switch castNode := tomlValueCheck(node, ctx).(type) {
|
switch castNode := tomlValueCheck(node, ctx).(type) {
|
||||||
case *TomlTree:
|
case *TomlTree:
|
||||||
|
|||||||
+3
-4
@@ -2,7 +2,6 @@ package toml
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ func TestPathSliceStart(t *testing.T) {
|
|||||||
assertPath(t,
|
assertPath(t,
|
||||||
"$[123:]",
|
"$[123:]",
|
||||||
buildPath(
|
buildPath(
|
||||||
newMatchSliceFn(123, math.MaxInt64, 1),
|
newMatchSliceFn(123, maxInt, 1),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +133,7 @@ func TestPathSliceStartStep(t *testing.T) {
|
|||||||
assertPath(t,
|
assertPath(t,
|
||||||
"$[123::7]",
|
"$[123::7]",
|
||||||
buildPath(
|
buildPath(
|
||||||
newMatchSliceFn(123, math.MaxInt64, 7),
|
newMatchSliceFn(123, maxInt, 7),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@ func TestPathSliceStep(t *testing.T) {
|
|||||||
assertPath(t,
|
assertPath(t,
|
||||||
"$[::7]",
|
"$[::7]",
|
||||||
buildPath(
|
buildPath(
|
||||||
newMatchSliceFn(0, math.MaxInt64, 7),
|
newMatchSliceFn(0, maxInt, 7),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package toml
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -98,13 +99,16 @@ func (p *tomlParser) parseGroupArray() tomlParserStateFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get or create group array element at the indicated part in the path
|
// get or create group array element at the indicated part in the path
|
||||||
keys := strings.Split(key.val, ".")
|
keys, err := parseKey(key.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(key, "invalid group array key: %s", err)
|
||||||
|
}
|
||||||
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 []*TomlTree
|
||||||
if destTree == nil {
|
if destTree == nil {
|
||||||
array = make([]*TomlTree, 0)
|
array = make([]*TomlTree, 0)
|
||||||
} else if destTree.([]*TomlTree) != nil {
|
} else if target, ok := destTree.([]*TomlTree); ok && target != nil {
|
||||||
array = destTree.([]*TomlTree)
|
array = destTree.([]*TomlTree)
|
||||||
} else {
|
} else {
|
||||||
p.raiseError(key, "key %s is already assigned and not of type group array", key)
|
p.raiseError(key, "key %s is already assigned and not of type group array", key)
|
||||||
@@ -153,7 +157,10 @@ func (p *tomlParser) parseGroup() tomlParserStateFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.seenGroupKeys = append(p.seenGroupKeys, key.val)
|
p.seenGroupKeys = append(p.seenGroupKeys, key.val)
|
||||||
keys := strings.Split(key.val, ".")
|
keys, err := parseKey(key.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(key, "invalid group array key: %s", err)
|
||||||
|
}
|
||||||
if err := p.tree.createSubTree(keys, startToken.Position); err != nil {
|
if err := p.tree.createSubTree(keys, startToken.Position); err != nil {
|
||||||
p.raiseError(key, "%s", err)
|
p.raiseError(key, "%s", err)
|
||||||
}
|
}
|
||||||
@@ -165,6 +172,7 @@ func (p *tomlParser) parseGroup() tomlParserStateFn {
|
|||||||
func (p *tomlParser) parseAssign() tomlParserStateFn {
|
func (p *tomlParser) parseAssign() tomlParserStateFn {
|
||||||
key := p.getToken()
|
key := p.getToken()
|
||||||
p.assume(tokenEqual)
|
p.assume(tokenEqual)
|
||||||
|
|
||||||
value := p.parseRvalue()
|
value := p.parseRvalue()
|
||||||
var groupKey []string
|
var groupKey []string
|
||||||
if len(p.currentGroup) > 0 {
|
if len(p.currentGroup) > 0 {
|
||||||
@@ -186,16 +194,42 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// assign value to the found group
|
// assign value to the found group
|
||||||
localKey := []string{key.val}
|
keyVals, err := parseKey(key.val)
|
||||||
finalKey := append(groupKey, key.val)
|
if err != nil {
|
||||||
|
p.raiseError(key, "%s", err)
|
||||||
|
}
|
||||||
|
if len(keyVals) != 1 {
|
||||||
|
p.raiseError(key, "Invalid key")
|
||||||
|
}
|
||||||
|
keyVal := keyVals[0]
|
||||||
|
localKey := []string{keyVal}
|
||||||
|
finalKey := append(groupKey, keyVal)
|
||||||
if targetNode.GetPath(localKey) != nil {
|
if targetNode.GetPath(localKey) != nil {
|
||||||
p.raiseError(key, "The following key was defined twice: %s",
|
p.raiseError(key, "The following key was defined twice: %s",
|
||||||
strings.Join(finalKey, "."))
|
strings.Join(finalKey, "."))
|
||||||
}
|
}
|
||||||
targetNode.values[key.val] = &tomlValue{value, key.Position}
|
var toInsert interface{}
|
||||||
|
|
||||||
|
switch value.(type) {
|
||||||
|
case *TomlTree:
|
||||||
|
toInsert = value
|
||||||
|
default:
|
||||||
|
toInsert = &tomlValue{value, key.Position}
|
||||||
|
}
|
||||||
|
targetNode.values[keyVal] = toInsert
|
||||||
return p.parseStart
|
return p.parseStart
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var numberUnderscoreInvalidRegexp *regexp.Regexp
|
||||||
|
|
||||||
|
func cleanupNumberToken(value string) (string, error) {
|
||||||
|
if numberUnderscoreInvalidRegexp.MatchString(value) {
|
||||||
|
return "", fmt.Errorf("invalid use of _ in number")
|
||||||
|
}
|
||||||
|
cleanedVal := strings.Replace(value, "_", "", -1)
|
||||||
|
return cleanedVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *tomlParser) parseRvalue() interface{} {
|
func (p *tomlParser) parseRvalue() interface{} {
|
||||||
tok := p.getToken()
|
tok := p.getToken()
|
||||||
if tok == nil || tok.typ == tokenEOF {
|
if tok == nil || tok.typ == tokenEOF {
|
||||||
@@ -210,25 +244,37 @@ func (p *tomlParser) parseRvalue() interface{} {
|
|||||||
case tokenFalse:
|
case tokenFalse:
|
||||||
return false
|
return false
|
||||||
case tokenInteger:
|
case tokenInteger:
|
||||||
val, err := strconv.ParseInt(tok.val, 10, 64)
|
cleanedVal, err := cleanupNumberToken(tok.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(tok, "%s", err)
|
||||||
|
}
|
||||||
|
val, err := strconv.ParseInt(cleanedVal, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.raiseError(tok, "%s", err)
|
p.raiseError(tok, "%s", err)
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
case tokenFloat:
|
case tokenFloat:
|
||||||
val, err := strconv.ParseFloat(tok.val, 64)
|
cleanedVal, err := cleanupNumberToken(tok.val)
|
||||||
|
if err != nil {
|
||||||
|
p.raiseError(tok, "%s", err)
|
||||||
|
}
|
||||||
|
val, err := strconv.ParseFloat(cleanedVal, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.raiseError(tok, "%s", err)
|
p.raiseError(tok, "%s", err)
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
case tokenDate:
|
case tokenDate:
|
||||||
val, err := time.Parse(time.RFC3339, tok.val)
|
val, err := time.ParseInLocation(time.RFC3339Nano, tok.val, time.UTC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.raiseError(tok, "%s", err)
|
p.raiseError(tok, "%s", err)
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
case tokenLeftBracket:
|
case tokenLeftBracket:
|
||||||
return p.parseArray()
|
return p.parseArray()
|
||||||
|
case tokenLeftCurlyBrace:
|
||||||
|
return p.parseInlineTable()
|
||||||
|
case tokenEqual:
|
||||||
|
p.raiseError(tok, "cannot have multiple equals for the same key")
|
||||||
case tokenError:
|
case tokenError:
|
||||||
p.raiseError(tok, "%s", tok)
|
p.raiseError(tok, "%s", tok)
|
||||||
}
|
}
|
||||||
@@ -238,7 +284,51 @@ func (p *tomlParser) parseRvalue() interface{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *tomlParser) parseArray() []interface{} {
|
func tokenIsComma(t *token) bool {
|
||||||
|
return t != nil && t.typ == tokenComma
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseInlineTable() *TomlTree {
|
||||||
|
tree := newTomlTree()
|
||||||
|
var previous *token
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
follow := p.peek()
|
||||||
|
if follow == nil || follow.typ == tokenEOF {
|
||||||
|
p.raiseError(follow, "unterminated inline table")
|
||||||
|
}
|
||||||
|
switch follow.typ {
|
||||||
|
case tokenRightCurlyBrace:
|
||||||
|
p.getToken()
|
||||||
|
break Loop
|
||||||
|
case tokenKey:
|
||||||
|
if !tokenIsComma(previous) && previous != nil {
|
||||||
|
p.raiseError(follow, "comma expected between fields in inline table")
|
||||||
|
}
|
||||||
|
key := p.getToken()
|
||||||
|
p.assume(tokenEqual)
|
||||||
|
value := p.parseRvalue()
|
||||||
|
tree.Set(key.val, value)
|
||||||
|
case tokenComma:
|
||||||
|
if previous == nil {
|
||||||
|
p.raiseError(follow, "inline table cannot start with a comma")
|
||||||
|
}
|
||||||
|
if tokenIsComma(previous) {
|
||||||
|
p.raiseError(follow, "need field between two commas in inline table")
|
||||||
|
}
|
||||||
|
p.getToken()
|
||||||
|
default:
|
||||||
|
p.raiseError(follow, "unexpected token type in inline table: %s", follow.typ.String())
|
||||||
|
}
|
||||||
|
previous = follow
|
||||||
|
}
|
||||||
|
if tokenIsComma(previous) {
|
||||||
|
p.raiseError(previous, "trailing comma at the end of inline table")
|
||||||
|
}
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *tomlParser) parseArray() interface{} {
|
||||||
var array []interface{}
|
var array []interface{}
|
||||||
arrayType := reflect.TypeOf(nil)
|
arrayType := reflect.TypeOf(nil)
|
||||||
for {
|
for {
|
||||||
@@ -248,7 +338,7 @@ func (p *tomlParser) parseArray() []interface{} {
|
|||||||
}
|
}
|
||||||
if follow.typ == tokenRightBracket {
|
if follow.typ == tokenRightBracket {
|
||||||
p.getToken()
|
p.getToken()
|
||||||
return array
|
break
|
||||||
}
|
}
|
||||||
val := p.parseRvalue()
|
val := p.parseRvalue()
|
||||||
if arrayType == nil {
|
if arrayType == nil {
|
||||||
@@ -259,7 +349,7 @@ func (p *tomlParser) parseArray() []interface{} {
|
|||||||
}
|
}
|
||||||
array = append(array, val)
|
array = append(array, val)
|
||||||
follow = p.peek()
|
follow = p.peek()
|
||||||
if follow == nil {
|
if follow == nil || follow.typ == tokenEOF {
|
||||||
p.raiseError(follow, "unterminated array")
|
p.raiseError(follow, "unterminated array")
|
||||||
}
|
}
|
||||||
if follow.typ != tokenRightBracket && follow.typ != tokenComma {
|
if follow.typ != tokenRightBracket && follow.typ != tokenComma {
|
||||||
@@ -269,6 +359,17 @@ func (p *tomlParser) parseArray() []interface{} {
|
|||||||
p.getToken()
|
p.getToken()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// An array of TomlTrees is actually an array of inline
|
||||||
|
// tables, which is a shorthand for a table array. If the
|
||||||
|
// array was not converted from []interface{} to []*TomlTree,
|
||||||
|
// the two notations would not be equivalent.
|
||||||
|
if arrayType == reflect.TypeOf(newTomlTree()) {
|
||||||
|
tomlArray := make([]*TomlTree, len(array))
|
||||||
|
for i, v := range array {
|
||||||
|
tomlArray[i] = v.(*TomlTree)
|
||||||
|
}
|
||||||
|
return tomlArray
|
||||||
|
}
|
||||||
return array
|
return array
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,3 +386,7 @@ func parseToml(flow chan token) *TomlTree {
|
|||||||
parser.run()
|
parser.run()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
numberUnderscoreInvalidRegexp = regexp.MustCompile(`([^\d]_|_[^\d]|_$|^_)`)
|
||||||
|
}
|
||||||
|
|||||||
+330
-10
@@ -2,26 +2,34 @@ package toml
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
)
|
)
|
||||||
|
|
||||||
func assertTree(t *testing.T, tree *TomlTree, err error, ref map[string]interface{}) {
|
func assertSubTree(t *testing.T, path []string, tree *TomlTree, 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
|
||||||
}
|
}
|
||||||
for k, v := range ref {
|
for k, v := range ref {
|
||||||
|
nextPath := append(path, k)
|
||||||
|
t.Log("asserting path", nextPath)
|
||||||
// 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 []*TomlTree:
|
||||||
|
t.Log("\tcomparing key", nextPath, "by array iteration")
|
||||||
for idx, item := range node {
|
for idx, item := range node {
|
||||||
assertTree(t, item, err, v.([]map[string]interface{})[idx])
|
assertSubTree(t, nextPath, item, err, v.([]map[string]interface{})[idx])
|
||||||
}
|
}
|
||||||
case *TomlTree:
|
case *TomlTree:
|
||||||
assertTree(t, node, err, v.(map[string]interface{}))
|
t.Log("\tcomparing key", nextPath, "by subtree assestion")
|
||||||
|
assertSubTree(t, nextPath, node, err, v.(map[string]interface{}))
|
||||||
default:
|
default:
|
||||||
|
t.Log("\tcomparing key", nextPath, "by string representation because it's of type", reflect.TypeOf(node))
|
||||||
if fmt.Sprintf("%v", node) != fmt.Sprintf("%v", v) {
|
if fmt.Sprintf("%v", node) != fmt.Sprintf("%v", v) {
|
||||||
t.Errorf("was expecting %v at %v but got %v", v, k, node)
|
t.Errorf("was expecting %v at %v but got %v", v, k, node)
|
||||||
}
|
}
|
||||||
@@ -29,6 +37,12 @@ func assertTree(t *testing.T, tree *TomlTree, err error, ref map[string]interfac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertTree(t *testing.T, tree *TomlTree, err error, ref map[string]interface{}) {
|
||||||
|
t.Log("Asserting tree:\n", spew.Sdump(tree))
|
||||||
|
assertSubTree(t, []string{}, tree, err, ref)
|
||||||
|
t.Log("Finished tree assertion.")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateSubTree(t *testing.T) {
|
func TestCreateSubTree(t *testing.T) {
|
||||||
tree := newTomlTree()
|
tree := newTomlTree()
|
||||||
tree.createSubTree([]string{"a", "b", "c"}, Position{})
|
tree.createSubTree([]string{"a", "b", "c"}, Position{})
|
||||||
@@ -51,12 +65,10 @@ func TestSimpleKV(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: from the BurntSushi test suite
|
func TestNumberInKey(t *testing.T) {
|
||||||
// NOTE: this test is pure evil due to the embedded '.'
|
tree, err := Load("hello2 = 42")
|
||||||
func TestSpecialKV(t *testing.T) {
|
|
||||||
tree, err := Load("~!@#$^&*()_+-`1234567890[]\\|/?><.,;: = 1")
|
|
||||||
assertTree(t, tree, err, map[string]interface{}{
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
"~!@#$^&*()_+-`1234567890[]\\|/?><.,;:": int64(1),
|
"hello2": int64(42),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +82,44 @@ func TestSimpleNumbers(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNumbersWithUnderscores(t *testing.T) {
|
||||||
|
tree, err := Load("a = 1_000")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"a": int64(1000),
|
||||||
|
})
|
||||||
|
|
||||||
|
tree, err = Load("a = 5_349_221")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"a": int64(5349221),
|
||||||
|
})
|
||||||
|
|
||||||
|
tree, err = Load("a = 1_2_3_4_5")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"a": int64(12345),
|
||||||
|
})
|
||||||
|
|
||||||
|
tree, err = Load("flt8 = 9_224_617.445_991_228_313")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"flt8": float64(9224617.445991228313),
|
||||||
|
})
|
||||||
|
|
||||||
|
tree, err = Load("flt9 = 1e1_00")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"flt9": float64(1e100),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloatsWithExponents(t *testing.T) {
|
||||||
|
tree, err := Load("a = 5e+22\nb = 5E+22\nc = -5e+22\nd = -5e-22\ne = 6.626e-34")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"a": float64(5e+22),
|
||||||
|
"b": float64(5E+22),
|
||||||
|
"c": float64(-5e+22),
|
||||||
|
"d": float64(-5e-22),
|
||||||
|
"e": float64(6.626e-34),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestSimpleDate(t *testing.T) {
|
func TestSimpleDate(t *testing.T) {
|
||||||
tree, err := Load("a = 1979-05-27T07:32:00Z")
|
tree, err := Load("a = 1979-05-27T07:32:00Z")
|
||||||
assertTree(t, tree, err, map[string]interface{}{
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
@@ -77,6 +127,20 @@ func TestSimpleDate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDateOffset(t *testing.T) {
|
||||||
|
tree, err := Load("a = 1979-05-27T00:32:00-07:00")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"a": time.Date(1979, time.May, 27, 0, 32, 0, 0, time.FixedZone("", -7*60*60)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateNano(t *testing.T) {
|
||||||
|
tree, err := Load("a = 1979-05-27T00:32:00.999999999-07:00")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"a": time.Date(1979, time.May, 27, 0, 32, 0, 999999999, time.FixedZone("", -7*60*60)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestSimpleString(t *testing.T) {
|
func TestSimpleString(t *testing.T) {
|
||||||
tree, err := Load("a = \"hello world\"")
|
tree, err := Load("a = \"hello world\"")
|
||||||
assertTree(t, tree, err, map[string]interface{}{
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
@@ -84,6 +148,13 @@ func TestSimpleString(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSpaceKey(t *testing.T) {
|
||||||
|
tree, err := Load("\"a b\" = \"hello world\"")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"a b": "hello world",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStringEscapables(t *testing.T) {
|
func TestStringEscapables(t *testing.T) {
|
||||||
tree, err := Load("a = \"a \\n b\"")
|
tree, err := Load("a = \"a \\n b\"")
|
||||||
assertTree(t, tree, err, map[string]interface{}{
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
@@ -127,6 +198,41 @@ func TestNestedKeys(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNestedQuotedUnicodeKeys(t *testing.T) {
|
||||||
|
tree, err := Load("[ j . \"ʞ\" . l ]\nd = 42")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"j": map[string]interface{}{
|
||||||
|
"ʞ": map[string]interface{}{
|
||||||
|
"l": map[string]interface{}{
|
||||||
|
"d": int64(42),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
tree, err = Load("[ g . h . i ]\nd = 42")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"g": map[string]interface{}{
|
||||||
|
"h": map[string]interface{}{
|
||||||
|
"i": map[string]interface{}{
|
||||||
|
"d": int64(42),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
tree, err = Load("[ d.e.f ]\nk = 42")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"d": map[string]interface{}{
|
||||||
|
"e": map[string]interface{}{
|
||||||
|
"f": map[string]interface{}{
|
||||||
|
"k": int64(42),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestArrayOne(t *testing.T) {
|
func TestArrayOne(t *testing.T) {
|
||||||
tree, err := Load("a = [1]")
|
tree, err := Load("a = [1]")
|
||||||
assertTree(t, tree, err, map[string]interface{}{
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
@@ -193,9 +299,21 @@ func TestArrayNestedStrings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseUnknownRvalue(t *testing.T) {
|
||||||
|
_, err := Load("a = !bssss")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expecting a parse error")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load("a = /b")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expecting a parse error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMissingValue(t *testing.T) {
|
func TestMissingValue(t *testing.T) {
|
||||||
_, err := Load("a = ")
|
_, err := Load("a = ")
|
||||||
if err.Error() != "(1, 4): expecting a value" {
|
if err.Error() != "(1, 5): expecting a value" {
|
||||||
t.Error("Bad error message:", err.Error())
|
t.Error("Bad error message:", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,6 +323,16 @@ func TestUnterminatedArray(t *testing.T) {
|
|||||||
if err.Error() != "(1, 8): unterminated array" {
|
if err.Error() != "(1, 8): unterminated array" {
|
||||||
t.Error("Bad error message:", err.Error())
|
t.Error("Bad error message:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = Load("a = [1")
|
||||||
|
if err.Error() != "(1, 7): unterminated array" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load("a = [1 2")
|
||||||
|
if err.Error() != "(1, 8): missing comma" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewlinesInArrays(t *testing.T) {
|
func TestNewlinesInArrays(t *testing.T) {
|
||||||
@@ -228,6 +356,80 @@ func TestArrayWithExtraCommaComment(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSimpleInlineGroup(t *testing.T) {
|
||||||
|
tree, err := Load("key = {a = 42}")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"key": map[string]interface{}{
|
||||||
|
"a": int64(42),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoubleInlineGroup(t *testing.T) {
|
||||||
|
tree, err := Load("key = {a = 42, b = \"foo\"}")
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"key": map[string]interface{}{
|
||||||
|
"a": int64(42),
|
||||||
|
"b": "foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExampleInlineGroup(t *testing.T) {
|
||||||
|
tree, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
|
||||||
|
point = { x = 1, y = 2 }`)
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"name": map[string]interface{}{
|
||||||
|
"first": "Tom",
|
||||||
|
"last": "Preston-Werner",
|
||||||
|
},
|
||||||
|
"point": map[string]interface{}{
|
||||||
|
"x": int64(1),
|
||||||
|
"y": int64(2),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExampleInlineGroupInArray(t *testing.T) {
|
||||||
|
tree, err := Load(`points = [{ x = 1, y = 2 }]`)
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"points": []map[string]interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"x": int64(1),
|
||||||
|
"y": int64(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineTableUnterminated(t *testing.T) {
|
||||||
|
_, err := Load("foo = {")
|
||||||
|
if err.Error() != "(1, 8): unterminated inline table" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineTableCommaExpected(t *testing.T) {
|
||||||
|
_, err := Load("foo = {hello = 53 test = foo}")
|
||||||
|
if err.Error() != "(1, 19): comma expected between fields in inline table" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineTableCommaStart(t *testing.T) {
|
||||||
|
_, err := Load("foo = {, hello = 53}")
|
||||||
|
if err.Error() != "(1, 8): inline table cannot start with a comma" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInlineTableDoubleComma(t *testing.T) {
|
||||||
|
_, err := Load("foo = {hello = 53,, foo = 17}")
|
||||||
|
if err.Error() != "(1, 19): need field between two commas in inline table" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDuplicateGroups(t *testing.T) {
|
func TestDuplicateGroups(t *testing.T) {
|
||||||
_, err := Load("[foo]\na=2\n[foo]b=3")
|
_, err := Load("[foo]\na=2\n[foo]b=3")
|
||||||
if err.Error() != "(3, 2): duplicated tables" {
|
if err.Error() != "(3, 2): duplicated tables" {
|
||||||
@@ -265,7 +467,7 @@ func TestImplicitDeclarationBefore(t *testing.T) {
|
|||||||
|
|
||||||
func TestFloatsWithoutLeadingZeros(t *testing.T) {
|
func TestFloatsWithoutLeadingZeros(t *testing.T) {
|
||||||
_, err := Load("a = .42")
|
_, err := Load("a = .42")
|
||||||
if err.Error() != "(1, 4): cannot start float with a dot" {
|
if err.Error() != "(1, 5): cannot start float with a dot" {
|
||||||
t.Error("Bad error message:", err.Error())
|
t.Error("Bad error message:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +520,42 @@ func TestParseFile(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFileCRLF(t *testing.T) {
|
||||||
|
tree, err := LoadFile("example-crlf.toml")
|
||||||
|
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"title": "TOML Example",
|
||||||
|
"owner": map[string]interface{}{
|
||||||
|
"name": "Tom Preston-Werner",
|
||||||
|
"organization": "GitHub",
|
||||||
|
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||||
|
"dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
"database": map[string]interface{}{
|
||||||
|
"server": "192.168.1.1",
|
||||||
|
"ports": []int64{8001, 8001, 8002},
|
||||||
|
"connection_max": 5000,
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
"servers": map[string]interface{}{
|
||||||
|
"alpha": map[string]interface{}{
|
||||||
|
"ip": "10.0.0.1",
|
||||||
|
"dc": "eqdc10",
|
||||||
|
},
|
||||||
|
"beta": map[string]interface{}{
|
||||||
|
"ip": "10.0.0.2",
|
||||||
|
"dc": "eqdc10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"clients": map[string]interface{}{
|
||||||
|
"data": []interface{}{
|
||||||
|
[]string{"gamma", "delta"},
|
||||||
|
[]int64{1, 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseKeyGroupArray(t *testing.T) {
|
func TestParseKeyGroupArray(t *testing.T) {
|
||||||
tree, err := Load("[[foo.bar]] a = 42\n[[foo.bar]] a = 69")
|
tree, err := Load("[[foo.bar]] a = 42\n[[foo.bar]] a = 69")
|
||||||
assertTree(t, tree, err, map[string]interface{}{
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
@@ -330,6 +568,40 @@ func TestParseKeyGroupArray(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseKeyGroupArrayUnfinished(t *testing.T) {
|
||||||
|
_, err := Load("[[foo.bar]\na = 42")
|
||||||
|
if err.Error() != "(1, 10): was expecting token [[, but got unclosed key group array instead" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load("[[foo.[bar]\na = 42")
|
||||||
|
if err.Error() != "(1, 3): unexpected token group name cannot contain ']', was expecting a key group array" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseKeyGroupArrayQueryExample(t *testing.T) {
|
||||||
|
tree, err := Load(`
|
||||||
|
[[book]]
|
||||||
|
title = "The Stand"
|
||||||
|
author = "Stephen King"
|
||||||
|
[[book]]
|
||||||
|
title = "For Whom the Bell Tolls"
|
||||||
|
author = "Ernest Hemmingway"
|
||||||
|
[[book]]
|
||||||
|
title = "Neuromancer"
|
||||||
|
author = "William Gibson"
|
||||||
|
`)
|
||||||
|
|
||||||
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
|
"book": []map[string]interface{}{
|
||||||
|
{"title": "The Stand", "author": "Stephen King"},
|
||||||
|
{"title": "For Whom the Bell Tolls", "author": "Ernest Hemmingway"},
|
||||||
|
{"title": "Neuromancer", "author": "William Gibson"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseKeyGroupArraySpec(t *testing.T) {
|
func TestParseKeyGroupArraySpec(t *testing.T) {
|
||||||
tree, err := Load("[[fruit]]\n name=\"apple\"\n [fruit.physical]\n color=\"red\"\n shape=\"round\"\n [[fruit]]\n name=\"banana\"")
|
tree, err := Load("[[fruit]]\n name=\"apple\"\n [fruit.physical]\n color=\"red\"\n shape=\"round\"\n [[fruit]]\n name=\"banana\"")
|
||||||
assertTree(t, tree, err, map[string]interface{}{
|
assertTree(t, tree, err, map[string]interface{}{
|
||||||
@@ -436,3 +708,51 @@ func TestNestedTreePosition(t *testing.T) {
|
|||||||
"foo.bar.b": Position{3, 1},
|
"foo.bar.b": Position{3, 1},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvalidGroupArray(t *testing.T) {
|
||||||
|
_, err := Load("[key#group]\nanswer = 42")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Should error")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load("[foo.[bar]\na = 42")
|
||||||
|
if err.Error() != "(1, 2): unexpected token group name cannot contain ']', was expecting a key group" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoubleEqual(t *testing.T) {
|
||||||
|
_, err := Load("foo= = 2")
|
||||||
|
if err.Error() != "(1, 6): cannot have multiple equals for the same key" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupArrayReassign(t *testing.T) {
|
||||||
|
_, err := Load("[hello]\n[[hello]]")
|
||||||
|
if err.Error() != "(2, 3): key \"hello\" is already assigned and not of type group array" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidFloatParsing(t *testing.T) {
|
||||||
|
_, err := Load("a=1e_2")
|
||||||
|
if err.Error() != "(1, 3): invalid use of _ in number" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load("a=1e2_")
|
||||||
|
if err.Error() != "(1, 3): invalid use of _ in number" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load("a=1__2")
|
||||||
|
if err.Error() != "(1, 3): invalid use of _ in number" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Load("a=_1_2")
|
||||||
|
if err.Error() != "(1, 3): cannot start number with underscore" {
|
||||||
|
t.Error("Bad error message:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-8
@@ -6,13 +6,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
// Position of a document element within a TOML document.
|
||||||
Position of a document element within a TOML document.
|
//
|
||||||
|
// Line and Col are both 1-indexed positions for the element's line number and
|
||||||
Line and Col are both 1-indexed positions for the element's line number and
|
// column number, respectively. Values of zero or less will cause Invalid(),
|
||||||
column number, respectively. Values of zero or less will cause Invalid(),
|
// to return true.
|
||||||
to return true.
|
|
||||||
*/
|
|
||||||
type Position struct {
|
type Position struct {
|
||||||
Line int // line within the document
|
Line int // line within the document
|
||||||
Col int // column within the line
|
Col int // column within the line
|
||||||
@@ -24,7 +22,7 @@ func (p *Position) String() string {
|
|||||||
return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
|
return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns whether or not the position is valid (i.e. with negative or
|
// Invalid returns whether or not the position is valid (i.e. with negative or
|
||||||
// null values)
|
// null values)
|
||||||
func (p *Position) Invalid() bool {
|
func (p *Position) Invalid() bool {
|
||||||
return p.Line <= 0 || p.Col <= 0
|
return p.Line <= 0 || p.Col <= 0
|
||||||
|
|||||||
@@ -4,36 +4,47 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type of a user-defined filter function, for use with Query.SetFilter().
|
// NodeFilterFn represents a user-defined filter function, for use with
|
||||||
|
// Query.SetFilter().
|
||||||
//
|
//
|
||||||
// The return value of the function must indicate if 'node' is to be included
|
// The return value of the function must indicate if 'node' is to be included
|
||||||
// at this stage of the TOML path. Returning true will include the node, and
|
// at this stage of the TOML path. Returning true will include the node, and
|
||||||
// returning false will exclude it.
|
// returning false will exclude it.
|
||||||
//
|
//
|
||||||
// NOTE: Care should be taken to write script callbacks such that they are safe
|
// NOTE: Care should be taken to write script callbacks such that they are safe
|
||||||
// to use from multiple goroutines.
|
// to use from multiple goroutines.
|
||||||
type NodeFilterFn func(node interface{}) bool
|
type NodeFilterFn func(node interface{}) bool
|
||||||
|
|
||||||
// The result of Executing a Query
|
// QueryResult is the result of Executing a Query.
|
||||||
type QueryResult struct {
|
type QueryResult struct {
|
||||||
items []interface{}
|
items []interface{}
|
||||||
positions []Position
|
positions []Position
|
||||||
}
|
}
|
||||||
|
|
||||||
// appends a value/position pair to the result set
|
// appends a value/position pair to the result set.
|
||||||
func (r *QueryResult) appendResult(node interface{}, pos Position) {
|
func (r *QueryResult) appendResult(node interface{}, pos Position) {
|
||||||
r.items = append(r.items, node)
|
r.items = append(r.items, node)
|
||||||
r.positions = append(r.positions, pos)
|
r.positions = append(r.positions, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set of values within a QueryResult. The order of values is not guaranteed
|
// Values is a set of values within a QueryResult. The order of values is not
|
||||||
// to be in document order, and may be different each time a query is executed.
|
// guaranteed to be in document order, and may be different each time a query is
|
||||||
|
// executed.
|
||||||
func (r *QueryResult) Values() []interface{} {
|
func (r *QueryResult) Values() []interface{} {
|
||||||
return r.items
|
values := make([]interface{}, len(r.items))
|
||||||
|
for i, v := range r.items {
|
||||||
|
o, ok := v.(*tomlValue)
|
||||||
|
if ok {
|
||||||
|
values[i] = o.value
|
||||||
|
} else {
|
||||||
|
values[i] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set of positions for values within a QueryResult. Each index in Positions()
|
// Positions is a set of positions for values within a QueryResult. Each index
|
||||||
// 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 *QueryResult) Positions() []Position {
|
||||||
return r.positions
|
return r.positions
|
||||||
}
|
}
|
||||||
@@ -77,13 +88,13 @@ func (q *Query) appendPath(next pathFn) {
|
|||||||
next.setNext(newTerminatingFn()) // init the next functor
|
next.setNext(newTerminatingFn()) // init the next functor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compiles a TOML path expression. The returned Query can be used to match
|
// CompileQuery compiles a TOML path expression. The returned Query can be used
|
||||||
// elements within a TomlTree and its descendants.
|
// to match elements within a TomlTree and its descendants.
|
||||||
func CompileQuery(path string) (*Query, error) {
|
func CompileQuery(path string) (*Query, error) {
|
||||||
return parseQuery(lexQuery(path))
|
return parseQuery(lexQuery(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Executes a query against a TomlTree, and returns the result of the query.
|
// Execute executes a query against a TomlTree, and returns the result of the query.
|
||||||
func (q *Query) Execute(tree *TomlTree) *QueryResult {
|
func (q *Query) Execute(tree *TomlTree) *QueryResult {
|
||||||
result := &QueryResult{
|
result := &QueryResult{
|
||||||
items: []interface{}{},
|
items: []interface{}{},
|
||||||
@@ -101,8 +112,8 @@ func (q *Query) Execute(tree *TomlTree) *QueryResult {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets a user-defined filter function. These may be used inside "?(..)" query
|
// SetFilter sets a user-defined filter function. These may be used inside
|
||||||
// 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) {
|
||||||
if q.filters == &defaultFilterFunctions {
|
if q.filters == &defaultFilterFunctions {
|
||||||
// clone the static table
|
// clone the static table
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -105,7 +105,7 @@ func (l *queryLexer) peek() rune {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *queryLexer) accept(valid string) bool {
|
func (l *queryLexer) accept(valid string) bool {
|
||||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
if strings.ContainsRune(valid, l.next()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
l.backup()
|
l.backup()
|
||||||
|
|||||||
+3
-3
@@ -9,9 +9,10 @@ package toml
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxInt = int(^uint(0) >> 1)
|
||||||
|
|
||||||
type queryParser struct {
|
type queryParser struct {
|
||||||
flow chan token
|
flow chan token
|
||||||
tokensBuffer []token
|
tokensBuffer []token
|
||||||
@@ -137,7 +138,6 @@ func (p *queryParser) parseMatchExpr() queryParserStateFn {
|
|||||||
return nil // allow EOF at this stage
|
return nil // allow EOF at this stage
|
||||||
}
|
}
|
||||||
return p.parseError(tok, "expected match expression")
|
return p.parseError(tok, "expected match expression")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *queryParser) parseBracketExpr() queryParserStateFn {
|
func (p *queryParser) parseBracketExpr() queryParserStateFn {
|
||||||
@@ -203,7 +203,7 @@ loop: // labeled loop for easy breaking
|
|||||||
|
|
||||||
func (p *queryParser) parseSliceExpr() queryParserStateFn {
|
func (p *queryParser) parseSliceExpr() queryParserStateFn {
|
||||||
// init slice to grab all elements
|
// init slice to grab all elements
|
||||||
start, end, step := 0, math.MaxInt64, 1
|
start, end, step := 0, maxInt, 1
|
||||||
|
|
||||||
// parse optional start
|
// parse optional start
|
||||||
tok := p.getToken()
|
tok := p.getToken()
|
||||||
|
|||||||
@@ -5,15 +5,29 @@ set -e
|
|||||||
# set the path to the present working directory
|
# set the path to the present working directory
|
||||||
export GOPATH=`pwd`
|
export GOPATH=`pwd`
|
||||||
|
|
||||||
# Vendorize the BurntSushi test suite
|
function git_clone() {
|
||||||
# NOTE: this gets a specific release to avoid versioning issues
|
path=$1
|
||||||
if [ ! -d 'src/github.com/BurntSushi/toml-test' ]; then
|
branch=$2
|
||||||
mkdir -p src/github.com/BurntSushi
|
version=$3
|
||||||
git clone https://github.com/BurntSushi/toml-test.git src/github.com/BurntSushi/toml-test
|
if [ ! -d "src/$path" ]; then
|
||||||
fi
|
mkdir -p src/$path
|
||||||
pushd src/github.com/BurntSushi/toml-test
|
git clone https://$path.git src/$path
|
||||||
git reset --hard '0.2.0' # use the released version, NOT tip
|
fi
|
||||||
popd
|
pushd src/$path
|
||||||
|
git checkout "$branch"
|
||||||
|
git reset --hard "$version"
|
||||||
|
popd
|
||||||
|
}
|
||||||
|
|
||||||
|
go get github.com/pelletier/go-buffruneio
|
||||||
|
go get github.com/davecgh/go-spew/spew
|
||||||
|
|
||||||
|
# get code for BurntSushi TOML validation
|
||||||
|
# pinning all to 'HEAD' for version 0.3.x work (TODO: pin to commit hash when tests stabilize)
|
||||||
|
git_clone github.com/BurntSushi/toml master HEAD
|
||||||
|
git_clone github.com/BurntSushi/toml-test master HEAD #was: 0.2.0 HEAD
|
||||||
|
|
||||||
|
# build the BurntSushi test application
|
||||||
go build -o toml-test github.com/BurntSushi/toml-test
|
go build -o toml-test github.com/BurntSushi/toml-test
|
||||||
|
|
||||||
# vendorize the current lib for testing
|
# vendorize the current lib for testing
|
||||||
@@ -23,6 +37,42 @@ cp *.go *.toml src/github.com/pelletier/go-toml
|
|||||||
cp cmd/*.go src/github.com/pelletier/go-toml/cmd
|
cp cmd/*.go src/github.com/pelletier/go-toml/cmd
|
||||||
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 and then the BurntSushi test suite
|
# Run basic unit tests
|
||||||
go test -v github.com/pelletier/go-toml
|
go test -v github.com/pelletier/go-toml
|
||||||
./toml-test ./test_program_bin | tee test_out
|
|
||||||
|
# run the entire BurntSushi test suite
|
||||||
|
if [[ $# -eq 0 ]] ; then
|
||||||
|
echo "Running all BurntSushi tests"
|
||||||
|
./toml-test ./test_program_bin | tee test_out
|
||||||
|
else
|
||||||
|
# run a specific test
|
||||||
|
test=$1
|
||||||
|
test_path='src/github.com/BurntSushi/toml-test/tests'
|
||||||
|
valid_test="$test_path/valid/$test"
|
||||||
|
invalid_test="$test_path/invalid/$test"
|
||||||
|
|
||||||
|
if [ -e "$valid_test.toml" ]; then
|
||||||
|
echo "Valid Test TOML for $test:"
|
||||||
|
echo "===="
|
||||||
|
cat "$valid_test.toml"
|
||||||
|
|
||||||
|
echo "Valid Test JSON for $test:"
|
||||||
|
echo "===="
|
||||||
|
cat "$valid_test.json"
|
||||||
|
|
||||||
|
echo "Go-TOML Output for $test:"
|
||||||
|
echo "===="
|
||||||
|
cat "$valid_test.toml" | ./test_program_bin
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -e "$invalid_test.toml" ]; then
|
||||||
|
echo "Invalid Test TOML for $test:"
|
||||||
|
echo "===="
|
||||||
|
cat "$invalid_test.toml"
|
||||||
|
|
||||||
|
echo "Go-TOML Output for $test:"
|
||||||
|
echo "===="
|
||||||
|
echo "go-toml Output:"
|
||||||
|
cat "$invalid_test.toml" | ./test_program_bin
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const (
|
|||||||
tokenEqual
|
tokenEqual
|
||||||
tokenLeftBracket
|
tokenLeftBracket
|
||||||
tokenRightBracket
|
tokenRightBracket
|
||||||
|
tokenLeftCurlyBrace
|
||||||
|
tokenRightCurlyBrace
|
||||||
tokenLeftParen
|
tokenLeftParen
|
||||||
tokenRightParen
|
tokenRightParen
|
||||||
tokenDoubleLeftBracket
|
tokenDoubleLeftBracket
|
||||||
@@ -44,6 +46,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var tokenTypeNames = []string{
|
var tokenTypeNames = []string{
|
||||||
|
"Error",
|
||||||
"EOF",
|
"EOF",
|
||||||
"Comment",
|
"Comment",
|
||||||
"Key",
|
"Key",
|
||||||
@@ -54,7 +57,9 @@ var tokenTypeNames = []string{
|
|||||||
"Float",
|
"Float",
|
||||||
"=",
|
"=",
|
||||||
"[",
|
"[",
|
||||||
"[",
|
"]",
|
||||||
|
"{",
|
||||||
|
"}",
|
||||||
"(",
|
"(",
|
||||||
")",
|
")",
|
||||||
"]]",
|
"]]",
|
||||||
@@ -102,9 +107,6 @@ func (t token) String() string {
|
|||||||
return t.val
|
return t.val
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(t.val) > 10 {
|
|
||||||
return fmt.Sprintf("%.10q...", t.val)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%q", t.val)
|
return fmt.Sprintf("%q", t.val)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +119,14 @@ func isAlphanumeric(r rune) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isKeyChar(r rune) bool {
|
func isKeyChar(r rune) bool {
|
||||||
// "Keys start with the first non-whitespace character and end with the last
|
// Keys start with the first character that isn't whitespace or [ and end
|
||||||
// non-whitespace character before the equals sign."
|
// with the last non-whitespace character before the equals sign. Keys
|
||||||
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '=')
|
// cannot contain a # character."
|
||||||
|
return !(r == '\r' || r == '\n' || r == eof || r == '=')
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKeyStartChar(r rune) bool {
|
||||||
|
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '[')
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDigit(r rune) bool {
|
func isDigit(r rune) bool {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package toml
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTokenStringer(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
tt tokenType
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{tokenError, "Error"},
|
||||||
|
{tokenEOF, "EOF"},
|
||||||
|
{tokenComment, "Comment"},
|
||||||
|
{tokenKey, "Key"},
|
||||||
|
{tokenString, "String"},
|
||||||
|
{tokenInteger, "Integer"},
|
||||||
|
{tokenTrue, "True"},
|
||||||
|
{tokenFalse, "False"},
|
||||||
|
{tokenFloat, "Float"},
|
||||||
|
{tokenEqual, "="},
|
||||||
|
{tokenLeftBracket, "["},
|
||||||
|
{tokenRightBracket, "]"},
|
||||||
|
{tokenLeftCurlyBrace, "{"},
|
||||||
|
{tokenRightCurlyBrace, "}"},
|
||||||
|
{tokenLeftParen, "("},
|
||||||
|
{tokenRightParen, ")"},
|
||||||
|
{tokenDoubleLeftBracket, "]]"},
|
||||||
|
{tokenDoubleRightBracket, "[["},
|
||||||
|
{tokenDate, "Date"},
|
||||||
|
{tokenKeyGroup, "KeyGroup"},
|
||||||
|
{tokenKeyGroupArray, "KeyGroupArray"},
|
||||||
|
{tokenComma, ","},
|
||||||
|
{tokenColon, ":"},
|
||||||
|
{tokenDollar, "$"},
|
||||||
|
{tokenStar, "*"},
|
||||||
|
{tokenQuestion, "?"},
|
||||||
|
{tokenDot, "."},
|
||||||
|
{tokenDotDot, ".."},
|
||||||
|
{tokenEOL, "EOL"},
|
||||||
|
{tokenEOL + 1, "Unknown"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
got := test.tt.String()
|
||||||
|
if got != test.expect {
|
||||||
|
t.Errorf("[%d] invalid string of token type; got %q, expected %q", i, got, test.expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenString(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
tok token
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{token{Position{1, 1}, tokenEOF, ""}, "EOF"},
|
||||||
|
{token{Position{1, 1}, tokenError, "Δt"}, "Δt"},
|
||||||
|
{token{Position{1, 1}, tokenString, "bar"}, `"bar"`},
|
||||||
|
{token{Position{1, 1}, tokenString, "123456789012345"}, `"123456789012345"`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
got := test.tok.String()
|
||||||
|
if got != test.expect {
|
||||||
|
t.Errorf("[%d] invalid of string token; got %q, expected %q", i, got, test.expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,10 @@ package toml
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type tomlValue struct {
|
type tomlValue struct {
|
||||||
@@ -28,6 +27,13 @@ func newTomlTree() *TomlTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TreeFromMap initializes a new TomlTree object using the given map.
|
||||||
|
func TreeFromMap(m map[string]interface{}) *TomlTree {
|
||||||
|
return &TomlTree{
|
||||||
|
values: m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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 *TomlTree) Has(key string) bool {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
@@ -59,7 +65,11 @@ func (t *TomlTree) Get(key string) interface{} {
|
|||||||
if key == "" {
|
if key == "" {
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
return t.GetPath(strings.Split(key, "."))
|
comps, err := parseKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t.GetPath(comps)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPath returns the element in the tree indicated by 'keys'.
|
// GetPath returns the element in the tree indicated by 'keys'.
|
||||||
@@ -84,7 +94,7 @@ func (t *TomlTree) GetPath(keys []string) interface{} {
|
|||||||
}
|
}
|
||||||
subtree = node[len(node)-1]
|
subtree = node[len(node)-1]
|
||||||
default:
|
default:
|
||||||
return nil // cannot naigate through other node types
|
return nil // cannot navigate through other node types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// branch based on final node type
|
// branch based on final node type
|
||||||
@@ -171,7 +181,7 @@ func (t *TomlTree) SetPath(keys []string, value interface{}) {
|
|||||||
nextTree, exists := subtree.values[intermediateKey]
|
nextTree, exists := subtree.values[intermediateKey]
|
||||||
if !exists {
|
if !exists {
|
||||||
nextTree = newTomlTree()
|
nextTree = newTomlTree()
|
||||||
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 *TomlTree:
|
||||||
@@ -185,7 +195,21 @@ func (t *TomlTree) SetPath(keys []string, value interface{}) {
|
|||||||
subtree = node[len(node)-1]
|
subtree = node[len(node)-1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subtree.values[keys[len(keys)-1]] = value
|
|
||||||
|
var toInsert interface{}
|
||||||
|
|
||||||
|
switch value.(type) {
|
||||||
|
case *TomlTree:
|
||||||
|
toInsert = value
|
||||||
|
case []*TomlTree:
|
||||||
|
toInsert = value
|
||||||
|
case *tomlValue:
|
||||||
|
toInsert = value
|
||||||
|
default:
|
||||||
|
toInsert = &tomlValue{value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
subtree.values[keys[len(keys)-1]] = toInsert
|
||||||
}
|
}
|
||||||
|
|
||||||
// createSubTree takes a tree and a key and create the necessary intermediate
|
// createSubTree takes a tree and a key and create the necessary intermediate
|
||||||
@@ -215,122 +239,24 @@ func (t *TomlTree) createSubTree(keys []string, pos Position) error {
|
|||||||
case *TomlTree:
|
case *TomlTree:
|
||||||
subtree = node
|
subtree = node
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown type for path %s (%s)",
|
return fmt.Errorf("unknown type for path %s (%s): %T (%#v)",
|
||||||
strings.Join(keys, "."), intermediateKey)
|
strings.Join(keys, "."), intermediateKey, nextTree, nextTree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodes a string to a TOML-compliant string value
|
// Query compiles and executes a query on a tree and returns the query result.
|
||||||
func encodeTomlString(value string) string {
|
|
||||||
result := ""
|
|
||||||
for _, rr := range value {
|
|
||||||
intRr := uint16(rr)
|
|
||||||
switch rr {
|
|
||||||
case '\b':
|
|
||||||
result += "\\b"
|
|
||||||
case '\t':
|
|
||||||
result += "\\t"
|
|
||||||
case '\n':
|
|
||||||
result += "\\n"
|
|
||||||
case '\f':
|
|
||||||
result += "\\f"
|
|
||||||
case '\r':
|
|
||||||
result += "\\r"
|
|
||||||
case '"':
|
|
||||||
result += "\\\""
|
|
||||||
case '\\':
|
|
||||||
result += "\\\\"
|
|
||||||
default:
|
|
||||||
if intRr < 0x001F {
|
|
||||||
result += fmt.Sprintf("\\u%0.4X", intRr)
|
|
||||||
} else {
|
|
||||||
result += string(rr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value print support function for ToString()
|
|
||||||
// Outputs the TOML compliant string representation of a value
|
|
||||||
func toTomlValue(item interface{}, indent int) string {
|
|
||||||
tab := strings.Repeat(" ", indent)
|
|
||||||
switch value := item.(type) {
|
|
||||||
case int64:
|
|
||||||
return tab + strconv.FormatInt(value, 10)
|
|
||||||
case float64:
|
|
||||||
return tab + strconv.FormatFloat(value, 'f', -1, 64)
|
|
||||||
case string:
|
|
||||||
return tab + "\"" + encodeTomlString(value) + "\""
|
|
||||||
case bool:
|
|
||||||
if value {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return "false"
|
|
||||||
case time.Time:
|
|
||||||
return tab + value.Format(time.RFC3339)
|
|
||||||
case []interface{}:
|
|
||||||
result := tab + "[\n"
|
|
||||||
for _, item := range value {
|
|
||||||
result += toTomlValue(item, indent+2) + ",\n"
|
|
||||||
}
|
|
||||||
return result + tab + "]"
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unsupported value type: %v", value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursive support function for ToString()
|
|
||||||
// Outputs a tree, using the provided keyspace to prefix group names
|
|
||||||
func (t *TomlTree) toToml(indent, keyspace string) string {
|
|
||||||
result := ""
|
|
||||||
for k, v := range t.values {
|
|
||||||
// figure out the keyspace
|
|
||||||
combinedKey := k
|
|
||||||
if keyspace != "" {
|
|
||||||
combinedKey = keyspace + "." + combinedKey
|
|
||||||
}
|
|
||||||
// output based on type
|
|
||||||
switch node := v.(type) {
|
|
||||||
case []*TomlTree:
|
|
||||||
for _, item := range node {
|
|
||||||
if len(item.Keys()) > 0 {
|
|
||||||
result += fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey)
|
|
||||||
}
|
|
||||||
result += item.toToml(indent+" ", combinedKey)
|
|
||||||
}
|
|
||||||
case *TomlTree:
|
|
||||||
if len(node.Keys()) > 0 {
|
|
||||||
result += fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
|
|
||||||
}
|
|
||||||
result += node.toToml(indent+" ", combinedKey)
|
|
||||||
case *tomlValue:
|
|
||||||
result += fmt.Sprintf("%s%s = %s\n", indent, k, toTomlValue(node.value, 0))
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unsupported node type: %v", node))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TomlTree) Query(query string) (*QueryResult, error) {
|
func (t *TomlTree) Query(query string) (*QueryResult, error) {
|
||||||
if q, err := CompileQuery(query); err != nil {
|
q, err := CompileQuery(query)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
|
||||||
return q.Execute(t), nil
|
|
||||||
}
|
}
|
||||||
|
return q.Execute(t), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToString generates a human-readable representation of the current tree.
|
// LoadReader creates a TomlTree from any io.Reader.
|
||||||
// Output spans multiple lines, and is suitable for ingest by a TOML parser
|
func LoadReader(reader io.Reader) (tree *TomlTree, err error) {
|
||||||
func (t *TomlTree) ToString() string {
|
|
||||||
return t.toToml("", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load creates a TomlTree from a string.
|
|
||||||
func Load(content string) (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 {
|
||||||
@@ -339,18 +265,21 @@ func Load(content string) (tree *TomlTree, err error) {
|
|||||||
err = errors.New(r.(string))
|
err = errors.New(r.(string))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
tree = parseToml(lexToml(content))
|
tree = parseToml(lexToml(reader))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load creates a TomlTree from a string.
|
||||||
|
func Load(content string) (tree *TomlTree, err error) {
|
||||||
|
return LoadReader(strings.NewReader(content))
|
||||||
|
}
|
||||||
|
|
||||||
// LoadFile creates a TomlTree from a file.
|
// LoadFile creates a TomlTree from a file.
|
||||||
func LoadFile(path string) (tree *TomlTree, err error) {
|
func LoadFile(path string) (tree *TomlTree, err error) {
|
||||||
buff, ferr := ioutil.ReadFile(path)
|
file, err := os.Open(path)
|
||||||
if ferr != nil {
|
if err != nil {
|
||||||
err = ferr
|
return nil, err
|
||||||
} else {
|
|
||||||
s := string(buff)
|
|
||||||
tree, err = Load(s)
|
|
||||||
}
|
}
|
||||||
return
|
defer file.Close()
|
||||||
|
return LoadReader(file)
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-1
@@ -15,6 +15,47 @@ func TestTomlHas(t *testing.T) {
|
|||||||
if !tree.Has("test.key") {
|
if !tree.Has("test.key") {
|
||||||
t.Errorf("Has - expected test.key to exists")
|
t.Errorf("Has - expected test.key to exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tree.Has("") {
|
||||||
|
t.Errorf("Should return false if the key is not provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTomlGet(t *testing.T) {
|
||||||
|
tree, _ := Load(`
|
||||||
|
[test]
|
||||||
|
key = "value"
|
||||||
|
`)
|
||||||
|
|
||||||
|
if tree.Get("") != tree {
|
||||||
|
t.Errorf("Get should return the tree itself when given an empty path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tree.Get("test.key") != "value" {
|
||||||
|
t.Errorf("Get should return the value")
|
||||||
|
}
|
||||||
|
if tree.Get(`\`) != nil {
|
||||||
|
t.Errorf("should return nil when the key is malformed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTomlGetDefault(t *testing.T) {
|
||||||
|
tree, _ := Load(`
|
||||||
|
[test]
|
||||||
|
key = "value"
|
||||||
|
`)
|
||||||
|
|
||||||
|
if tree.GetDefault("", "hello") != tree {
|
||||||
|
t.Error("GetDefault should return the tree itself when given an empty path")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tree.GetDefault("test.key", "hello") != "value" {
|
||||||
|
t.Error("Get should return the value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tree.GetDefault("whatever", "hello") != "hello" {
|
||||||
|
t.Error("GetDefault should return the default value if the key does not exist")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTomlHasPath(t *testing.T) {
|
func TestTomlHasPath(t *testing.T) {
|
||||||
@@ -46,6 +87,11 @@ func TestTomlGetPath(t *testing.T) {
|
|||||||
t.Errorf("GetPath[%d] %v - expected %v, got %v instead.", idx, item.Path, item.Expected, result)
|
t.Errorf("GetPath[%d] %v - expected %v, got %v instead.", idx, item.Path, item.Expected, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tree, _ := Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6")
|
||||||
|
if tree.GetPath([]string{"whatever"}) != nil {
|
||||||
|
t.Error("GetPath should return nil when the key does not exist")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTomlQuery(t *testing.T) {
|
func TestTomlQuery(t *testing.T) {
|
||||||
@@ -65,10 +111,18 @@ func TestTomlQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tt, ok := values[0].(*TomlTree); !ok {
|
if tt, ok := values[0].(*TomlTree); !ok {
|
||||||
t.Errorf("Expected type of TomlTree: %T Tv", values[0], values[0])
|
t.Errorf("Expected type of TomlTree: %T", values[0])
|
||||||
} else if tt.Get("a") != int64(1) {
|
} else if tt.Get("a") != int64(1) {
|
||||||
t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a"))
|
t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a"))
|
||||||
} else if tt.Get("b") != int64(2) {
|
} else if tt.Get("b") != int64(2) {
|
||||||
t.Errorf("Expected 'b' with a value 2: %v", tt.Get("b"))
|
t.Errorf("Expected 'b' with a value 2: %v", tt.Get("b"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTomlFromMap(t *testing.T) {
|
||||||
|
simpleMap := map[string]interface{}{"hello": 42}
|
||||||
|
tree := TreeFromMap(simpleMap)
|
||||||
|
if tree.Get("hello") != 42 {
|
||||||
|
t.Fatal("hello should be 42, not", tree.Get("hello"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Tools to convert a TomlTree to different representations
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encodes a string to a TOML-compliant string value
|
||||||
|
func encodeTomlString(value string) string {
|
||||||
|
result := ""
|
||||||
|
for _, rr := range value {
|
||||||
|
intRr := uint16(rr)
|
||||||
|
switch rr {
|
||||||
|
case '\b':
|
||||||
|
result += "\\b"
|
||||||
|
case '\t':
|
||||||
|
result += "\\t"
|
||||||
|
case '\n':
|
||||||
|
result += "\\n"
|
||||||
|
case '\f':
|
||||||
|
result += "\\f"
|
||||||
|
case '\r':
|
||||||
|
result += "\\r"
|
||||||
|
case '"':
|
||||||
|
result += "\\\""
|
||||||
|
case '\\':
|
||||||
|
result += "\\\\"
|
||||||
|
default:
|
||||||
|
if intRr < 0x001F {
|
||||||
|
result += fmt.Sprintf("\\u%0.4X", intRr)
|
||||||
|
} else {
|
||||||
|
result += string(rr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value print support function for ToString()
|
||||||
|
// Outputs the TOML compliant string representation of a value
|
||||||
|
func toTomlValue(item interface{}, indent int) string {
|
||||||
|
tab := strings.Repeat(" ", indent)
|
||||||
|
switch value := item.(type) {
|
||||||
|
case int64:
|
||||||
|
return tab + strconv.FormatInt(value, 10)
|
||||||
|
case float64:
|
||||||
|
return tab + strconv.FormatFloat(value, 'f', -1, 64)
|
||||||
|
case string:
|
||||||
|
return tab + "\"" + encodeTomlString(value) + "\""
|
||||||
|
case bool:
|
||||||
|
if value {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
case time.Time:
|
||||||
|
return tab + value.Format(time.RFC3339)
|
||||||
|
case []interface{}:
|
||||||
|
result := tab + "[\n"
|
||||||
|
for _, item := range value {
|
||||||
|
result += toTomlValue(item, indent+2) + ",\n"
|
||||||
|
}
|
||||||
|
return result + tab + "]"
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported value type: %v", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive support function for ToString()
|
||||||
|
// Outputs a tree, using the provided keyspace to prefix group names
|
||||||
|
func (t *TomlTree) toToml(indent, keyspace string) string {
|
||||||
|
result := ""
|
||||||
|
for k, v := range t.values {
|
||||||
|
// figure out the keyspace
|
||||||
|
combinedKey := k
|
||||||
|
if keyspace != "" {
|
||||||
|
combinedKey = keyspace + "." + combinedKey
|
||||||
|
}
|
||||||
|
// output based on type
|
||||||
|
switch node := v.(type) {
|
||||||
|
case []*TomlTree:
|
||||||
|
for _, item := range node {
|
||||||
|
if len(item.Keys()) > 0 {
|
||||||
|
result += fmt.Sprintf("\n%s[[%s]]\n", indent, combinedKey)
|
||||||
|
}
|
||||||
|
result += item.toToml(indent+" ", combinedKey)
|
||||||
|
}
|
||||||
|
case *TomlTree:
|
||||||
|
if len(node.Keys()) > 0 {
|
||||||
|
result += fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
|
||||||
|
}
|
||||||
|
result += node.toToml(indent+" ", combinedKey)
|
||||||
|
case map[string]interface{}:
|
||||||
|
sub := TreeFromMap(node)
|
||||||
|
|
||||||
|
if len(sub.Keys()) > 0 {
|
||||||
|
result += fmt.Sprintf("\n%s[%s]\n", indent, combinedKey)
|
||||||
|
}
|
||||||
|
result += sub.toToml(indent+" ", combinedKey)
|
||||||
|
case *tomlValue:
|
||||||
|
result += fmt.Sprintf("%s%s = %s\n", indent, k, toTomlValue(node.value, 0))
|
||||||
|
default:
|
||||||
|
result += fmt.Sprintf("%s%s = %s\n", indent, k, toTomlValue(v, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToString is an alias for String
|
||||||
|
func (t *TomlTree) ToString() string {
|
||||||
|
return t.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToString generates a human-readable representation of the current tree.
|
||||||
|
// Output spans multiple lines, and is suitable for ingest by a TOML parser
|
||||||
|
func (t *TomlTree) String() string {
|
||||||
|
return t.toToml("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMap recursively generates a representation of the current tree using map[string]interface{}.
|
||||||
|
func (t *TomlTree) ToMap() map[string]interface{} {
|
||||||
|
result := map[string]interface{}{}
|
||||||
|
|
||||||
|
for k, v := range t.values {
|
||||||
|
switch node := v.(type) {
|
||||||
|
case []*TomlTree:
|
||||||
|
result[k] = make([]interface{}, 0)
|
||||||
|
for _, item := range node {
|
||||||
|
result[k] = item.ToMap()
|
||||||
|
}
|
||||||
|
case *TomlTree:
|
||||||
|
result[k] = node.ToMap()
|
||||||
|
case map[string]interface{}:
|
||||||
|
sub := TreeFromMap(node)
|
||||||
|
result[k] = sub.ToMap()
|
||||||
|
case *tomlValue:
|
||||||
|
result[k] = node.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTomlTreeConversionToString(t *testing.T) {
|
||||||
|
toml, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
|
||||||
|
points = { x = 1, y = 2 }`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reparsedTree, err := Load(toml.ToString())
|
||||||
|
|
||||||
|
assertTree(t, reparsedTree, err, map[string]interface{}{
|
||||||
|
"name": map[string]interface{}{
|
||||||
|
"first": "Tom",
|
||||||
|
"last": "Preston-Werner",
|
||||||
|
},
|
||||||
|
"points": map[string]interface{}{
|
||||||
|
"x": int64(1),
|
||||||
|
"y": int64(2),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMaps(t *testing.T, actual, expected map[string]interface{}) {
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Fatal("trees aren't equal.\n", "Expected:\n", expected, "\nActual:\n", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTomlTreeConversionToMapSimple(t *testing.T) {
|
||||||
|
tree, _ := Load("a = 42\nb = 17")
|
||||||
|
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"a": int64(42),
|
||||||
|
"b": int64(17),
|
||||||
|
}
|
||||||
|
|
||||||
|
testMaps(t, tree.ToMap(), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTomlTreeConversionToMapExampleFile(t *testing.T) {
|
||||||
|
tree, _ := LoadFile("example.toml")
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"title": "TOML Example",
|
||||||
|
"owner": map[string]interface{}{
|
||||||
|
"name": "Tom Preston-Werner",
|
||||||
|
"organization": "GitHub",
|
||||||
|
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
|
||||||
|
"dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
"database": map[string]interface{}{
|
||||||
|
"server": "192.168.1.1",
|
||||||
|
"ports": []interface{}{int64(8001), int64(8001), int64(8002)},
|
||||||
|
"connection_max": int64(5000),
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
"servers": map[string]interface{}{
|
||||||
|
"alpha": map[string]interface{}{
|
||||||
|
"ip": "10.0.0.1",
|
||||||
|
"dc": "eqdc10",
|
||||||
|
},
|
||||||
|
"beta": map[string]interface{}{
|
||||||
|
"ip": "10.0.0.2",
|
||||||
|
"dc": "eqdc10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"clients": map[string]interface{}{
|
||||||
|
"data": []interface{}{
|
||||||
|
[]interface{}{"gamma", "delta"},
|
||||||
|
[]interface{}{int64(1), int64(2)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testMaps(t, tree.ToMap(), expected)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user