Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86a7f2508e | |||
| 713478a34e | |||
| 5dd3b53635 | |||
| 262211488d | |||
| abdecb7be7 | |||
| f69e0b0837 | |||
| cd055fa448 | |||
| c9ea292f59 | |||
| 7b208738bc | |||
| c8b5633273 | |||
| b28c2d0c9e | |||
| 1c0f7f552c | |||
| 0773148832 | |||
| fbf99a1816 | |||
| 1cfdab9cee | |||
| dc20c454d7 | |||
| 0c4e891f3e | |||
| b1d602f733 | |||
| a4d623ad05 | |||
| 4fdde9794a | |||
| 3085454477 | |||
| 01609e0ab7 | |||
| a34fc5f051 | |||
| e8d5dbf787 | |||
| 5ffe2e5565 | |||
| 278c4d97ec | |||
| c743c90315 | |||
| 8081f3cc09 | |||
| 72f17747a0 | |||
| 979a055512 | |||
| def5433558 | |||
| c163b3f68b | |||
| bbe45c63f2 | |||
| 40a44dc51f | |||
| 2ba6587bf3 | |||
| 72d57d8477 | |||
| 0f6008f46e | |||
| be268e4049 | |||
| 53005a205f | |||
| 34d60ec92f |
@@ -0,0 +1 @@
|
|||||||
|
test_program/test_program_bin
|
||||||
@@ -1 +1,6 @@
|
|||||||
language: go
|
language: go
|
||||||
|
script: "./test.sh"
|
||||||
|
go:
|
||||||
|
- 1.1
|
||||||
|
- 1.2
|
||||||
|
- tip
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,9 +390,43 @@ 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
|
||||||
|
|
||||||
|
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)
|
l.emit(tokenLeftBracket)
|
||||||
return lexInsideKeyGroup
|
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 {
|
||||||
for {
|
for {
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, ""},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ package toml
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +14,8 @@ 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 {
|
||||||
@@ -179,7 +249,8 @@ func parse(flow chan token) *TomlTree {
|
|||||||
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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,15 +44,36 @@ 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]]
|
||||||
}
|
}
|
||||||
@@ -48,22 +84,34 @@ func (t *TomlTree) GetDefault(key string, def interface{}) interface{} {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user