Compare commits

..

20 Commits

Author SHA1 Message Date
Thomas Pelletier 65ca806488 Fix Unmarshaler call when value is missing (#439)
Fixes #431
2020-09-12 14:42:04 -04:00
Cameron Moore 5c94d86029 Use strings.Builder in lexer (#438)
Replace all string building operations in the lexer with
strings.Builder. Doing so shows significant performance improvements.
BurntSushi still has a slight edge in CPU performance, but there's still
much work to do on memory performance.

name                       old time/op    new time/op    delta
ParseToml-2                   311µs ± 0%     273µs ± 3%  -12.29%  (p=0.008 n=5+5)
UnmarshalToml-2               386µs ± 4%     349µs ± 3%   -9.63%  (p=0.008 n=5+5)
UnmarshalBurntSushiToml-2     368µs ± 8%     341µs ± 2%     ~     (p=0.056 n=5+5)

name                       old alloc/op   new alloc/op   delta
ParseToml-2                   132kB ± 0%     118kB ± 0%  -11.07%  (p=0.008 n=5+5)
UnmarshalToml-2               147kB ± 0%     133kB ± 0%   -9.92%  (p=0.008 n=5+5)
UnmarshalBurntSushiToml-2    82.6kB ± 0%    82.6kB ± 0%     ~     (p=1.000 n=5+5)

name                       old allocs/op  new allocs/op  delta
ParseToml-2                   3.19k ± 0%     1.91k ± 0%  -40.19%  (p=0.008 n=5+5)
UnmarshalToml-2               4.03k ± 0%     2.75k ± 0%  -31.83%  (p=0.008 n=5+5)
UnmarshalBurntSushiToml-2     1.73k ± 0%     1.73k ± 0%     ~     (all equal)

Out of curiosity, I benchmarked the results of updating each function
along the way to see how each change effected the overall performance:

name \ time/op             master       lexKey       lexLitStringAsString  lexStringAsString
ParseToml-2                 311µs ± 0%   299µs ± 1%            290µs ± 3%         273µs ± 3%
UnmarshalToml-2             386µs ± 4%   381µs ± 2%            364µs ± 2%         349µs ± 3%
UnmarshalBurntSushiToml-2   368µs ± 8%   341µs ± 2%            345µs ± 5%         341µs ± 2%

name \ alloc/op            master       lexKey       lexLitStringAsString  lexStringAsString
ParseToml-2                 132kB ± 0%   132kB ± 0%            125kB ± 0%         118kB ± 0%
UnmarshalToml-2             147kB ± 0%   146kB ± 0%            140kB ± 0%         133kB ± 0%
UnmarshalBurntSushiToml-2  82.6kB ± 0%  82.6kB ± 0%           82.6kB ± 0%        82.6kB ± 0%

name \ allocs/op           master       lexKey       lexLitStringAsString  lexStringAsString
ParseToml-2                 3.19k ± 0%   2.86k ± 0%            2.49k ± 0%         1.91k ± 0%
UnmarshalToml-2             4.03k ± 0%   3.70k ± 0%            3.33k ± 0%         2.75k ± 0%
UnmarshalBurntSushiToml-2   1.73k ± 0%   1.73k ± 0%            1.73k ± 0%         1.73k ± 0%

Benchmarks were run from the benchmark/ directory using:

go test -bench=.*Toml -benchmem -count=5 ./...
2020-09-12 12:01:32 -04:00
Thomas Pelletier b76eb62117 Marshal into empty interface{} (#433)
Allows to marshal a TOML document into an empty `interface{}`, resulting
in a `map[string]interface{}`.

Fixes #432
2020-09-11 10:04:46 -04:00
Thomas Pelletier 196ce3a1f6 Support go 1.15 (#434)
Fixes #428
2020-09-10 21:39:16 -04:00
Stephen Levine 9f8f82dfe8 Fix index exception when setting empty Tree slice (#425) 2020-09-10 21:19:18 -04:00
Stephen Levine 661484ae7e Add *Tree.SetPositionPath (#426)
Signed-off-by: Stephen Levine <stephen.levine@gmail.com>
2020-07-31 23:44:50 -04:00
AllenX2018 34de94e6a8 fix issue #421 2020-07-08 19:02:44 +08:00
Allen 88263a05cc move benchmark to a seperate diectory (#420)
Fixes #418
2020-06-15 17:55:19 -04:00
jixiuf 1dbe20e76c Fix TreeFromMap on list of interfaces (#416)
Fixes #415
2020-06-13 12:27:57 -04:00
AllenX2018 05bf3807d3 fix issue#414 2020-06-11 12:10:57 +08:00
Thomas Pelletier 06838de5d2 Merge branch 'RiyaJohn-better_lists' 2020-06-01 10:18:10 -04:00
Thomas Pelletier db62263e3e Added exta tests for GetArrayPath 2020-06-01 10:16:36 -04:00
RiyaJohn 2d866e3fae fix: rm int, int32, float32 2020-05-21 22:50:23 +05:30
RiyaJohn 100799f7b7 add testcase for bool 2020-05-18 16:13:17 +05:30
RiyaJohn ecd155a62f Merge remote-tracking branch 'upstream/master' into better_lists 2020-05-18 15:54:12 +05:30
RiyaJohn bcacc71a18 feat: add GetArray() with testcases 2020-05-18 15:26:15 +05:30
RiyaJohn 71c324cf7b add getArray logic 2020-04-27 12:06:33 +05:30
RiyaJohn 4c840f1b8b Merge remote-tracking branch 'upstream/master' 2020-04-27 12:02:06 +05:30
RiyaJohn 5060c72d94 add testcase for query pkg 2020-04-17 16:09:08 +05:30
RiyaJohn 0a459e938d add float to test case to check leading zeroes in exponent parts 2020-04-16 14:15:33 +05:30
17 changed files with 632 additions and 108 deletions
+20 -20
View File
@@ -13,9 +13,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.14" displayName: "Install Go 1.15"
inputs: inputs:
version: "1.14" version: "1.15"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml - script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml - script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
@@ -36,9 +36,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.14" displayName: "Install Go 1.15"
inputs: inputs:
version: "1.14" version: "1.15"
- task: Go@0 - task: Go@0
displayName: "go fmt ./..." displayName: "go fmt ./..."
inputs: inputs:
@@ -51,9 +51,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.14" displayName: "Install Go 1.15"
inputs: inputs:
version: "1.14" version: "1.15"
- task: Go@0 - task: Go@0
displayName: "Generate coverage" displayName: "Generate coverage"
inputs: inputs:
@@ -71,9 +71,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.14" displayName: "Install Go 1.15"
inputs: inputs:
version: "1.14" version: "1.15"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- task: Bash@3 - task: Bash@3
inputs: inputs:
@@ -86,9 +86,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.14" displayName: "Install Go 1.15"
inputs: inputs:
version: "1.14" version: "1.15"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml - script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml - script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
@@ -102,6 +102,15 @@ stages:
displayName: "unit tests" displayName: "unit tests"
strategy: strategy:
matrix: matrix:
linux 1.15:
goVersion: '1.15'
imageName: 'ubuntu-latest'
mac 1.15:
goVersion: '1.15'
imageName: 'macOS-latest'
windows 1.15:
goVersion: '1.15'
imageName: 'windows-latest'
linux 1.14: linux 1.14:
goVersion: '1.14' goVersion: '1.14'
imageName: 'ubuntu-latest' imageName: 'ubuntu-latest'
@@ -111,15 +120,6 @@ stages:
windows 1.14: windows 1.14:
goVersion: '1.14' goVersion: '1.14'
imageName: 'windows-latest' imageName: 'windows-latest'
linux 1.13:
goVersion: '1.13'
imageName: 'ubuntu-latest'
mac 1.13:
goVersion: '1.13'
imageName: 'macOS-latest'
windows 1.13:
goVersion: '1.13'
imageName: 'windows-latest'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
@@ -155,7 +155,7 @@ stages:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go" displayName: "Install Go"
inputs: inputs:
version: 1.14 version: 1.15
- task: Bash@3 - task: Bash@3
inputs: inputs:
targetType: inline targetType: inline
+4
View File
@@ -20,11 +20,15 @@ git clone ${reference_git} ${ref_tempdir} >/dev/null 2>/dev/null
pushd ${ref_tempdir} >/dev/null pushd ${ref_tempdir} >/dev/null
git checkout ${reference_ref} >/dev/null 2>/dev/null git checkout ${reference_ref} >/dev/null 2>/dev/null
go test -bench=. -benchmem | tee ${ref_benchmark} go test -bench=. -benchmem | tee ${ref_benchmark}
cd benchmark
go test -bench=. -benchmem | tee -a ${ref_benchmark}
popd >/dev/null popd >/dev/null
echo "" echo ""
echo "=== local" echo "=== local"
go test -bench=. -benchmem | tee ${local_benchmark} go test -bench=. -benchmem | tee ${local_benchmark}
cd benchmark
go test -bench=. -benchmem | tee -a ${local_benchmark}
echo "" echo ""
echo "=== diff" echo "=== diff"
@@ -1,4 +1,4 @@
package toml package benchmark
import ( import (
"bytes" "bytes"
@@ -8,7 +8,8 @@ import (
"time" "time"
burntsushi "github.com/BurntSushi/toml" burntsushi "github.com/BurntSushi/toml"
yaml "gopkg.in/yaml.v2" "github.com/pelletier/go-toml"
"gopkg.in/yaml.v2"
) )
type benchmarkDoc struct { type benchmarkDoc struct {
@@ -124,7 +125,7 @@ func BenchmarkParseToml(b *testing.B) {
} }
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := LoadReader(bytes.NewReader(fileBytes)) _, err := toml.LoadReader(bytes.NewReader(fileBytes))
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@@ -139,7 +140,7 @@ func BenchmarkUnmarshalToml(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
target := benchmarkDoc{} target := benchmarkDoc{}
err := Unmarshal(bytes, &target) err := toml.Unmarshal(bytes, &target)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
+11
View File
@@ -0,0 +1,11 @@
module github.com/pelletier/go-toml/benchmark
go 1.12
require (
github.com/BurntSushi/toml v0.3.1
github.com/pelletier/go-toml v0.0.0
gopkg.in/yaml.v2 v2.3.0
)
replace github.com/pelletier/go-toml => ../
+8
View File
@@ -0,0 +1,8 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+1 -5
View File
@@ -2,8 +2,4 @@ module github.com/pelletier/go-toml
go 1.12 go 1.12
require ( require github.com/davecgh/go-spew v1.1.1
github.com/BurntSushi/toml v0.3.1
github.com/davecgh/go-spew v1.1.1
gopkg.in/yaml.v2 v2.3.0
)
+53 -47
View File
@@ -306,7 +306,7 @@ func (l *tomlLexer) lexComma() tomlLexStateFn {
// Parse the key and emits its value without escape sequences. // Parse the key and emits its value without escape sequences.
// bare keys, basic string keys and literal string keys are supported. // bare keys, basic string keys and literal string keys are supported.
func (l *tomlLexer) lexKey() tomlLexStateFn { func (l *tomlLexer) lexKey() tomlLexStateFn {
growingString := "" var sb strings.Builder
for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() { for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
if r == '"' { if r == '"' {
@@ -315,7 +315,9 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil { if err != nil {
return l.errorf(err.Error()) return l.errorf(err.Error())
} }
growingString += "\"" + str + "\"" sb.WriteString("\"")
sb.WriteString(str)
sb.WriteString("\"")
l.next() l.next()
continue continue
} else if r == '\'' { } else if r == '\'' {
@@ -324,41 +326,45 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil { if err != nil {
return l.errorf(err.Error()) return l.errorf(err.Error())
} }
growingString += "'" + str + "'" sb.WriteString("'")
sb.WriteString(str)
sb.WriteString("'")
l.next() l.next()
continue 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) { } else if isSpace(r) {
str := " " var str strings.Builder
str.WriteString(" ")
// skip trailing whitespace // skip trailing whitespace
l.next() l.next()
for r = l.peek(); isSpace(r); r = l.peek() { for r = l.peek(); isSpace(r); r = l.peek() {
str += string(r) str.WriteRune(r)
l.next() l.next()
} }
// break loop if not a dot // break loop if not a dot
if r != '.' { if r != '.' {
break break
} }
str += "." str.WriteString(".")
// skip trailing whitespace after dot // skip trailing whitespace after dot
l.next() l.next()
for r = l.peek(); isSpace(r); r = l.peek() { for r = l.peek(); isSpace(r); r = l.peek() {
str += string(r) str.WriteRune(r)
l.next() l.next()
} }
growingString += str sb.WriteString(str.String())
continue continue
} else if r == '.' { } else if r == '.' {
// skip // skip
} else if !isValidBareChar(r) { } 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) sb.WriteRune(r)
l.next() l.next()
} }
l.emitWithValue(tokenKey, growingString) l.emitWithValue(tokenKey, sb.String())
return l.lexVoid return l.lexVoid
} }
@@ -383,7 +389,7 @@ func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
} }
func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) { func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) {
growingString := "" var sb strings.Builder
if discardLeadingNewLine { if discardLeadingNewLine {
if l.follow("\r\n") { if l.follow("\r\n") {
@@ -397,14 +403,14 @@ func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNe
// find end of string // find end of string
for { for {
if l.follow(terminator) { if l.follow(terminator) {
return growingString, nil return sb.String(), nil
} }
next := l.peek() next := l.peek()
if next == eof { if next == eof {
break break
} }
growingString += string(l.next()) sb.WriteRune(l.next())
} }
return "", errors.New("unclosed string") return "", errors.New("unclosed string")
@@ -438,7 +444,7 @@ func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
// Terminator is the substring indicating the end of the token. // Terminator is the substring indicating the end of the token.
// The resulting string does not include the terminator. // The resulting string does not include the terminator.
func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) { func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) {
growingString := "" var sb strings.Builder
if discardLeadingNewLine { if discardLeadingNewLine {
if l.follow("\r\n") { if l.follow("\r\n") {
@@ -451,7 +457,7 @@ func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine,
for { for {
if l.follow(terminator) { if l.follow(terminator) {
return growingString, nil return sb.String(), nil
} }
if l.follow("\\") { if l.follow("\\") {
@@ -469,61 +475,61 @@ func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine,
l.next() l.next()
} }
case '"': case '"':
growingString += "\"" sb.WriteString("\"")
l.next() l.next()
case 'n': case 'n':
growingString += "\n" sb.WriteString("\n")
l.next() l.next()
case 'b': case 'b':
growingString += "\b" sb.WriteString("\b")
l.next() l.next()
case 'f': case 'f':
growingString += "\f" sb.WriteString("\f")
l.next() l.next()
case '/': case '/':
growingString += "/" sb.WriteString("/")
l.next() l.next()
case 't': case 't':
growingString += "\t" sb.WriteString("\t")
l.next() l.next()
case 'r': case 'r':
growingString += "\r" sb.WriteString("\r")
l.next() l.next()
case '\\': case '\\':
growingString += "\\" sb.WriteString("\\")
l.next() l.next()
case 'u': case 'u':
l.next() l.next()
code := "" var code strings.Builder
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
c := l.peek() c := l.peek()
if !isHexDigit(c) { if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape") return "", errors.New("unfinished unicode escape")
} }
l.next() l.next()
code = code + string(c) code.WriteRune(c)
} }
intcode, err := strconv.ParseInt(code, 16, 32) intcode, err := strconv.ParseInt(code.String(), 16, 32)
if err != nil { if err != nil {
return "", errors.New("invalid unicode escape: \\u" + code) return "", errors.New("invalid unicode escape: \\u" + code.String())
} }
growingString += string(rune(intcode)) sb.WriteRune(rune(intcode))
case 'U': case 'U':
l.next() l.next()
code := "" var code strings.Builder
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
c := l.peek() c := l.peek()
if !isHexDigit(c) { if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape") return "", errors.New("unfinished unicode escape")
} }
l.next() l.next()
code = code + string(c) code.WriteRune(c)
} }
intcode, err := strconv.ParseInt(code, 16, 64) intcode, err := strconv.ParseInt(code.String(), 16, 64)
if err != nil { if err != nil {
return "", errors.New("invalid unicode escape: \\U" + code) return "", errors.New("invalid unicode escape: \\U" + code.String())
} }
growingString += string(rune(intcode)) sb.WriteRune(rune(intcode))
default: default:
return "", errors.New("invalid escape sequence: \\" + string(l.peek())) return "", errors.New("invalid escape sequence: \\" + string(l.peek()))
} }
@@ -534,7 +540,7 @@ func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine,
return "", fmt.Errorf("unescaped control character %U", r) return "", fmt.Errorf("unescaped control character %U", r)
} }
l.next() l.next()
growingString += string(r) sb.WriteRune(r)
} }
if l.peek() == eof { if l.peek() == eof {
@@ -769,19 +775,19 @@ func init() {
// /!\ also matches the empty string // /!\ also matches the empty string
// //
// Example matches: // Example matches:
//1979-05-27T07:32:00Z // 1979-05-27T07:32:00Z
//1979-05-27T00:32:00-07:00 // 1979-05-27T00:32:00-07:00
//1979-05-27T00:32:00.999999-07:00 // 1979-05-27T00:32:00.999999-07:00
//1979-05-27 07:32:00Z // 1979-05-27 07:32:00Z
//1979-05-27 00:32:00-07:00 // 1979-05-27 00:32:00-07:00
//1979-05-27 00:32:00.999999-07:00 // 1979-05-27 00:32:00.999999-07:00
//1979-05-27T07:32:00 // 1979-05-27T07:32:00
//1979-05-27T00:32:00.999999 // 1979-05-27T00:32:00.999999
//1979-05-27 07:32:00 // 1979-05-27 07:32:00
//1979-05-27 00:32:00.999999 // 1979-05-27 00:32:00.999999
//1979-05-27 // 1979-05-27
//07:32:00 // 07:32:00
//00:32:00.999999 // 00:32:00.999999
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})?)?`)
} }
+32 -3
View File
@@ -76,6 +76,7 @@ var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
var localDateType = reflect.TypeOf(LocalDate{}) var localDateType = reflect.TypeOf(LocalDate{})
var localTimeType = reflect.TypeOf(LocalTime{}) var localTimeType = reflect.TypeOf(LocalTime{})
var localDateTimeType = reflect.TypeOf(LocalDateTime{}) var localDateTimeType = reflect.TypeOf(LocalDateTime{})
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
// Check if the given marshal type maps to a Tree primitive // Check if the given marshal type maps to a Tree primitive
func isPrimitive(mtype reflect.Type) bool { func isPrimitive(mtype reflect.Type) bool {
@@ -436,6 +437,7 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
if tree, ok := val.(*Tree); ok && mtypef.Anonymous && !opts.nameFromTag && !e.promoteAnon { if tree, ok := val.(*Tree); ok && mtypef.Anonymous && !opts.nameFromTag && !e.promoteAnon {
e.appendTree(tval, tree) e.appendTree(tval, tree)
} else { } else {
val = e.wrapTomlValue(val, tval)
tval.SetPathWithOptions([]string{opts.name}, SetOptions{ tval.SetPathWithOptions([]string{opts.name}, SetOptions{
Comment: opts.comment, Comment: opts.comment,
Commented: opts.commented, Commented: opts.commented,
@@ -474,6 +476,7 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
val = e.wrapTomlValue(val, tval)
if e.quoteMapKeys { if e.quoteMapKeys {
keyStr, err := tomlValueStringRepresentation(key.String(), "", "", e.order, e.arraysOneElementPerLine) keyStr, err := tomlValueStringRepresentation(key.String(), "", "", e.order, e.arraysOneElementPerLine)
if err != nil { if err != nil {
@@ -516,13 +519,13 @@ func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (int
// Convert given marshal value to toml value // Convert given marshal value to toml value
func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) { func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
e.line++
if mtype.Kind() == reflect.Ptr { if mtype.Kind() == reflect.Ptr {
switch { switch {
case isCustomMarshaler(mtype): case isCustomMarshaler(mtype):
return callCustomMarshaler(mval) return callCustomMarshaler(mval)
case isTextMarshaler(mtype): case isTextMarshaler(mtype):
return callTextMarshaler(mval) b, err := callTextMarshaler(mval)
return string(b), err
default: default:
return e.valueToToml(mtype.Elem(), mval.Elem()) return e.valueToToml(mtype.Elem(), mval.Elem())
} }
@@ -534,7 +537,8 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
case isCustomMarshaler(mtype): case isCustomMarshaler(mtype):
return callCustomMarshaler(mval) return callCustomMarshaler(mval)
case isTextMarshaler(mtype): case isTextMarshaler(mtype):
return callTextMarshaler(mval) b, err := callTextMarshaler(mval)
return string(b), err
case isTree(mtype): case isTree(mtype):
return e.valueToTree(mtype, mval) return e.valueToTree(mtype, mval)
case isOtherSequence(mtype), isCustomMarshalerSequence(mtype), isTextMarshalerSequence(mtype): case isOtherSequence(mtype), isCustomMarshalerSequence(mtype), isTextMarshalerSequence(mtype):
@@ -577,6 +581,25 @@ func (e *Encoder) appendTree(t, o *Tree) error {
return nil return nil
} }
// Create a toml value with the current line number as the position line
func (e *Encoder) wrapTomlValue(val interface{}, parent *Tree) interface{} {
_, isTree := val.(*Tree)
_, isTreeS := val.([]*Tree)
if isTree || isTreeS {
return val
}
ret := &tomlValue{
value: val,
position: Position{
e.line,
parent.position.Col,
},
}
e.line++
return ret
}
// Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v. // Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v.
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for // Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
// sub-structs, and only definite types can be unmarshaled. // sub-structs, and only definite types can be unmarshaled.
@@ -681,6 +704,8 @@ func (d *Decoder) unmarshal(v interface{}) error {
switch elem.Kind() { switch elem.Kind() {
case reflect.Struct, reflect.Map: case reflect.Struct, reflect.Map:
case reflect.Interface:
elem = mapStringInterfaceType
default: default:
return errors.New("only a pointer to struct or map can be unmarshaled from TOML") return errors.New("only a pointer to struct or map can be unmarshaled from TOML")
} }
@@ -717,6 +742,10 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
if mvalPtr := reflect.New(mtype); isCustomUnmarshaler(mvalPtr.Type()) { if mvalPtr := reflect.New(mtype); isCustomUnmarshaler(mvalPtr.Type()) {
d.visitor.visitAll() d.visitor.visitAll()
if tval == nil {
return mvalPtr.Elem(), nil
}
if err := callCustomUnmarshaler(mvalPtr, tval.ToMap()); err != nil { if err := callCustomUnmarshaler(mvalPtr, tval.ToMap()); err != nil {
return reflect.ValueOf(nil), fmt.Errorf("unmarshal toml: %v", err) return reflect.ValueOf(nil), fmt.Errorf("unmarshal toml: %v", err)
} }
+155 -27
View File
@@ -14,60 +14,69 @@ import (
) )
type basicMarshalTestStruct struct { type basicMarshalTestStruct struct {
String string `toml:"Zstring"` String string `toml:"Zstring"`
StringList []string `toml:"Ystrlist"` StringList []string `toml:"Ystrlist"`
Sub basicMarshalTestSubStruct `toml:"Xsubdoc"` BasicMarshalTestSubAnonymousStruct
SubList []basicMarshalTestSubStruct `toml:"Wsublist"` Sub basicMarshalTestSubStruct `toml:"Xsubdoc"`
SubList []basicMarshalTestSubStruct `toml:"Wsublist"`
} }
type basicMarshalTestSubStruct struct { type basicMarshalTestSubStruct struct {
String2 string String2 string
} }
var basicTestData = basicMarshalTestStruct{ type BasicMarshalTestSubAnonymousStruct struct {
String: "Hello", String3 string
StringList: []string{"Howdy", "Hey There"},
Sub: basicMarshalTestSubStruct{"One"},
SubList: []basicMarshalTestSubStruct{{"Two"}, {"Three"}},
} }
var basicTestToml = []byte(`Ystrlist = ["Howdy", "Hey There"] var basicTestData = basicMarshalTestStruct{
Zstring = "Hello" String: "Hello",
StringList: []string{"Howdy", "Hey There"},
BasicMarshalTestSubAnonymousStruct: BasicMarshalTestSubAnonymousStruct{"One"},
Sub: basicMarshalTestSubStruct{"Two"},
SubList: []basicMarshalTestSubStruct{{"Three"}, {"Four"}},
}
[[Wsublist]] var basicTestToml = []byte(`String3 = "One"
String2 = "Two" Ystrlist = ["Howdy", "Hey There"]
Zstring = "Hello"
[[Wsublist]] [[Wsublist]]
String2 = "Three" String2 = "Three"
[[Wsublist]]
String2 = "Four"
[Xsubdoc] [Xsubdoc]
String2 = "One" String2 = "Two"
`) `)
var basicTestTomlCustomIndentation = []byte(`Ystrlist = ["Howdy", "Hey There"] var basicTestTomlCustomIndentation = []byte(`String3 = "One"
Ystrlist = ["Howdy", "Hey There"]
Zstring = "Hello" Zstring = "Hello"
[[Wsublist]]
String2 = "Two"
[[Wsublist]] [[Wsublist]]
String2 = "Three" String2 = "Three"
[[Wsublist]]
String2 = "Four"
[Xsubdoc] [Xsubdoc]
String2 = "One" String2 = "Two"
`) `)
var basicTestTomlOrdered = []byte(`Zstring = "Hello" var basicTestTomlOrdered = []byte(`Zstring = "Hello"
Ystrlist = ["Howdy", "Hey There"] Ystrlist = ["Howdy", "Hey There"]
String3 = "One"
[Xsubdoc] [Xsubdoc]
String2 = "One"
[[Wsublist]]
String2 = "Two" String2 = "Two"
[[Wsublist]] [[Wsublist]]
String2 = "Three" String2 = "Three"
[[Wsublist]]
String2 = "Four"
`) `)
var marshalTestToml = []byte(`title = "TOML Marshal Testing" var marshalTestToml = []byte(`title = "TOML Marshal Testing"
@@ -979,6 +988,40 @@ func TestCustomMarshaler(t *testing.T) {
} }
} }
type IntOrString string
func (x *IntOrString) MarshalTOML() ([]byte, error) {
s := *(*string)(x)
_, err := strconv.Atoi(s)
if err != nil {
return []byte(fmt.Sprintf(`"%s"`, s)), nil
}
return []byte(s), nil
}
func TestNestedCustomMarshaler(t *testing.T) {
num := IntOrString("100")
str := IntOrString("hello")
var parent = struct {
IntField *IntOrString `toml:"int"`
StringField *IntOrString `toml:"string"`
}{
&num,
&str,
}
result, err := Marshal(parent)
if err != nil {
t.Fatal(err)
}
expected := `int = 100
string = "hello"
`
if !bytes.Equal(result, []byte(expected)) {
t.Errorf("Bad nested text marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result)
}
}
type textMarshaler struct { type textMarshaler struct {
FirstName string FirstName string
LastName string LastName string
@@ -1079,7 +1122,7 @@ type customPointerMarshaler struct {
} }
func (m *customPointerMarshaler) MarshalTOML() ([]byte, error) { func (m *customPointerMarshaler) MarshalTOML() ([]byte, error) {
return []byte("hidden"), nil return []byte(`"hidden"`), nil
} }
type textPointerMarshaler struct { type textPointerMarshaler struct {
@@ -2151,10 +2194,7 @@ func TestUnmarshalBadDuration(t *testing.T) {
result := testBadDuration{} result := testBadDuration{}
err := NewDecoder(buf).Decode(&result) err := NewDecoder(buf).Decode(&result)
if err == nil { if err == nil {
t.Fatal() t.Fatal("expected bad duration error")
}
if err.Error() != "(1, 1): Can't convert 1z(string) to time.Duration. time: unknown unit z in duration 1z" {
t.Fatalf("unexpected error: %s", err)
} }
} }
@@ -3848,3 +3888,91 @@ func TestPreserveNotEmptyField(t *testing.T) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual) t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
} }
} }
// github issue 432
func TestUnmarshalEmptyInterface(t *testing.T) {
doc := []byte(`User = "pelletier"`)
var v interface{}
err := Unmarshal(doc, &v)
if err != nil {
t.Fatal(err)
}
x, ok := v.(map[string]interface{})
if !ok {
t.Fatal(err)
}
if x["User"] != "pelletier" {
t.Fatalf("expected User=pelletier, but got %v", x)
}
}
func TestUnmarshalEmptyInterfaceDeep(t *testing.T) {
doc := []byte(`
User = "pelletier"
Age = 99
[foo]
bar = 42
`)
var v interface{}
err := Unmarshal(doc, &v)
if err != nil {
t.Fatal(err)
}
x, ok := v.(map[string]interface{})
if !ok {
t.Fatal(err)
}
expected := map[string]interface{}{
"User": "pelletier",
"Age": 99,
"foo": map[string]interface{}{
"bar": 42,
},
}
reflect.DeepEqual(x, expected)
}
type Config struct {
Key string `toml:"key"`
Obj Custom `toml:"obj"`
}
type Custom struct {
v string
}
func (c *Custom) UnmarshalTOML(v interface{}) error {
c.v = "called"
return nil
}
func TestGithubIssue431(t *testing.T) {
doc := `key = "value"`
tree, err := LoadBytes([]byte(doc))
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
var c Config
if err := tree.Unmarshal(&c); err != nil {
t.Fatalf("unexpected error: %s", err)
}
if c.Key != "value" {
t.Errorf("expected c.Key='value', not '%s'", c.Key)
}
if c.Obj.v == "called" {
t.Errorf("UnmarshalTOML should not have been called")
}
}
+131 -1
View File
@@ -122,6 +122,89 @@ func (t *Tree) GetPath(keys []string) interface{} {
} }
} }
// GetArray returns the value at key in the Tree.
// It returns []string, []int64, etc type if key has homogeneous lists
// Key is a dot-separated path (e.g. a.b.c) without single/double quoted strings.
// Returns nil if the path does not exist in the tree.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetArray(key string) interface{} {
if key == "" {
return t
}
return t.GetArrayPath(strings.Split(key, "."))
}
// GetArrayPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetArrayPath(keys []string) interface{} {
if len(keys) == 0 {
return t
}
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
value, exists := subtree.values[intermediateKey]
if !exists {
return nil
}
switch node := value.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
return nil
}
subtree = node[len(node)-1]
default:
return nil // cannot navigate through other node types
}
}
// branch based on final node type
switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue:
switch n := node.value.(type) {
case []interface{}:
return getArray(n)
default:
return node.value
}
default:
return node
}
}
// if homogeneous array, then return slice type object over []interface{}
func getArray(n []interface{}) interface{} {
var s []string
var i64 []int64
var f64 []float64
var bl []bool
for _, value := range n {
switch v := value.(type) {
case string:
s = append(s, v)
case int64:
i64 = append(i64, v)
case float64:
f64 = append(f64, v)
case bool:
bl = append(bl, v)
default:
return n
}
}
if len(s) == len(n) {
return s
} else if len(i64) == len(n) {
return i64
} else if len(f64) == len(n) {
return f64
} else if len(bl) == len(n) {
return bl
}
return n
}
// GetPosition returns the position of the given key. // GetPosition returns the position of the given key.
func (t *Tree) GetPosition(key string) Position { func (t *Tree) GetPosition(key string) Position {
if key == "" { if key == "" {
@@ -130,6 +213,50 @@ func (t *Tree) GetPosition(key string) Position {
return t.GetPositionPath(strings.Split(key, ".")) return t.GetPositionPath(strings.Split(key, "."))
} }
// SetPositionPath sets the position of element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree position is set.
func (t *Tree) SetPositionPath(keys []string, pos Position) {
if len(keys) == 0 {
t.position = pos
return
}
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
value, exists := subtree.values[intermediateKey]
if !exists {
return
}
switch node := value.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
return
}
subtree = node[len(node)-1]
default:
return
}
}
// branch based on final node type
switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue:
node.position = pos
return
case *Tree:
node.position = pos
return
case []*Tree:
// go to most recent element
if len(node) == 0 {
return
}
node[len(node)-1].position = pos
return
}
}
// GetPositionPath returns the element in the tree indicated by 'keys'. // GetPositionPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned. // If keys is of length zero, the current tree is returned.
func (t *Tree) GetPositionPath(keys []string) Position { func (t *Tree) GetPositionPath(keys []string) Position {
@@ -212,7 +339,8 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
// go to most recent element // go to most recent element
if len(node) == 0 { if len(node) == 0 {
// create element if it does not exist // create element if it does not exist
subtree.values[intermediateKey] = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})) node = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}))
subtree.values[intermediateKey] = node
} }
subtree = node[len(node)-1] subtree = node[len(node)-1]
} }
@@ -232,6 +360,8 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
toInsert = value toInsert = value
case *tomlValue: case *tomlValue:
v.comment = opts.Comment v.comment = opts.Comment
v.commented = opts.Commented
v.multiline = opts.Multiline
toInsert = v toInsert = v
default: default:
toInsert = &tomlValue{value: value, toInsert = &tomlValue{value: value,
+81
View File
@@ -3,6 +3,7 @@
package toml package toml
import ( import (
"reflect"
"testing" "testing"
) )
@@ -39,6 +40,41 @@ func TestTomlGet(t *testing.T) {
} }
} }
func TestTomlGetArray(t *testing.T) {
tree, _ := Load(`
[test]
key = ["one", "two"]
key2 = [true, false, false]
key3 = [1.5,2.5]
`)
if tree.GetArray("") != tree {
t.Errorf("GetArray should return the tree itself when given an empty path")
}
expect := []string{"one", "two"}
actual := tree.GetArray("test.key").([]string)
if !reflect.DeepEqual(actual, expect) {
t.Errorf("GetArray should return the []string value")
}
expect2 := []bool{true, false, false}
actual2 := tree.GetArray("test.key2").([]bool)
if !reflect.DeepEqual(actual2, expect2) {
t.Errorf("GetArray should return the []bool value")
}
expect3 := []float64{1.5, 2.5}
actual3 := tree.GetArray("test.key3").([]float64)
if !reflect.DeepEqual(actual3, expect3) {
t.Errorf("GetArray should return the []float64 value")
}
if tree.GetArray(`\`) != nil {
t.Errorf("should return nil when the key is malformed")
}
}
func TestTomlGetDefault(t *testing.T) { func TestTomlGetDefault(t *testing.T) {
tree, _ := Load(` tree, _ := Load(`
[test] [test]
@@ -148,6 +184,51 @@ func TestTomlGetPath(t *testing.T) {
} }
} }
func TestTomlGetArrayPath(t *testing.T) {
for idx, item := range []struct {
Name string
Path []string
Make func() (tree *Tree, expected interface{})
}{
{
Name: "empty",
Path: []string{},
Make: func() (tree *Tree, expected interface{}) {
tree = newTree()
expected = tree
return
},
},
{
Name: "int64",
Path: []string{"a"},
Make: func() (tree *Tree, expected interface{}) {
var err error
tree, err = Load(`a = [1,2,3]`)
if err != nil {
panic(err)
}
expected = []int64{1, 2, 3}
return
},
},
} {
t.Run(item.Name, func(t *testing.T) {
tree, expected := item.Make()
result := tree.GetArrayPath(item.Path)
if !reflect.DeepEqual(result, expected) {
t.Errorf("GetArrayPath[%d] %v - expected %#v, got %#v instead.", idx, item.Path, 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.GetArrayPath([]string{"whatever"}) != nil {
t.Error("GetArrayPath should return nil when the key does not exist")
}
}
func TestTomlFromMap(t *testing.T) { func TestTomlFromMap(t *testing.T) {
simpleMap := map[string]interface{}{"hello": 42} simpleMap := map[string]interface{}{"hello": 42}
tree, err := TreeFromMap(simpleMap) tree, err := TreeFromMap(simpleMap)
+13
View File
@@ -57,6 +57,19 @@ func simpleValueCoercion(object interface{}) (interface{}, error) {
return float64(original), nil return float64(original), nil
case fmt.Stringer: case fmt.Stringer:
return original.String(), nil return original.String(), nil
case []interface{}:
value := reflect.ValueOf(original)
length := value.Len()
arrayValue := reflect.MakeSlice(value.Type(), 0, length)
for i := 0; i < length; i++ {
val := value.Index(i).Interface()
simpleValue, err := simpleValueCoercion(val)
if err != nil {
return nil, err
}
arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue))
}
return arrayValue.Interface(), nil
default: default:
return nil, fmt.Errorf("cannot convert type %T to Tree", object) return nil, fmt.Errorf("cannot convert type %T to Tree", object)
} }
+117
View File
@@ -1,6 +1,7 @@
package toml package toml
import ( import (
"reflect"
"strconv" "strconv"
"testing" "testing"
"time" "time"
@@ -124,3 +125,119 @@ func TestRoundTripArrayOfTables(t *testing.T) {
t.Errorf("want:\n%s\ngot:\n%s", want, got) t.Errorf("want:\n%s\ngot:\n%s", want, got)
} }
} }
func TestTomlSliceOfSlice(t *testing.T) {
tree, err := Load(` hosts=[["10.1.0.107:9092","10.1.0.107:9093", "192.168.0.40:9094"] ] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]string
}
var actual Struct
tree.Unmarshal(&actual)
expected := Struct{Hosts: [][]string{[]string{"10.1.0.107:9092", "10.1.0.107:9093", "192.168.0.40:9094"}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceOfSlice(t *testing.T) {
tree, err := Load(` hosts=[[["10.1.0.107:9092","10.1.0.107:9093", "192.168.0.40:9094"] ]] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][][]string
}
var actual Struct
tree.Unmarshal(&actual)
expected := Struct{Hosts: [][][]string{[][]string{[]string{"10.1.0.107:9092", "10.1.0.107:9093", "192.168.0.40:9094"}}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceInt(t *testing.T) {
tree, err := Load(` hosts=[[1,2,3],[4,5,6] ] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]int
}
var actual Struct
err = tree.Unmarshal(&actual)
if err != nil {
t.Error("should not error", err)
}
expected := Struct{Hosts: [][]int{[]int{1, 2, 3}, []int{4, 5, 6}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceInt64(t *testing.T) {
tree, err := Load(` hosts=[[1,2,3],[4,5,6] ] `)
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]int64
}
var actual Struct
err = tree.Unmarshal(&actual)
if err != nil {
t.Error("should not error", err)
}
expected := Struct{Hosts: [][]int64{[]int64{1, 2, 3}, []int64{4, 5, 6}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceInt64FromMap(t *testing.T) {
tree, err := TreeFromMap(map[string]interface{}{"hosts": [][]interface{}{[]interface{}{int32(1), int8(2), 3}}})
if err != nil {
t.Error("should not error", err)
}
type Struct struct {
Hosts [][]int64
}
var actual Struct
err = tree.Unmarshal(&actual)
if err != nil {
t.Error("should not error", err)
}
expected := Struct{Hosts: [][]int64{[]int64{1, 2, 3}}}
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual)
}
}
func TestTomlSliceOfSliceError(t *testing.T) { // make Codecov happy
_, err := TreeFromMap(map[string]interface{}{"hosts": [][]interface{}{[]interface{}{1, 2, []struct{}{}}}})
expected := "cannot convert type []struct {} to Tree"
if err.Error() != expected {
t.Fatalf("unexpected error: %s", err)
}
}
+1 -1
View File
@@ -163,7 +163,7 @@ func tomlValueStringRepresentation(v interface{}, commented string, indent strin
return "\"" + encodeTomlString(value) + "\"", nil return "\"" + encodeTomlString(value) + "\"", nil
case []byte: case []byte:
b, _ := v.([]byte) b, _ := v.([]byte)
return tomlValueStringRepresentation(string(b), commented, indent, ord, arraysOneElementPerLine) return string(b), nil
case bool: case bool:
if value { if value {
return "true", nil return "true", nil