Compare commits

..

40 Commits

Author SHA1 Message Date
Thomas Pelletier 86a7f2508e Merge pull request #17 from eanderton/master
TOML v0.2.0 Group Array Support and ToString Feature
2014-07-09 07:34:48 +02:00
eanderton 713478a34e Dropped travis support for go v1.0 2014-07-08 22:12:58 -04:00
eanderton 5dd3b53635 Fixed formatting 2014-07-08 22:02:42 -04:00
eanderton 262211488d Fixed path handling and key group array name lexing for test compliance 2014-07-08 22:00:46 -04:00
eanderton abdecb7be7 Refactored testing approach to use 'vendorized' libraries at test time. 2014-07-08 21:26:30 -04:00
eanderton f69e0b0837 merged from pelletier/go-toml 2014-07-08 18:38:07 -04:00
Thomas Pelletier cd055fa448 Remove outdated test dependency 2014-07-08 07:38:28 +02:00
eanderton c9ea292f59 Additional formatting 2014-07-07 21:08:57 -04:00
eanderton 7b208738bc Fixed formatting; added name to license file 2014-07-07 21:06:42 -04:00
eanderton c8b5633273 Group array support; ToString() support 2014-07-07 20:48:29 -04:00
Thomas Pelletier b28c2d0c9e Merge pull request #16 from half-ogre/15-fix-parser-on-windows
Fix parser on Windows
2014-02-21 14:27:47 +01:00
half-ogre 1c0f7f552c \r is not a keychar on Windows 2014-02-20 14:51:43 -08:00
Thomas Pelletier 0773148832 Merge branch 'master' of github.com:pelletier/go-toml 2013-12-10 22:00:49 +01:00
Thomas Pelletier fbf99a1816 Add Go 1.2 to the build matrix 2013-12-10 21:55:30 +01:00
Thomas Pelletier 1cfdab9cee Merge pull request #14 from pelletier/pelletier/go_test_support
Add BurntSushi's test suite
2013-12-10 12:52:51 -08:00
Thomas Pelletier dc20c454d7 Handle dots in keys 2013-12-10 21:51:40 +01:00
Thomas Pelletier 0c4e891f3e Handle non-alpha chars in keys 2013-12-10 19:46:56 +01:00
Thomas Pelletier b1d602f733 Add missing string escape sequences 2013-12-10 19:35:47 +01:00
Thomas Pelletier a4d623ad05 Fix implicit declaration 2013-12-10 19:29:01 +01:00
Thomas Pelletier 4fdde9794a Output JSON for test suite
Reused @BurntSushi's
2013-12-10 19:00:35 +01:00
Thomas Pelletier 3085454477 Don't allow duplicate keys 2013-12-10 17:50:59 +01:00
Thomas Pelletier 01609e0ab7 Add some tests for nested empty arrays 2013-12-10 17:46:30 +01:00
Thomas Pelletier a34fc5f051 Don't allow invalid escape sequences 2013-12-10 17:34:11 +01:00
Thomas Pelletier e8d5dbf787 Don't allow two equals for the same key 2013-12-10 17:28:16 +01:00
Thomas Pelletier 5ffe2e5565 Don't allow float to end with a dot 2013-12-10 17:24:53 +01:00
Thomas Pelletier 278c4d97ec Don't allow floats starting with a dot 2013-12-10 17:17:15 +01:00
Thomas Pelletier c743c90315 Don't allow empty intermediate tables 2013-12-10 16:23:53 +01:00
Thomas Pelletier 8081f3cc09 Don't allow tables to be redefined 2013-12-10 16:10:26 +01:00
Thomas Pelletier 72f17747a0 Prevent mixed types in arrays 2013-12-10 15:45:50 +01:00
Thomas Pelletier 979a055512 Retrieve the exit code from the test suites 2013-12-10 15:28:52 +01:00
Thomas Pelletier def5433558 Add a note on the README 2013-12-10 15:06:32 +01:00
Thomas Pelletier c163b3f68b Add wrapper to find the test binary 2013-12-10 15:02:55 +01:00
Thomas Pelletier bbe45c63f2 Add test script to run both unit and example tests 2013-12-10 14:50:52 +01:00
Thomas Pelletier 40a44dc51f Add BurntSushi's test suite 2013-12-10 14:43:27 +01:00
Thomas Pelletier 2ba6587bf3 Merge pull request #11 from pelletier/fix_comments_multilines_array
Comments in a multiline array cause parse error
2013-12-09 10:08:18 -08:00
Thomas Pelletier 72d57d8477 Fix multiline array comments lexing 2013-12-09 19:05:29 +01:00
Thomas Pelletier 0f6008f46e Add some tests for the lexer 2013-12-09 19:05:18 +01:00
Thomas Pelletier be268e4049 Include @cmars tests 2013-12-09 17:25:31 +01:00
Thomas Pelletier 53005a205f Handle keys with dash. ref #10 2013-12-09 17:12:07 +01:00
Thomas Pelletier 34d60ec92f Make Travis use multiple Go versions 2013-07-01 22:12:22 +02:00
12 changed files with 718 additions and 37 deletions
+1
View File
@@ -0,0 +1 @@
test_program/test_program_bin
+5
View File
@@ -1 +1,6 @@
language: go language: go
script: "./test.sh"
go:
- 1.1
- 1.2
- tip
+9 -1
View File
@@ -56,10 +56,18 @@ Feel free to report bugs and patches using GitHub's pull requests system on
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be [pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
much appreciated! much appreciated!
### Run tests
You have to make sure two kind of tests run:
1. The Go unit tests: `go test`
2. The TOML examples base: `./test_program/go-test.sh`
You can run both of them using `./test.sh`.
## License ## License
Copyright (c) 2013 Thomas Pelletier Copyright (c) 2013, 2014 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
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
# fail out of the script if anything here fails
set -e
# clear out stuff generated by test.sh
rm -rf src test_program_bin toml-test
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"encoding/json"
"fmt"
"github.com/pelletier/go-toml"
"io/ioutil"
"log"
"os"
"time"
)
func main() {
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
os.Exit(2)
}
tree, err := toml.Load(string(bytes))
if err != nil {
os.Exit(1)
}
typedTree := translate((map[string]interface{})(*tree))
if err := json.NewEncoder(os.Stdout).Encode(typedTree); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
os.Exit(0)
}
func translate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = translate(v)
}
return typed
case *toml.TomlTree:
return translate((map[string]interface{})(*orig))
case []*toml.TomlTree:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v)
}
return tag("array", typed)
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
return tag("float", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
+81 -7
View File
@@ -34,8 +34,11 @@ const (
tokenFloat tokenFloat
tokenLeftBracket tokenLeftBracket
tokenRightBracket tokenRightBracket
tokenDoubleLeftBracket
tokenDoubleRightBracket
tokenDate tokenDate
tokenKeyGroup tokenKeyGroup
tokenKeyGroupArray
tokenComma tokenComma
tokenEOL tokenEOL
) )
@@ -67,6 +70,12 @@ func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_' return unicode.IsLetter(r) || r == '_'
} }
func isKeyChar(r rune) bool {
// "Keys start with the first non-whitespace character and end with the last
// non-whitespace character before the equals sign."
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '=')
}
func isDigit(r rune) bool { func isDigit(r rune) bool {
return unicode.IsNumber(r) return unicode.IsNumber(r)
} }
@@ -163,14 +172,18 @@ func lexVoid(l *lexer) stateFn {
return lexEqual return lexEqual
} }
if isAlphanumeric(next) {
return lexKey
}
if isSpace(next) { if isSpace(next) {
l.ignore() l.ignore()
} }
if l.depth > 0 {
return lexRvalue
}
if isKeyChar(next) {
return lexKey
}
if l.next() == eof { if l.next() == eof {
break break
} }
@@ -184,6 +197,10 @@ func lexRvalue(l *lexer) stateFn {
for { for {
next := l.peek() next := l.peek()
switch next { switch next {
case '.':
return l.errorf("cannot start float with a dot")
case '=':
return l.errorf("cannot have multiple equals for the same key")
case '[': case '[':
l.depth += 1 l.depth += 1
return lexLeftBracket return lexLeftBracket
@@ -276,7 +293,7 @@ func lexComma(l *lexer) stateFn {
func lexKey(l *lexer) stateFn { func lexKey(l *lexer) stateFn {
l.ignore() l.ignore()
for isAlphanumeric(l.next()) { for isKeyChar(l.next()) {
} }
l.backup() l.backup()
l.emit(tokenKey) l.emit(tokenKey)
@@ -320,6 +337,15 @@ func lexString(l *lexer) stateFn {
} else if l.follow("\\n") { } else if l.follow("\\n") {
l.pos += 1 l.pos += 1
growing_string += "\n" growing_string += "\n"
} else if l.follow("\\b") {
l.pos += 1
growing_string += "\b"
} else if l.follow("\\f") {
l.pos += 1
growing_string += "\f"
} else if l.follow("\\/") {
l.pos += 1
growing_string += "/"
} else if l.follow("\\t") { } else if l.follow("\\t") {
l.pos += 1 l.pos += 1
growing_string += "\t" growing_string += "\t"
@@ -346,6 +372,9 @@ func lexString(l *lexer) stateFn {
return l.errorf("invalid unicode escape: \\u" + code) return l.errorf("invalid unicode escape: \\u" + code)
} }
growing_string += string(rune(intcode)) growing_string += string(rune(intcode))
} else if l.follow("\\") {
l.pos += 1
return l.errorf("invalid escape sequence: \\" + string(l.peek()))
} else { } else {
growing_string += string(l.peek()) growing_string += string(l.peek())
} }
@@ -361,8 +390,42 @@ func lexString(l *lexer) stateFn {
func lexKeyGroup(l *lexer) stateFn { func lexKeyGroup(l *lexer) stateFn {
l.ignore() l.ignore()
l.pos += 1 l.pos += 1
l.emit(tokenLeftBracket)
return lexInsideKeyGroup if l.peek() == '[' {
// token '[[' signifies an array of anonymous key groups
l.pos += 1
l.emit(tokenDoubleLeftBracket)
return lexInsideKeyGroupArray
} else {
// vanilla key group
l.emit(tokenLeftBracket)
return lexInsideKeyGroup
}
}
func lexInsideKeyGroupArray(l *lexer) stateFn {
for {
if l.peek() == ']' {
if l.pos > l.start {
l.emit(tokenKeyGroupArray)
}
l.ignore()
l.pos += 1
if l.peek() != ']' {
break // error
}
l.pos += 1
l.emit(tokenDoubleRightBracket)
return lexVoid
} else if l.peek() == '[' {
return l.errorf("group name cannot contain ']'")
}
if l.next() == eof {
break
}
}
return l.errorf("unclosed key group array")
} }
func lexInsideKeyGroup(l *lexer) stateFn { func lexInsideKeyGroup(l *lexer) stateFn {
@@ -375,6 +438,8 @@ func lexInsideKeyGroup(l *lexer) stateFn {
l.pos += 1 l.pos += 1
l.emit(tokenRightBracket) l.emit(tokenRightBracket)
return lexVoid return lexVoid
} else if l.peek() == '[' {
return l.errorf("group name cannot contain ']'")
} }
if l.next() == eof { if l.next() == eof {
@@ -401,6 +466,12 @@ func lexNumber(l *lexer) stateFn {
for { for {
next := l.next() next := l.next()
if next == '.' { if next == '.' {
if point_seen {
return l.errorf("cannot have two dots in one float")
}
if !isDigit(l.peek()) {
return l.errorf("float cannot end with a dot")
}
point_seen = true point_seen = true
} else if isDigit(next) { } else if isDigit(next) {
digit_seen = true digit_seen = true
@@ -408,6 +479,9 @@ func lexNumber(l *lexer) stateFn {
l.backup() l.backup()
break break
} }
if point_seen && !digit_seen {
return l.errorf("cannot start float with a dot")
}
} }
if !digit_seen { if !digit_seen {
+95
View File
@@ -84,6 +84,13 @@ func TestBasicKeyWithUnderscore(t *testing.T) {
}) })
} }
func TestBasicKeyWithDash(t *testing.T) {
testFlow(t, "hello-world", []token{
token{tokenKey, "hello-world"},
token{tokenEOF, ""},
})
}
func TestBasicKeyWithUppercaseMix(t *testing.T) { func TestBasicKeyWithUppercaseMix(t *testing.T) {
testFlow(t, "helloHELLOHello", []token{ testFlow(t, "helloHELLOHello", []token{
token{tokenKey, "helloHELLOHello"}, token{tokenKey, "helloHELLOHello"},
@@ -106,6 +113,23 @@ func TestBasicKeyAndEqual(t *testing.T) {
}) })
} }
func TestKeyWithSharpAndEqual(t *testing.T) {
testFlow(t, "key#name = 5", []token{
token{tokenKey, "key#name"},
token{tokenEqual, "="},
token{tokenInteger, "5"},
token{tokenEOF, ""},
})
}
func TestKeyWithSymbolsAndEqual(t *testing.T) {
testFlow(t, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:' = 5", []token{
token{tokenKey, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:'"},
token{tokenEqual, "="},
token{tokenInteger, "5"},
token{tokenEOF, ""},
})
}
func TestKeyEqualStringEscape(t *testing.T) { func TestKeyEqualStringEscape(t *testing.T) {
testFlow(t, "foo = \"hello\\\"\"", []token{ testFlow(t, "foo = \"hello\\\"\"", []token{
token{tokenKey, "foo"}, token{tokenKey, "foo"},
@@ -200,6 +224,22 @@ func TestArrayInts(t *testing.T) {
}) })
} }
func TestMultilineArrayComments(t *testing.T) {
testFlow(t, "a = [1, # wow\n2, # such items\n3, # so array\n]", []token{
token{tokenKey, "a"},
token{tokenEqual, "="},
token{tokenLeftBracket, "["},
token{tokenInteger, "1"},
token{tokenComma, ","},
token{tokenInteger, "2"},
token{tokenComma, ","},
token{tokenInteger, "3"},
token{tokenComma, ","},
token{tokenRightBracket, "]"},
token{tokenEOF, ""},
})
}
func TestKeyEqualArrayBools(t *testing.T) { func TestKeyEqualArrayBools(t *testing.T) {
testFlow(t, "foo = [true, false, true]", []token{ testFlow(t, "foo = [true, false, true]", []token{
token{tokenKey, "foo"}, token{tokenKey, "foo"},
@@ -245,6 +285,52 @@ func TestKeyEqualDate(t *testing.T) {
}) })
} }
func TestFloatEndingWithDot(t *testing.T) {
testFlow(t, "foo = 42.", []token{
token{tokenKey, "foo"},
token{tokenEqual, "="},
token{tokenError, "float cannot end with a dot"},
})
}
func TestFloatWithTwoDots(t *testing.T) {
testFlow(t, "foo = 4.2.", []token{
token{tokenKey, "foo"},
token{tokenEqual, "="},
token{tokenError, "cannot have two dots in one float"},
})
}
func TestDoubleEqualKey(t *testing.T) {
testFlow(t, "foo= = 2", []token{
token{tokenKey, "foo"},
token{tokenEqual, "="},
token{tokenError, "cannot have multiple equals for the same key"},
})
}
func TestInvalidEsquapeSequence(t *testing.T) {
testFlow(t, "foo = \"\\x\"", []token{
token{tokenKey, "foo"},
token{tokenEqual, "="},
token{tokenError, "invalid escape sequence: \\x"},
})
}
func TestNestedArrays(t *testing.T) {
testFlow(t, "foo = [[[]]]", []token{
token{tokenKey, "foo"},
token{tokenEqual, "="},
token{tokenLeftBracket, "["},
token{tokenLeftBracket, "["},
token{tokenLeftBracket, "["},
token{tokenRightBracket, "]"},
token{tokenRightBracket, "]"},
token{tokenRightBracket, "]"},
token{tokenEOF, ""},
})
}
func TestKeyEqualNumber(t *testing.T) { func TestKeyEqualNumber(t *testing.T) {
testFlow(t, "foo = 42", []token{ testFlow(t, "foo = 42", []token{
token{tokenKey, "foo"}, token{tokenKey, "foo"},
@@ -318,3 +404,12 @@ func TestUnicodeString(t *testing.T) {
token{tokenEOF, ""}, token{tokenEOF, ""},
}) })
} }
func TestKeyGroupArray(t *testing.T) {
testFlow(t, "[[foo]]", []token{
token{tokenDoubleLeftBracket, "[["},
token{tokenKeyGroupArray, "foo"},
token{tokenDoubleRightBracket, "]]"},
token{tokenEOF, ""},
})
}
+86 -15
View File
@@ -4,15 +4,18 @@ package toml
import ( import (
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"strings"
"time" "time"
) )
type parser struct { type parser struct {
flow chan token flow chan token
tree *TomlTree tree *TomlTree
tokensBuffer []token tokensBuffer []token
currentGroup string currentGroup []string
seenGroupKeys []string
} }
type parserStateFn func(*parser) parserStateFn type parserStateFn func(*parser) parserStateFn
@@ -68,6 +71,8 @@ func parseStart(p *parser) parserStateFn {
} }
switch tok.typ { switch tok.typ {
case tokenDoubleLeftBracket:
return parseGroupArray
case tokenLeftBracket: case tokenLeftBracket:
return parseGroup return parseGroup
case tokenKey: case tokenKey:
@@ -80,15 +85,53 @@ func parseStart(p *parser) parserStateFn {
return nil return nil
} }
func parseGroupArray(p *parser) parserStateFn {
p.getToken() // discard the [[
key := p.getToken()
if key.typ != tokenKeyGroupArray {
panic(fmt.Sprintf("unexpected token %s, was expecting a key group array", key))
}
// get or create group array element at the indicated part in the path
p.currentGroup = strings.Split(key.val, ".")
dest_tree := p.tree.GetPath(p.currentGroup)
var array []*TomlTree
if dest_tree == nil {
array = make([]*TomlTree, 0)
} else if dest_tree.([]*TomlTree) != nil {
array = dest_tree.([]*TomlTree)
} else {
panic(fmt.Sprintf("key %s is already assigned and not of type group array", key))
}
// add a new tree to the end of the group array
new_tree := make(TomlTree)
array = append(array, &new_tree)
p.tree.SetPath(p.currentGroup, array)
// keep this key name from use by other kinds of assignments
p.seenGroupKeys = append(p.seenGroupKeys, key.val)
// move to next parser state
p.assume(tokenDoubleRightBracket)
return parseStart(p)
}
func parseGroup(p *parser) parserStateFn { func parseGroup(p *parser) parserStateFn {
p.getToken() // discard the [ p.getToken() // discard the [
key := p.getToken() key := p.getToken()
if key.typ != tokenKeyGroup { if key.typ != tokenKeyGroup {
panic(fmt.Sprintf("unexpected token %s, was expecting a key group", key)) panic(fmt.Sprintf("unexpected token %s, was expecting a key group", key))
} }
for _, item := range p.seenGroupKeys {
if item == key.val {
panic("duplicated tables")
}
}
p.seenGroupKeys = append(p.seenGroupKeys, key.val)
p.tree.createSubTree(key.val) p.tree.createSubTree(key.val)
p.assume(tokenRightBracket) p.assume(tokenRightBracket)
p.currentGroup = key.val p.currentGroup = strings.Split(key.val, ".")
return parseStart(p) return parseStart(p)
} }
@@ -96,11 +139,31 @@ func parseAssign(p *parser) parserStateFn {
key := p.getToken() key := p.getToken()
p.assume(tokenEqual) p.assume(tokenEqual)
value := parseRvalue(p) value := parseRvalue(p)
final_key := key.val var group_key []string
if p.currentGroup != "" { if len(p.currentGroup) > 0 {
final_key = p.currentGroup + "." + key.val group_key = p.currentGroup
} else {
group_key = make([]string, 0)
} }
p.tree.Set(final_key, value)
// find the group to assign, looking out for arrays of groups
var target_node *TomlTree
switch node := p.tree.GetPath(group_key).(type) {
case []*TomlTree:
target_node = node[len(node)-1]
case *TomlTree:
target_node = node
default:
panic(fmt.Sprintf("Unknown group type for path %v", group_key))
}
// assign value to the found group
local_key := []string{key.val}
final_key := append(group_key, key.val)
if target_node.GetPath(local_key) != nil {
panic(fmt.Sprintf("the following key was defined twice: %s", strings.Join(final_key, ".")))
}
target_node.SetPath(local_key, value)
return parseStart(p) return parseStart(p)
} }
@@ -137,6 +200,8 @@ func parseRvalue(p *parser) interface{} {
return val return val
case tokenLeftBracket: case tokenLeftBracket:
return parseArray(p) return parseArray(p)
case tokenError:
panic(tok.val)
} }
panic("never reached") panic("never reached")
@@ -146,6 +211,7 @@ func parseRvalue(p *parser) interface{} {
func parseArray(p *parser) []interface{} { func parseArray(p *parser) []interface{} {
array := make([]interface{}, 0) array := make([]interface{}, 0)
arrayType := reflect.TypeOf(nil)
for { for {
follow := p.peek() follow := p.peek()
if follow == nil || follow.typ == tokenEOF { if follow == nil || follow.typ == tokenEOF {
@@ -156,14 +222,18 @@ func parseArray(p *parser) []interface{} {
return array return array
} }
val := parseRvalue(p) val := parseRvalue(p)
if arrayType == nil {
arrayType = reflect.TypeOf(val)
}
if reflect.TypeOf(val) != arrayType {
panic("mixed types in array")
}
array = append(array, val) array = append(array, val)
follow = p.peek() follow = p.peek()
if follow == nil { if follow == nil {
panic("unterminated array") panic("unterminated array")
} }
if follow.typ != tokenRightBracket && follow.typ != tokenComma { if follow.typ != tokenRightBracket && follow.typ != tokenComma {
fmt.Println(follow.typ)
fmt.Println(follow.val)
panic("missing comma") panic("missing comma")
} }
if follow.typ == tokenComma { if follow.typ == tokenComma {
@@ -176,10 +246,11 @@ func parseArray(p *parser) []interface{} {
func parse(flow chan token) *TomlTree { func parse(flow chan token) *TomlTree {
result := make(TomlTree) result := make(TomlTree)
parser := &parser{ parser := &parser{
flow: flow, flow: flow,
tree: &result, tree: &result,
tokensBuffer: make([]token, 0), tokensBuffer: make([]token, 0),
currentGroup: "", currentGroup: make([]string, 0),
seenGroupKeys: make([]string, 0),
} }
parser.run() parser.run()
return parser.tree return parser.tree
+143 -3
View File
@@ -12,9 +12,18 @@ func assertTree(t *testing.T, tree *TomlTree, err error, ref map[string]interfac
return return
} }
for k, v := range ref { for k, v := range ref {
if fmt.Sprintf("%v", tree.Get(k)) != fmt.Sprintf("%v", v) { node := tree.Get(k)
t.Log("was expecting", v, "at", k, "but got", tree.Get(k)) switch cast_node := node.(type) {
t.Error() case []*TomlTree:
for idx, item := range cast_node {
assertTree(t, item, err, v.([]map[string]interface{})[idx])
}
case *TomlTree:
assertTree(t, cast_node, err, v.(map[string]interface{}))
default:
if fmt.Sprintf("%v", node) != fmt.Sprintf("%v", v) {
t.Errorf("was expecting %v at %v but got %v", v, k, node)
}
} }
} }
} }
@@ -142,6 +151,25 @@ func TestArrayNested(t *testing.T) {
}) })
} }
func TestNestedEmptyArrays(t *testing.T) {
tree, err := Load("a = [[[]]]")
assertTree(t, tree, err, map[string]interface{}{
"a": [][][]interface{}{[][]interface{}{[]interface{}{}}},
})
}
func TestArrayMixedTypes(t *testing.T) {
_, err := Load("a = [42, 16.0]")
if err.Error() != "mixed types in array" {
t.Error("Bad error message:", err.Error())
}
_, err = Load("a = [42, \"hello\"]")
if err.Error() != "mixed types in array" {
t.Error("Bad error message:", err.Error())
}
}
func TestArrayNestedStrings(t *testing.T) { func TestArrayNestedStrings(t *testing.T) {
tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]") tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -170,6 +198,67 @@ func TestNewlinesInArrays(t *testing.T) {
}) })
} }
func TestArrayWithExtraComma(t *testing.T) {
tree, err := Load("a = [1,\n2,\n3,\n]")
assertTree(t, tree, err, map[string]interface{}{
"a": []int64{int64(1), int64(2), int64(3)},
})
}
func TestArrayWithExtraCommaComment(t *testing.T) {
tree, err := Load("a = [1, # wow\n2, # such items\n3, # so array\n]")
assertTree(t, tree, err, map[string]interface{}{
"a": []int64{int64(1), int64(2), int64(3)},
})
}
func TestDuplicateGroups(t *testing.T) {
_, err := Load("[foo]\na=2\n[foo]b=3")
if err.Error() != "duplicated tables" {
t.Error("Bad error message:", err.Error())
}
}
func TestDuplicateKeys(t *testing.T) {
_, err := Load("foo = 2\nfoo = 3")
if err.Error() != "the following key was defined twice: foo" {
t.Error("Bad error message:", err.Error())
}
}
func TestEmptyIntermediateTable(t *testing.T) {
_, err := Load("[foo..bar]")
if err.Error() != "empty intermediate table" {
t.Error("Bad error message:", err.Error())
}
}
func TestImplicitDeclarationBefore(t *testing.T) {
tree, err := Load("[a.b.c]\nanswer = 42\n[a]\nbetter = 43")
assertTree(t, tree, err, map[string]interface{}{
"a": map[string]interface{}{
"b": map[string]interface{}{
"c": map[string]interface{}{
"answer": int64(42),
},
},
"better": int64(43),
},
})
}
func TestFloatsWithoutLeadingZeros(t *testing.T) {
_, err := Load("a = .42")
if err.Error() != "cannot start float with a dot" {
t.Error("Bad error message:", err.Error())
}
_, err = Load("a = -.42")
if err.Error() != "cannot start float with a dot" {
t.Error("Bad error message:", err.Error())
}
}
func TestMissingFile(t *testing.T) { func TestMissingFile(t *testing.T) {
_, err := LoadFile("foo.toml") _, err := LoadFile("foo.toml")
if err.Error() != "open foo.toml: no such file or directory" { if err.Error() != "open foo.toml: no such file or directory" {
@@ -197,3 +286,54 @@ func TestParseFile(t *testing.T) {
"clients.data": []interface{}{[]string{"gamma", "delta"}, []int64{1, 2}}, "clients.data": []interface{}{[]string{"gamma", "delta"}, []int64{1, 2}},
}) })
} }
func TestParseKeyGroupArray(t *testing.T) {
tree, err := Load("[[foo.bar]] a = 42\n[[foo.bar]] a = 69")
assertTree(t, tree, err, map[string]interface{}{
"foo": map[string]interface{}{
"bar": []map[string]interface{}{
{"a": int64(42)},
{"a": int64(69)},
},
},
})
}
func TestToTomlValue(t *testing.T) {
for idx, item := range []struct {
Value interface{}
Expect string
}{
{int64(12345), "12345"},
{float64(123.45), "123.45"},
{bool(true), "true"},
{"hello world", "\"hello world\""},
{"\b\t\n\f\r\"\\", "\"\\b\\t\\n\\f\\r\\\"\\\\\""},
{"\x05", "\"\\u0005\""},
{time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
"1979-05-27T07:32:00Z"},
{[]interface{}{"gamma", "delta"},
"[\n \"gamma\",\n \"delta\",\n]"},
} {
result := toTomlValue(item.Value, 0)
if result != item.Expect {
t.Errorf("Test %d - got '%s', expected '%s'", idx, result, item.Expect)
}
}
}
func TestToString(t *testing.T) {
tree := &TomlTree{
"foo": &TomlTree{
"bar": []*TomlTree{
{"a": int64(42)},
{"a": int64(69)},
},
},
}
result := tree.ToString()
expected := "\n[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n"
if result != expected {
t.Errorf("Expected got '%s', expected '%s'", result, expected)
}
}
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# fail out of the script if anything here fails
set -e
# set the path to the present working directory
export GOPATH=`pwd`
# Vendorize the BurntSushi test suite
# NOTE: this gets a specific release to avoid versioning issues
if [ ! -d 'src/github.com/BurntSushi/toml-test' ]; then
mkdir -p src/github.com/BurntSushi
git clone https://github.com/BurntSushi/toml-test.git src/github.com/BurntSushi/toml-test
fi
pushd src/github.com/BurntSushi/toml-test
git reset --hard '0.2.0' # use the released version, NOT tip
popd
go build -o toml-test github.com/BurntSushi/toml-test
# vendorize the current lib for testing
# NOTE: this basically mocks an install without having to go back out to github for code
mkdir -p src/github.com/pelletier/go-toml/cmd
cp *.go *.toml src/github.com/pelletier/go-toml
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
# Run basic unit tests and then the BurntSushi test suite
go test -v github.com/pelletier/go-toml
./toml-test ./test_program_bin | tee test_out
+159 -11
View File
@@ -1,20 +1,35 @@
// TOML markup language parser. // TOML markup language parser.
// //
// This version supports the specification as described in // This version supports the specification as described in
// https://github.com/mojombo/toml/tree/e3656ad493400895f4460f1244a25f8f8e31a32a // https://github.com/toml-lang/toml/blob/master/versions/toml-v0.2.0.md
package toml package toml
import ( import (
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time"
) )
// Definition of a TomlTree. // Definition of a TomlTree.
// This is the result of the parsing of a TOML file. // This is the result of the parsing of a TOML file.
type TomlTree map[string]interface{} type TomlTree map[string]interface{}
// Has returns a boolean indicating if the toplevel tree contains the given
// key.
func (t *TomlTree) Has(key string) bool {
mp := (map[string]interface{})(*t)
for k, _ := range mp {
if k == key {
return true
}
}
return false
}
// Keys returns the keys of the toplevel tree. // Keys returns the keys of the toplevel tree.
// Warning: this is a costly operation. // Warning: this is a costly operation.
func (t *TomlTree) Keys() []string { func (t *TomlTree) Keys() []string {
@@ -29,41 +44,74 @@ func (t *TomlTree) Keys() []string {
// Get the value at key in the TomlTree. // Get the value at key in the TomlTree.
// Key is a dot-separated path (e.g. a.b.c). // Key is a dot-separated path (e.g. a.b.c).
// Returns nil if the path does not exist in the tree. // Returns nil if the path does not exist in the tree.
// If keys is of length zero, the current tree is returned.
func (t *TomlTree) Get(key string) interface{} { func (t *TomlTree) Get(key string) interface{} {
if key == "" {
return t
}
return t.GetPath(strings.Split(key, "."))
}
// Returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned.
func (t *TomlTree) GetPath(keys []string) interface{} {
if len(keys) == 0 {
return t
}
subtree := t subtree := t
keys := strings.Split(key, ".")
for _, intermediate_key := range keys[:len(keys)-1] { for _, intermediate_key := range keys[:len(keys)-1] {
_, exists := (*subtree)[intermediate_key] _, exists := (*subtree)[intermediate_key]
if !exists { if !exists {
return nil return nil
} }
subtree = (*subtree)[intermediate_key].(*TomlTree) switch node := (*subtree)[intermediate_key].(type) {
case *TomlTree:
subtree = node
case []*TomlTree:
// go to most recent element
if len(node) == 0 {
return nil //(*subtree)[intermediate_key] = append(node, &TomlTree{})
}
subtree = node[len(node)-1]
}
} }
return (*subtree)[keys[len(keys)-1]] return (*subtree)[keys[len(keys)-1]]
} }
// Same as Get but with a default value // Same as Get but with a default value
func (t *TomlTree) GetDefault(key string, def interface{}) interface{} { func (t *TomlTree) GetDefault(key string, def interface{}) interface{} {
val := t.Get(key) val := t.Get(key)
if val == nil { if val == nil {
return def return def
} }
return val; return val
} }
// Set an element in the tree. // Set an element in the tree.
// Key is a dot-separated path (e.g. a.b.c). // Key is a dot-separated path (e.g. a.b.c).
// Creates all necessary intermediates trees, if needed. // Creates all necessary intermediates trees, if needed.
func (t *TomlTree) Set(key string, value interface{}) { func (t *TomlTree) Set(key string, value interface{}) {
t.SetPath(strings.Split(key, "."), value)
}
func (t *TomlTree) SetPath(keys []string, value interface{}) {
subtree := t subtree := t
keys := strings.Split(key, ".")
for _, intermediate_key := range keys[:len(keys)-1] { for _, intermediate_key := range keys[:len(keys)-1] {
_, exists := (*subtree)[intermediate_key] _, exists := (*subtree)[intermediate_key]
if !exists { if !exists {
var new_tree TomlTree = make(TomlTree) var new_tree TomlTree = make(TomlTree)
(*subtree)[intermediate_key] = &new_tree (*subtree)[intermediate_key] = &new_tree
} }
subtree = (*subtree)[intermediate_key].(*TomlTree) switch node := (*subtree)[intermediate_key].(type) {
case *TomlTree:
subtree = node
case []*TomlTree:
// go to most recent element
if len(node) == 0 {
(*subtree)[intermediate_key] = append(node, &TomlTree{})
}
subtree = node[len(node)-1]
}
} }
(*subtree)[keys[len(keys)-1]] = value (*subtree)[keys[len(keys)-1]] = value
} }
@@ -76,6 +124,9 @@ func (t *TomlTree) Set(key string, value interface{}) {
func (t *TomlTree) createSubTree(key string) { func (t *TomlTree) createSubTree(key string) {
subtree := t subtree := t
for _, intermediate_key := range strings.Split(key, ".") { for _, intermediate_key := range strings.Split(key, ".") {
if intermediate_key == "" {
panic("empty intermediate table")
}
_, exists := (*subtree)[intermediate_key] _, exists := (*subtree)[intermediate_key]
if !exists { if !exists {
var new_tree TomlTree = make(TomlTree) var new_tree TomlTree = make(TomlTree)
@@ -85,6 +136,104 @@ func (t *TomlTree) createSubTree(key string) {
} }
} }
// encodes a string to a TOML-compliant string value
func encodeTomlString(value string) string {
result := ""
for _, rr := range value {
int_rr := 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 int_rr < 0x001F {
result += fmt.Sprintf("\\u%0.4X", int_rr)
} 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"
} else {
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(keyspace string) string {
result := ""
for k, v := range (map[string]interface{})(*t) {
// figure out the keyspace
combined_key := k
if keyspace != "" {
combined_key = keyspace + "." + combined_key
}
// output based on type
switch node := v.(type) {
case []*TomlTree:
for _, item := range node {
if len(item.Keys()) > 0 {
result += fmt.Sprintf("\n[[%s]]\n", combined_key)
}
result += item.toToml(combined_key)
}
case *TomlTree:
if len(node.Keys()) > 0 {
result += fmt.Sprintf("\n[%s]\n", combined_key)
}
result += node.toToml(combined_key)
default:
result += fmt.Sprintf("%s = %s\n", k, toTomlValue(node, 0))
}
}
return result
}
// 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) ToString() string {
return t.toToml("")
}
// Create a TomlTree from a string. // Create a TomlTree from a string.
func Load(content string) (tree *TomlTree, err error) { func Load(content string) (tree *TomlTree, err error) {
defer func() { defer func() {
@@ -109,6 +258,5 @@ func LoadFile(path string) (tree *TomlTree, err error) {
s := string(buff) s := string(buff)
tree, err = Load(s) tree, err = Load(s)
} }
return return
} }
+24
View File
@@ -1 +1,25 @@
package toml package toml
import (
"testing"
)
func TestTomlGetPath(t *testing.T) {
node := make(TomlTree)
//TODO: set other node data
for idx, item := range []struct {
Path []string
Expected interface{}
}{
{ // empty path test
[]string{},
&node,
},
} {
result := node.GetPath(item.Path)
if result != item.Expected {
t.Errorf("GetPath[%d] %v - expected %v, got %v instead.", idx, item.Path, item.Expected, result)
}
}
}