Compare commits

...

28 Commits

Author SHA1 Message Date
Thomas Pelletier 64ff1ea4d5 Don't hang when reading an invalid rvalue (#77)
Fixes #76
2016-06-30 16:21:25 +02:00
Sam Broughton b39f6ef1f9 Add a toml linter (#74)
* Add a toml linter

* Use if/else instead of os.Exit(0)

* Add usage warning about destructive changes
2016-06-06 12:29:13 +02:00
Sam Broughton c187221f01 Implement fmt.Stringer and alias ToString (#73) 2016-06-06 10:23:55 +02:00
Thomas Pelletier 8e6ab94eec Fix inline tables parsing
Inline tables were wrapped inside a TomlValue, although they should
just be part of the tree.
2016-04-22 17:38:16 +02:00
Thomas Pelletier 8d9c606c69 Improve test coverage (#66) 2016-04-22 14:26:15 +02:00
Thomas Pelletier 288bc57940 Better logging for parser tests (#65)
* Better logging for parser tests

* Add spew to tests deps list
2016-04-22 11:01:31 +02:00
Thomas Pelletier e3b2497729 TomlTree.ToMap (#59)
* Extract TomlTree conversion to its own file

* Implement ToMap

* Reorder imports in tomltree_conversions
2016-04-22 09:46:28 +02:00
Thomas Pelletier 1a8565204c Fix multiline strings (#62) 2016-04-21 17:47:41 +02:00
Thomas Pelletier e58cfd32d4 Bump to golang 1.6.2 on Travis 2016-04-21 09:22:47 +02:00
Cameron Moore a2ae216b47 Add more token tests (#58) 2016-04-19 09:43:26 +02:00
Thomas Pelletier 8645be8dc7 Merge pull request #57 from moorereason/simplify
Fix a couple issues found by gosimple
2016-04-19 09:41:51 +02:00
Cameron Moore 99b9371c53 Use strings.ContainsRune instead of IndexRune 2016-04-18 17:14:57 -05:00
Cameron Moore 92c565e02b Use literal string for regexp pattern 2016-04-18 17:14:18 -05:00
Cameron Moore 6e26017b00 Clean up lint (#56)
The only real change in this commit is that MaxInt is made private.
Everything else should be gofmt'ing, docs and cleanup of lint.
2016-04-18 16:58:23 +02:00
Thomas Pelletier 9d93af61de Add couple tests 2016-04-18 16:46:44 +02:00
Thomas Pelletier 4d8fb95ffe Update coveralls badge 2016-04-18 10:02:19 +02:00
Thomas Pelletier 0e41db2176 Update documentation for Query
Fix #54
2016-04-18 09:51:42 +02:00
Thomas Pelletier afca7f3334 Hardcode Go versions in .travis.yml 2016-04-13 09:23:15 +02:00
Thomas Pelletier d6a90e60ed Fix #52: query matcher doesn't handle arrays tables
Also improve coverage of query matcher.
2016-03-16 09:56:04 -07:00
Thomas Pelletier fe63e9f76d Run tests for 1.6 2016-02-20 13:29:42 +01:00
Thomas Pelletier 7f50e4c339 Merge pull request #51 from pelletier/pelletier/fix-crlf-support
Fix support for CRLF line ending
2016-02-20 13:20:03 +01:00
Thomas Pelletier a402e618c3 sudo is not needed by travis anymore 2016-02-19 14:17:07 +01:00
Thomas Pelletier 2df083520a Fix support for CRLF line ending 2016-02-19 14:12:13 +01:00
Thomas Pelletier 8176e30b38 Fix printf formatting 2016-01-31 17:07:37 +01:00
Thomas Pelletier 14c964fc02 Merge pull request #49 from pelletier/generic-input
Generic input
2016-01-31 16:57:17 +01:00
Thomas Pelletier f963bc320f Generic input
Fixes #47
2016-01-31 16:54:40 +01:00
Thomas Pelletier 0488b850c6 Have Travis run 1.5.3 2016-01-14 11:33:30 +01:00
Thomas Pelletier 346e676fa2 2015 -> 2016 2016-01-05 10:06:54 +01:00
24 changed files with 1061 additions and 384 deletions
+3 -5
View File
@@ -1,11 +1,9 @@
language: go language: go
script: "./test.sh" script: "./test.sh"
sudo: false
go: go:
- 1.2.2 - 1.4.3
- 1.3.3 - 1.5.4
- 1.4.2 - 1.6.2
- 1.5.1
- tip - tip
before_install: before_install:
- go get github.com/axw/gocov/gocov - go get github.com/axw/gocov/gocov
+2 -2
View File
@@ -7,7 +7,7 @@ This library supports TOML version
[![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml) [![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml)
[![Build Status](https://travis-ci.org/pelletier/go-toml.svg?branch=master)](https://travis-ci.org/pelletier/go-toml) [![Build Status](https://travis-ci.org/pelletier/go-toml.svg?branch=master)](https://travis-ci.org/pelletier/go-toml)
[![Coverage Status](https://coveralls.io/repos/pelletier/go-toml/badge.svg?branch=master&service=github)](https://coveralls.io/github/pelletier/go-toml?branch=master) [![Coverage Status](https://coveralls.io/repos/github/pelletier/go-toml/badge.svg?branch=master)](https://coveralls.io/github/pelletier/go-toml?branch=master)
## Features ## Features
@@ -98,7 +98,7 @@ You can run both of them using `./test.sh`.
## License ## License
Copyright (c) 2013 - 2015 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
+61
View File
@@ -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
}
+7 -2
View File
@@ -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
View File
@@ -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)
} }
} }
} }
+29
View File
@@ -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
+266 -200
View File
@@ -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,10 +134,15 @@ 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 {
@@ -142,7 +153,8 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
return l.lexKey return l.lexKey
} }
if l.next() == eof { if next == eof {
l.next()
break break
} }
} }
@@ -177,9 +189,10 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
return l.lexLiteralString 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
} }
@@ -196,14 +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
} }
dateMatch := dateRegexp.FindString(l.input[l.pos:]) if next == eof {
l.next()
break
}
possibleDate := string(l.input.Peek(35))
dateMatch := dateRegexp.FindString(possibleDate)
if dateMatch != "" { if dateMatch != "" {
l.ignore() l.fastForward(len(dateMatch))
l.pos += len(dateMatch)
return l.lexDate return l.lexDate
} }
@@ -211,13 +230,11 @@ 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)
@@ -225,15 +242,13 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
} }
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
l.ignore() l.next()
l.pos++
l.emit(tokenLeftCurlyBrace) l.emit(tokenLeftCurlyBrace)
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
l.ignore() l.next()
l.pos++
l.emit(tokenRightCurlyBrace) l.emit(tokenRightCurlyBrace)
return l.lexRvalue return l.lexRvalue
} }
@@ -244,137 +259,147 @@ func (l *tomlLexer) lexDate() tomlLexStateFn {
} }
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 := ""
inQuotes := false
for r := l.next(); isKeyChar(r) || r == '\n'; r = l.next() { for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
if r == '"' { if r == '"' {
inQuotes = !inQuotes 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' { } else if r == '\n' {
return l.errorf("keys cannot contain new lines") return l.errorf("keys cannot contain new lines")
} else if isSpace(r) && !inQuotes { } else if isSpace(r) {
break break
} else if !isValidBareChar(r) && !inQuotes { } else if !isValidBareChar(r) {
return l.errorf("keys cannot contain %c character", 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) lexLiteralString() tomlLexStateFn { func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) {
l.pos++
l.ignore()
growingString := "" growingString := ""
// handle special case for triple-quote if discardLeadingNewLine {
terminator := "'" if l.follow("\r\n") {
if l.follow("''") { l.skip()
l.pos += 2 l.skip()
l.ignore() } else if l.peek() == '\n' {
terminator = "'''" l.skip()
// special case: discard leading newline
if l.peek() == '\n' {
l.pos++
l.ignore()
} }
} }
// find end of string // find end of string
for { for {
if l.follow(terminator) { if l.follow(terminator) {
l.emitWithValue(tokenString, growingString) return growingString, nil
l.pos += len(terminator)
l.ignore()
return l.lexRvalue
} }
growingString += string(l.peek()) next := l.peek()
if next == eof {
if l.next() == eof {
break break
} }
growingString += string(l.next())
} }
return l.errorf("unclosed string") return "", errors.New("unclosed string")
} }
func (l *tomlLexer) lexString() tomlLexStateFn { func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
l.pos++ l.skip()
l.ignore()
growingString := ""
// handle special case for triple-quote // handle special case for triple-quote
terminator := "\"" terminator := "'"
if l.follow("\"\"") { discardLeadingNewLine := false
l.pos += 2 if l.follow("''") {
l.ignore() l.skip()
terminator = "\"\"\"" l.skip()
terminator = "'''"
discardLeadingNewLine = true
}
// special case: discard leading newline str, err := l.lexLiteralStringAsString(terminator, discardLeadingNewLine)
if l.peek() == '\n' { if err != nil {
l.pos++ return l.errorf(err.Error())
l.ignore() }
l.emitWithValue(tokenString, str)
l.fastForward(len(terminator))
l.ignore()
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 { for {
if l.follow(terminator) { if l.follow(terminator) {
l.emitWithValue(tokenString, growingString) return growingString, nil
l.pos += len(terminator)
l.ignore()
return l.lexRvalue
} }
if l.follow("\\") { if l.follow("\\") {
l.pos++ l.next()
switch l.peek() { switch l.peek() {
case '\r': case '\r':
fallthrough fallthrough
@@ -384,87 +409,119 @@ func (l *tomlLexer) lexString() tomlLexStateFn {
fallthrough fallthrough
case ' ': case ' ':
// skip all whitespace chars following backslash // skip all whitespace chars following backslash
l.pos++
for strings.ContainsRune("\r\n\t ", l.peek()) { for strings.ContainsRune("\r\n\t ", l.peek()) {
l.pos++ l.next()
} }
l.pos--
case '"': case '"':
growingString += "\"" growingString += "\""
l.next()
case 'n': case 'n':
growingString += "\n" growingString += "\n"
l.next()
case 'b': case 'b':
growingString += "\b" growingString += "\b"
l.next()
case 'f': case 'f':
growingString += "\f" growingString += "\f"
l.next()
case '/': case '/':
growingString += "/" growingString += "/"
l.next()
case 't': case 't':
growingString += "\t" growingString += "\t"
l.next()
case 'r': case 'r':
growingString += "\r" growingString += "\r"
l.next()
case '\\': case '\\':
growingString += "\\" growingString += "\\"
l.next()
case 'u': case 'u':
l.pos++ l.next()
code := "" code := ""
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
c := l.peek() c := l.peek()
l.pos++
if !isHexDigit(c) { if !isHexDigit(c) {
return l.errorf("unfinished unicode escape") return "", errors.New("unfinished unicode escape")
} }
l.next()
code = code + string(c) code = code + string(c)
} }
l.pos--
intcode, err := strconv.ParseInt(code, 16, 32) intcode, err := strconv.ParseInt(code, 16, 32)
if err != nil { if err != nil {
return l.errorf("invalid unicode escape: \\u" + code) return "", errors.New("invalid unicode escape: \\u" + code)
} }
growingString += string(rune(intcode)) growingString += string(rune(intcode))
case 'U': case 'U':
l.pos++ l.next()
code := "" code := ""
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
c := l.peek() c := l.peek()
l.pos++
if !isHexDigit(c) { if !isHexDigit(c) {
return l.errorf("unfinished unicode escape") return "", errors.New("unfinished unicode escape")
} }
l.next()
code = code + string(c) code = code + string(c)
} }
l.pos--
intcode, err := strconv.ParseInt(code, 16, 64) intcode, err := strconv.ParseInt(code, 16, 64)
if err != nil { if err != nil {
return l.errorf("invalid unicode escape: \\U" + code) return "", errors.New("invalid unicode escape: \\U" + code)
} }
growingString += string(rune(intcode)) growingString += string(rune(intcode))
default: default:
return l.errorf("invalid escape sequence: \\" + string(l.peek())) return "", errors.New("invalid escape sequence: \\" + string(l.peek()))
} }
} else { } else {
r := l.peek() r := l.peek()
if 0x00 <= r && r <= 0x1F {
return l.errorf("unescaped control character %U", r) if 0x00 <= r && r <= 0x1F && !(acceptNewLines && (r == '\n' || r == '\r')) {
return "", fmt.Errorf("unescaped control character %U", r)
} }
l.next()
growingString += string(r) 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
} }
@@ -474,86 +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 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' { } else if next == 'e' || next == 'E' {
expSeen = true expSeen = true
if !l.accept("+") { l.next()
l.accept("-") 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 == '_' { } else if next == '_' {
l.next()
} else { } else {
l.backup()
break break
} }
if pointSeen && !digitSeen { if pointSeen && !digitSeen {
@@ -572,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}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})") 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
+82 -3
View File
@@ -1,15 +1,19 @@
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("compared (got)", token, "to (expected)", expected)
t.Log("\tvalue:", token.val, "<->", expected.val) 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("\ttype:", token.typ.String(), "<->", expected.typ.String())
t.Log("\tline:", token.Line, "<->", expected.Line) t.Log("\tline:", token.Line, "<->", expected.Line)
t.Log("\tcolumn:", token.Col, "<->", expected.Col) t.Log("\tcolumn:", token.Col, "<->", expected.Col)
@@ -83,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"},
@@ -464,6 +480,12 @@ func TestKeyEqualNumber(t *testing.T) {
token{Position{1, 8}, tokenFloat, "9_224_617.445_991_228_313"}, token{Position{1, 8}, tokenFloat, "9_224_617.445_991_228_313"},
token{Position{1, 33}, tokenEOF, ""}, 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) {
@@ -491,6 +513,16 @@ func TestKeyEqualStringUnicodeEscape(t *testing.T) {
token{Position{1, 8}, tokenString, "hello δ"}, token{Position{1, 8}, tokenString, "hello δ"},
token{Position{1, 25}, tokenEOF, ""}, 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) { func TestKeyEqualStringNoEscape(t *testing.T) {
@@ -531,6 +563,11 @@ func TestLiteralString(t *testing.T) {
token{Position{1, 8}, tokenString, `<\i\c*\s*>`}, token{Position{1, 8}, tokenString, `<\i\c*\s*>`},
token{Position{1, 19}, tokenEOF, ""}, 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) { func TestMultilineLiteralString(t *testing.T) {
@@ -547,6 +584,12 @@ func TestMultilineLiteralString(t *testing.T) {
token{Position{2, 1}, tokenString, "hello\n'literal'\nworld"}, token{Position{2, 1}, tokenString, "hello\n'literal'\nworld"},
token{Position{4, 9}, tokenEOF, ""}, 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) { func TestMultilineString(t *testing.T) {
@@ -557,7 +600,7 @@ func TestMultilineString(t *testing.T) {
token{Position{1, 34}, tokenEOF, ""}, token{Position{1, 34}, tokenEOF, ""},
}) })
testFlow(t, "foo = \"\"\"\nhello\\\n\"literal\"\\\nworld\"\"\"", []token{ testFlow(t, "foo = \"\"\"\r\nhello\\\r\n\"literal\"\\\nworld\"\"\"", []token{
token{Position{1, 1}, tokenKey, "foo"}, token{Position{1, 1}, tokenKey, "foo"},
token{Position{1, 5}, tokenEqual, "="}, token{Position{1, 5}, tokenEqual, "="},
token{Position{2, 1}, tokenString, "hello\"literal\"world"}, token{Position{2, 1}, tokenString, "hello\"literal\"world"},
@@ -584,6 +627,20 @@ func TestMultilineString(t *testing.T) {
token{Position{1, 11}, tokenString, "The quick brown fox jumps over the lazy dog."}, token{Position{1, 11}, tokenString, "The quick brown fox jumps over the lazy dog."},
token{Position{5, 11}, tokenEOF, ""}, 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) {
@@ -594,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{
@@ -627,3 +692,17 @@ func TestInvalidFloat(t *testing.T) {
token{Position{1, 7}, tokenEOF, ""}, 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 \`},
})
}
+9 -2
View File
@@ -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 -3
View File
@@ -109,7 +109,7 @@ func TestPathSliceStart(t *testing.T) {
assertPath(t, assertPath(t,
"$[123:]", "$[123:]",
buildPath( buildPath(
newMatchSliceFn(123, MaxInt, 1), newMatchSliceFn(123, maxInt, 1),
)) ))
} }
@@ -133,7 +133,7 @@ func TestPathSliceStartStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[123::7]", "$[123::7]",
buildPath( buildPath(
newMatchSliceFn(123, MaxInt, 7), newMatchSliceFn(123, maxInt, 7),
)) ))
} }
@@ -149,7 +149,7 @@ func TestPathSliceStep(t *testing.T) {
assertPath(t, assertPath(t,
"$[::7]", "$[::7]",
buildPath( buildPath(
newMatchSliceFn(0, MaxInt, 7), newMatchSliceFn(0, maxInt, 7),
)) ))
} }
+9 -1
View File
@@ -208,7 +208,15 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
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[keyVal] = &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
} }
+106 -5
View File
@@ -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{})
@@ -285,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())
} }
} }
@@ -441,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())
} }
@@ -494,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{}{
@@ -506,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{}{
@@ -618,6 +714,11 @@ func TestInvalidGroupArray(t *testing.T) {
if err == nil { if err == nil {
t.Error("Should error") 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) { func TestDoubleEqual(t *testing.T) {
+6 -8
View File
@@ -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
+29 -18
View File
@@ -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
+70
View File
@@ -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
View File
@@ -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()
+2 -3
View File
@@ -11,7 +11,7 @@ import (
"fmt" "fmt"
) )
const MaxInt = int(^uint(0) >> 1) const maxInt = int(^uint(0) >> 1)
type queryParser struct { type queryParser struct {
flow chan token flow chan token
@@ -138,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 {
@@ -204,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, MaxInt, 1 start, end, step := 0, maxInt, 1
// parse optional start // parse optional start
tok := p.getToken() tok := p.getToken()
+3
View File
@@ -19,6 +19,9 @@ function git_clone() {
popd popd
} }
go get github.com/pelletier/go-buffruneio
go get github.com/davecgh/go-spew/spew
# get code for BurntSushi TOML validation # get code for BurntSushi TOML validation
# pinning all to 'HEAD' for version 0.3.x work (TODO: pin to commit hash when tests stabilize) # 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 master HEAD
-3
View File
@@ -107,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)
} }
+67
View File
@@ -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)
}
}
}
+21 -123
View File
@@ -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,7 @@ func newTomlTree() *TomlTree {
} }
} }
// TreeFromMap initializes a new TomlTree object using the given map.
func TreeFromMap(m map[string]interface{}) *TomlTree { func TreeFromMap(m map[string]interface{}) *TomlTree {
return &TomlTree{ return &TomlTree{
values: m, values: m,
@@ -94,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
@@ -246,122 +246,17 @@ func (t *TomlTree) createSubTree(keys []string, pos Position) error {
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 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
}
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 {
@@ -370,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
View File
@@ -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"))
}
}
+144
View File
@@ -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
}
+82
View File
@@ -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)
}