Compare commits

...

25 Commits

Author SHA1 Message Date
Allen 8e8d2a6aad Support unmarshal into toml.Tree (#347)
Fixes #333
2020-04-03 07:10:45 -04:00
Thomas Pelletier 3f7178ffd6 CI computes manifest for built binaries (#346) 2020-03-30 11:59:45 -04:00
Thomas Pelletier 9fd5922321 Empty commit to trigger release 2020-03-30 10:59:59 -04:00
Thomas Pelletier 610cf85ed6 CI Build Binaries (#345) 2020-03-30 10:51:26 -04:00
Thomas Pelletier 99f8a2a010 Fix codecov on pull requests (#344) 2020-03-25 13:21:25 -04:00
Thomas Pelletier 556d384d4c Bump go version to 1.14 and 1.13 (#343) 2020-03-25 12:52:59 -04:00
Allen eb7280e4a7 Add interface{} support (#341) 2020-03-25 11:12:18 -04:00
Allen 7ee1118b4b Fix unmarshaling of nested structs (#340)
Fixes #339
2020-03-23 11:23:21 -04:00
Allen a12e102214 Fix multiline + non-primitive commenting (#336)
Fixes #216
2020-03-16 22:51:47 -04:00
jinleiw ad60b7e437 Add support for nested interface{} unmarshal (#335)
Co-authored-by: jlwang <jlwang@sysnew.com>

Fixes #331
2020-03-16 10:38:53 -04:00
Allen 3503483c73 Fix unexpected token type in inline table (#334)
Fixes #321
2020-03-10 13:39:48 -04:00
Thomas Pelletier d2d17bccec Update CI vm images versions (#328)
Received an email from Microsoft stating that those versions will be
discontinued. Switching to use -latest for all of them to not be
bothered with that in the future.
2020-01-24 12:32:53 -05:00
dependabot-preview[bot] 76a94674c9 Bump gopkg.in/yaml.v2 from 2.2.7 to 2.2.8 (#327)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.7 to 2.2.8.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.7...v2.2.8)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-23 11:32:53 -05:00
Nicolas Bedos 80f8b7660b Support default values for inner structs (#326) 2020-01-13 09:39:27 -05:00
dependabot-preview[bot] 6f6ca41621 Bump gopkg.in/yaml.v2 from 2.2.5 to 2.2.7 (#324)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.5 to 2.2.7.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.5...v2.2.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-20 21:21:26 -05:00
dependabot-preview[bot] c4efb7477c Bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.5 (#322) 2019-11-06 15:49:39 +00:00
Thomas Pelletier 903d9455db TOML 0.5.0 (#320)
go-toml now officially supports all TOML 0.5.0 features. If anything
does not work according to the spec, please file a bug!
2019-10-25 14:53:56 -04:00
Thomas Pelletier a89a075e1b Test for accidental newlines (#319) 2019-10-25 14:44:53 -04:00
Thomas Pelletier 5e74bb91ea Local time support (#318) 2019-10-25 14:28:32 -04:00
Thomas Pelletier 3a4d7af89e Local DateTime support (#317) 2019-10-25 14:07:46 -04:00
Thomas Pelletier 8a362ad712 Short-date support (#298) 2019-10-25 13:21:44 -04:00
Thomas Pelletier 5edf9acd3e Add testing for encodeMultilineTomlString (#313) 2019-10-21 14:31:28 -04:00
Thomas Pelletier e95df67ba3 Delete token.Int() (#312)
Not used anywhere.
2019-10-21 14:01:10 -04:00
Jonathan Lloyd bef0f57967 Fix key parsing in line tables (#311)
A bug was reported that indicated that inline tables did not fully support bare keys:
$ echo 'foo = { -bar => "buz"}' | ./tomljson
(1, 9): unexpected token type in inline table: Error

$ echo 'foo = { "whatever" = "buz"}' | ./tomljson
(1, 10): unexpected token type in inline table: String

echo 'foo = { _no = "buz"}' | ./tomljson
(1, 9): unexpected token type in inline table: Error

This change makes a couple of tweaks to to allow for all key variants in inline tables

Fixes: #282
2019-10-20 20:36:14 -04:00
Marcin Białoń e87c92d4f4 Marshal arrays (#310)
Fixes #285
2019-10-09 12:33:56 -04:00
18 changed files with 2439 additions and 149 deletions
+29
View File
@@ -0,0 +1,29 @@
export CGO_ENABLED=0
go := go
go.goos ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f1)
go.goarch ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f2)
out.tools := tomll tomljson jsontoml
out.dist := $(out.tools:=_$(go.goos)_$(go.goarch).tar.xz)
sources := $(wildcard **/*.go)
.PHONY:
tools: $(out.tools)
$(out.tools): $(sources)
GOOS=$(go.goos) GOARCH=$(go.goarch) $(go) build ./cmd/$@
.PHONY:
dist: $(out.dist)
$(out.dist):%_$(go.goos)_$(go.goarch).tar.xz: %
if [ "$(go.goos)" = "windows" ]; then \
tar -cJf $@ $^.exe; \
else \
tar -cJf $@ $^; \
fi
.PHONY:
clean:
rm -rf $(out.tools) $(out.dist)
+1 -1
View File
@@ -3,7 +3,7 @@
Go library for the [TOML](https://github.com/mojombo/toml) format. Go library for the [TOML](https://github.com/mojombo/toml) format.
This library supports TOML version This library supports TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md) [v0.5.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md)
[![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)
[![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE) [![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE)
+85 -22
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.13" displayName: "Install Go 1.14"
inputs: inputs:
version: "1.13" version: "1.14"
- 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.13" displayName: "Install Go 1.14"
inputs: inputs:
version: "1.13" version: "1.14"
- 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.13" displayName: "Install Go 1.14"
inputs: inputs:
version: "1.13" version: "1.14"
- task: Go@0 - task: Go@0
displayName: "Generate coverage" displayName: "Generate coverage"
inputs: inputs:
@@ -62,16 +62,18 @@ stages:
- task: Bash@3 - task: Bash@3
inputs: inputs:
targetType: 'inline' targetType: 'inline'
script: 'bash <(curl -s https://codecov.io/bash) -t $(CODECOV_TOKEN)' script: 'bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}'
env:
CODECOV_TOKEN: $(CODECOV_TOKEN)
- job: benchmark - job: benchmark
displayName: "benchmark" displayName: "benchmark"
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.13" displayName: "Install Go 1.14"
inputs: inputs:
version: "1.13" version: "1.14"
- 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:
@@ -84,9 +86,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.13" displayName: "Install Go 1.14"
inputs: inputs:
version: "1.13" version: "1.14"
- 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
@@ -100,24 +102,24 @@ stages:
displayName: "unit tests" displayName: "unit tests"
strategy: strategy:
matrix: matrix:
linux 1.14:
goVersion: '1.14'
imageName: 'ubuntu-latest'
mac 1.14:
goVersion: '1.14'
imageName: 'macOS-latest'
windows 1.14:
goVersion: '1.14'
imageName: 'windows-latest'
linux 1.13: linux 1.13:
goVersion: '1.13' goVersion: '1.13'
imageName: 'ubuntu-latest' imageName: 'ubuntu-latest'
mac 1.13: mac 1.13:
goVersion: '1.13' goVersion: '1.13'
imageName: 'macos-10.13' imageName: 'macOS-latest'
windows 1.13: windows 1.13:
goVersion: '1.13' goVersion: '1.13'
imageName: 'vs2017-win2016' imageName: 'windows-latest'
linux 1.12:
goVersion: '1.12'
imageName: 'ubuntu-latest'
mac 1.12:
goVersion: '1.12'
imageName: 'macos-10.13'
windows 1.12:
goVersion: '1.12'
imageName: 'vs2017-win2016'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
@@ -130,6 +132,67 @@ stages:
inputs: inputs:
command: 'test' command: 'test'
arguments: './...' arguments: './...'
- stage: build_binaries
displayName: "Build binaries"
dependsOn: run_checks
jobs:
- job: build_binary
displayName: "Build binary"
strategy:
matrix:
linux_amd64:
GOOS: linux
GOARCH: amd64
darwin_amd64:
GOOS: darwin
GOARCH: amd64
windows_amd64:
GOOS: windows
GOARCH: amd64
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go"
inputs:
version: 1.14
- task: Bash@3
inputs:
targetType: inline
script: "make dist"
env:
go.goos: $(GOOS)
go.goarch: $(GOARCH)
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: '*.tar.xz'
TargetFolder: '$(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: binaries
- stage: build_binaries_manifest
displayName: "Build binaries manifest"
dependsOn: build_binaries
jobs:
- job: build_manifest
displayName: "Build binaries manifest"
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'binaries'
downloadPath: '$(Build.SourcesDirectory)'
- task: Bash@3
inputs:
targetType: inline
script: "cd binaries && sha256sum --binary *.tar.xz | tee $(Build.ArtifactStagingDirectory)/sha256sums.txt"
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: manifest
- stage: build_docker_image - stage: build_docker_image
displayName: "Build Docker image" displayName: "Build Docker image"
+1 -1
View File
@@ -1,7 +1,7 @@
// Package toml is a TOML parser and manipulation library. // Package toml is a TOML parser and manipulation library.
// //
// This version supports the specification as described in // This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md // https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
// //
// Marshaling // Marshaling
// //
+1 -1
View File
@@ -5,5 +5,5 @@ go 1.12
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
gopkg.in/yaml.v2 v2.2.4 gopkg.in/yaml.v2 v2.2.8
) )
+6
View File
@@ -9,3 +9,9 @@ gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+33 -5
View File
@@ -223,9 +223,12 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
} }
possibleDate := l.peekString(35) possibleDate := l.peekString(35)
dateMatch := dateRegexp.FindString(possibleDate) dateSubmatches := dateRegexp.FindStringSubmatch(possibleDate)
if dateMatch != "" { if dateSubmatches != nil && dateSubmatches[0] != "" {
l.fastForward(len(dateMatch)) l.fastForward(len(dateSubmatches[0]))
if dateSubmatches[2] == "" { // no timezone information => local date
return l.lexLocalDate
}
return l.lexDate return l.lexDate
} }
@@ -247,7 +250,7 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
l.next() l.next()
l.emit(tokenLeftCurlyBrace) l.emit(tokenLeftCurlyBrace)
return l.lexRvalue return l.lexVoid
} }
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
@@ -261,6 +264,11 @@ func (l *tomlLexer) lexDate() tomlLexStateFn {
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexLocalDate() tomlLexStateFn {
l.emit(tokenLocalDate)
return l.lexRvalue
}
func (l *tomlLexer) lexTrue() tomlLexStateFn { func (l *tomlLexer) lexTrue() tomlLexStateFn {
l.fastForward(4) l.fastForward(4)
l.emit(tokenTrue) l.emit(tokenTrue)
@@ -733,7 +741,27 @@ func (l *tomlLexer) run() {
} }
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})`) // Regexp for all date/time formats supported by TOML.
// Group 1: nano precision
// Group 2: timezone
//
// /!\ also matches the empty string
//
// Example matches:
//1979-05-27T07:32:00Z
//1979-05-27T00:32:00-07:00
//1979-05-27T00:32:00.999999-07:00
//1979-05-27 07:32:00Z
//1979-05-27 00:32:00-07:00
//1979-05-27 00:32:00.999999-07:00
//1979-05-27T07:32:00
//1979-05-27T00:32:00.999999
//1979-05-27 07:32:00
//1979-05-27 00:32:00.999999
//1979-05-27
//07:32:00
//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})?)?`)
} }
// Entry point // Entry point
+71 -7
View File
@@ -290,14 +290,26 @@ func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
} }
func TestDateRegexp(t *testing.T) { func TestDateRegexp(t *testing.T) {
if dateRegexp.FindString("1979-05-27T07:32:00Z") == "" { cases := map[string]string{
t.Error("basic lexing") "basic": "1979-05-27T07:32:00Z",
"offset": "1979-05-27T00:32:00-07:00",
"nano precision": "1979-05-27T00:32:00.999999-07:00",
"basic-no-T": "1979-05-27 07:32:00Z",
"offset-no-T": "1979-05-27 00:32:00-07:00",
"nano precision-no-T": "1979-05-27 00:32:00.999999-07:00",
"no-tz": "1979-05-27T07:32:00",
"no-tz-nano": "1979-05-27T00:32:00.999999",
"no-tz-no-t": "1979-05-27 07:32:00",
"no-tz-no-t-nano": "1979-05-27 00:32:00.999999",
"date-no-tz": "1979-05-27",
"time-no-tz": "07:32:00",
"time-no-tz-nano": "00:32:00.999999",
} }
if dateRegexp.FindString("1979-05-27T00:32:00-07:00") == "" {
t.Error("offset lexing") for name, value := range cases {
} if dateRegexp.FindString(value) == "" {
if dateRegexp.FindString("1979-05-27T00:32:00.999999-07:00") == "" { t.Error("failed date regexp test", name)
t.Error("nano precision lexing") }
} }
if dateRegexp.FindString("1979-05-27 07:32:00Z") == "" { if dateRegexp.FindString("1979-05-27 07:32:00Z") == "" {
t.Error("space delimiter lexing") t.Error("space delimiter lexing")
@@ -735,6 +747,58 @@ func TestLexUnknownRvalue(t *testing.T) {
}) })
} }
func TestLexInlineTableBareKey(t *testing.T) {
testFlow(t, `foo = { bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "bar"},
{Position{1, 13}, tokenEqual, "="},
{Position{1, 16}, tokenString, "baz"},
{Position{1, 21}, tokenRightCurlyBrace, "}"},
{Position{1, 22}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyDash(t *testing.T) {
testFlow(t, `foo = { -bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "-bar"},
{Position{1, 14}, tokenEqual, "="},
{Position{1, 17}, tokenString, "baz"},
{Position{1, 22}, tokenRightCurlyBrace, "}"},
{Position{1, 23}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyUnderscore(t *testing.T) {
testFlow(t, `foo = { _bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "_bar"},
{Position{1, 14}, tokenEqual, "="},
{Position{1, 17}, tokenString, "baz"},
{Position{1, 22}, tokenRightCurlyBrace, "}"},
{Position{1, 23}, tokenEOF, ""},
})
}
func TestLexInlineTableQuotedKey(t *testing.T) {
testFlow(t, `foo = { "bar" = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "\"bar\""},
{Position{1, 15}, tokenEqual, "="},
{Position{1, 18}, tokenString, "baz"},
{Position{1, 23}, tokenRightCurlyBrace, "}"},
{Position{1, 24}, tokenEOF, ""},
})
}
func BenchmarkLexer(b *testing.B) { func BenchmarkLexer(b *testing.B) {
sample := `title = "Hugo: A Fast and Flexible Website Generator" sample := `title = "Hugo: A Fast and Flexible Website Generator"
baseurl = "http://gohugo.io/" baseurl = "http://gohugo.io/"
+281
View File
@@ -0,0 +1,281 @@
// Implementation of TOML's local date/time.
// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go
// to avoid pulling all the Google dependencies.
//
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package civil implements types for civil time, a time-zone-independent
// representation of time that follows the rules of the proleptic
// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second
// minutes.
//
// Because they lack location information, these types do not represent unique
// moments or intervals of time. Use time.Time for that purpose.
package toml
import (
"fmt"
"time"
)
// A LocalDate represents a date (year, month, day).
//
// This type does not include location information, and therefore does not
// describe a unique 24-hour timespan.
type LocalDate struct {
Year int // Year (e.g., 2014).
Month time.Month // Month of the year (January = 1, ...).
Day int // Day of the month, starting at 1.
}
// LocalDateOf returns the LocalDate in which a time occurs in that time's location.
func LocalDateOf(t time.Time) LocalDate {
var d LocalDate
d.Year, d.Month, d.Day = t.Date()
return d
}
// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents.
func ParseLocalDate(s string) (LocalDate, error) {
t, err := time.Parse("2006-01-02", s)
if err != nil {
return LocalDate{}, err
}
return LocalDateOf(t), nil
}
// String returns the date in RFC3339 full-date format.
func (d LocalDate) String() string {
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
}
// IsValid reports whether the date is valid.
func (d LocalDate) IsValid() bool {
return LocalDateOf(d.In(time.UTC)) == d
}
// In returns the time corresponding to time 00:00:00 of the date in the location.
//
// In is always consistent with time.LocalDate, even when time.LocalDate returns a time
// on a different day. For example, if loc is America/Indiana/Vincennes, then both
// time.LocalDate(1955, time.May, 1, 0, 0, 0, 0, loc)
// and
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}.In(loc)
// return 23:00:00 on April 30, 1955.
//
// In panics if loc is nil.
func (d LocalDate) In(loc *time.Location) time.Time {
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc)
}
// AddDays returns the date that is n days in the future.
// n can also be negative to go into the past.
func (d LocalDate) AddDays(n int) LocalDate {
return LocalDateOf(d.In(time.UTC).AddDate(0, 0, n))
}
// DaysSince returns the signed number of days between the date and s, not including the end day.
// This is the inverse operation to AddDays.
func (d LocalDate) DaysSince(s LocalDate) (days int) {
// We convert to Unix time so we do not have to worry about leap seconds:
// Unix time increases by exactly 86400 seconds per day.
deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix()
return int(deltaUnix / 86400)
}
// Before reports whether d1 occurs before d2.
func (d1 LocalDate) Before(d2 LocalDate) bool {
if d1.Year != d2.Year {
return d1.Year < d2.Year
}
if d1.Month != d2.Month {
return d1.Month < d2.Month
}
return d1.Day < d2.Day
}
// After reports whether d1 occurs after d2.
func (d1 LocalDate) After(d2 LocalDate) bool {
return d2.Before(d1)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of d.String().
func (d LocalDate) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The date is expected to be a string in a format accepted by ParseLocalDate.
func (d *LocalDate) UnmarshalText(data []byte) error {
var err error
*d, err = ParseLocalDate(string(data))
return err
}
// A LocalTime represents a time with nanosecond precision.
//
// This type does not include location information, and therefore does not
// describe a unique moment in time.
//
// This type exists to represent the TIME type in storage-based APIs like BigQuery.
// Most operations on Times are unlikely to be meaningful. Prefer the LocalDateTime type.
type LocalTime struct {
Hour int // The hour of the day in 24-hour format; range [0-23]
Minute int // The minute of the hour; range [0-59]
Second int // The second of the minute; range [0-59]
Nanosecond int // The nanosecond of the second; range [0-999999999]
}
// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs
// in that time's location. It ignores the date.
func LocalTimeOf(t time.Time) LocalTime {
var tm LocalTime
tm.Hour, tm.Minute, tm.Second = t.Clock()
tm.Nanosecond = t.Nanosecond()
return tm
}
// ParseLocalTime parses a string and returns the time value it represents.
// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After
// the HH:MM:SS part of the string, an optional fractional part may appear,
// consisting of a decimal point followed by one to nine decimal digits.
// (RFC3339 admits only one digit after the decimal point).
func ParseLocalTime(s string) (LocalTime, error) {
t, err := time.Parse("15:04:05.999999999", s)
if err != nil {
return LocalTime{}, err
}
return LocalTimeOf(t), nil
}
// String returns the date in the format described in ParseLocalTime. If Nanoseconds
// is zero, no fractional part will be generated. Otherwise, the result will
// end with a fractional part consisting of a decimal point and nine digits.
func (t LocalTime) String() string {
s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second)
if t.Nanosecond == 0 {
return s
}
return s + fmt.Sprintf(".%09d", t.Nanosecond)
}
// IsValid reports whether the time is valid.
func (t LocalTime) IsValid() bool {
// Construct a non-zero time.
tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC)
return LocalTimeOf(tm) == t
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of t.String().
func (t LocalTime) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The time is expected to be a string in a format accepted by ParseLocalTime.
func (t *LocalTime) UnmarshalText(data []byte) error {
var err error
*t, err = ParseLocalTime(string(data))
return err
}
// A LocalDateTime represents a date and time.
//
// This type does not include location information, and therefore does not
// describe a unique moment in time.
type LocalDateTime struct {
Date LocalDate
Time LocalTime
}
// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub.
// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location.
func LocalDateTimeOf(t time.Time) LocalDateTime {
return LocalDateTime{
Date: LocalDateOf(t),
Time: LocalTimeOf(t),
}
}
// ParseLocalDateTime parses a string and returns the LocalDateTime it represents.
// ParseLocalDateTime accepts a variant of the RFC3339 date-time format that omits
// the time offset but includes an optional fractional time, as described in
// ParseLocalTime. Informally, the accepted format is
// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF]
// where the 'T' may be a lower-case 't'.
func ParseLocalDateTime(s string) (LocalDateTime, error) {
t, err := time.Parse("2006-01-02T15:04:05.999999999", s)
if err != nil {
t, err = time.Parse("2006-01-02t15:04:05.999999999", s)
if err != nil {
return LocalDateTime{}, err
}
}
return LocalDateTimeOf(t), nil
}
// String returns the date in the format described in ParseLocalDate.
func (dt LocalDateTime) String() string {
return dt.Date.String() + "T" + dt.Time.String()
}
// IsValid reports whether the datetime is valid.
func (dt LocalDateTime) IsValid() bool {
return dt.Date.IsValid() && dt.Time.IsValid()
}
// In returns the time corresponding to the LocalDateTime in the given location.
//
// If the time is missing or ambigous at the location, In returns the same
// result as time.LocalDate. For example, if loc is America/Indiana/Vincennes, then
// both
// time.LocalDate(1955, time.May, 1, 0, 30, 0, 0, loc)
// and
// civil.LocalDateTime{
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}},
// civil.LocalTime{Minute: 30}}.In(loc)
// return 23:30:00 on April 30, 1955.
//
// In panics if loc is nil.
func (dt LocalDateTime) In(loc *time.Location) time.Time {
return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc)
}
// Before reports whether dt1 occurs before dt2.
func (dt1 LocalDateTime) Before(dt2 LocalDateTime) bool {
return dt1.In(time.UTC).Before(dt2.In(time.UTC))
}
// After reports whether dt1 occurs after dt2.
func (dt1 LocalDateTime) After(dt2 LocalDateTime) bool {
return dt2.Before(dt1)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of dt.String().
func (dt LocalDateTime) MarshalText() ([]byte, error) {
return []byte(dt.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The datetime is expected to be a string in a format accepted by ParseLocalDateTime
func (dt *LocalDateTime) UnmarshalText(data []byte) error {
var err error
*dt, err = ParseLocalDateTime(string(data))
return err
}
+446
View File
@@ -0,0 +1,446 @@
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package toml
import (
"encoding/json"
"reflect"
"testing"
"time"
)
func cmpEqual(x, y interface{}) bool {
return reflect.DeepEqual(x, y)
}
func TestDates(t *testing.T) {
for _, test := range []struct {
date LocalDate
loc *time.Location
wantStr string
wantTime time.Time
}{
{
date: LocalDate{2014, 7, 29},
loc: time.Local,
wantStr: "2014-07-29",
wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local),
},
{
date: LocalDateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)),
loc: time.UTC,
wantStr: "2014-08-20",
wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC),
},
{
date: LocalDateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)),
loc: time.UTC,
wantStr: "0999-01-26",
wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC),
},
} {
if got := test.date.String(); got != test.wantStr {
t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr)
}
if got := test.date.In(test.loc); !got.Equal(test.wantTime) {
t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime)
}
}
}
func TestDateIsValid(t *testing.T) {
for _, test := range []struct {
date LocalDate
want bool
}{
{LocalDate{2014, 7, 29}, true},
{LocalDate{2000, 2, 29}, true},
{LocalDate{10000, 12, 31}, true},
{LocalDate{1, 1, 1}, true},
{LocalDate{0, 1, 1}, true}, // year zero is OK
{LocalDate{-1, 1, 1}, true}, // negative year is OK
{LocalDate{1, 0, 1}, false},
{LocalDate{1, 1, 0}, false},
{LocalDate{2016, 1, 32}, false},
{LocalDate{2016, 13, 1}, false},
{LocalDate{1, -1, 1}, false},
{LocalDate{1, 1, -1}, false},
} {
got := test.date.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.date, got, test.want)
}
}
}
func TestParseDate(t *testing.T) {
for _, test := range []struct {
str string
want LocalDate // if empty, expect an error
}{
{"2016-01-02", LocalDate{2016, 1, 2}},
{"2016-12-31", LocalDate{2016, 12, 31}},
{"0003-02-04", LocalDate{3, 2, 4}},
{"999-01-26", LocalDate{}},
{"", LocalDate{}},
{"2016-01-02x", LocalDate{}},
} {
got, err := ParseLocalDate(test.str)
if got != test.want {
t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want)
}
if err != nil && test.want != (LocalDate{}) {
t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str)
}
}
}
func TestDateArithmetic(t *testing.T) {
for _, test := range []struct {
desc string
start LocalDate
end LocalDate
days int
}{
{
desc: "zero days noop",
start: LocalDate{2014, 5, 9},
end: LocalDate{2014, 5, 9},
days: 0,
},
{
desc: "crossing a year boundary",
start: LocalDate{2014, 12, 31},
end: LocalDate{2015, 1, 1},
days: 1,
},
{
desc: "negative number of days",
start: LocalDate{2015, 1, 1},
end: LocalDate{2014, 12, 31},
days: -1,
},
{
desc: "full leap year",
start: LocalDate{2004, 1, 1},
end: LocalDate{2005, 1, 1},
days: 366,
},
{
desc: "full non-leap year",
start: LocalDate{2001, 1, 1},
end: LocalDate{2002, 1, 1},
days: 365,
},
{
desc: "crossing a leap second",
start: LocalDate{1972, 6, 30},
end: LocalDate{1972, 7, 1},
days: 1,
},
{
desc: "dates before the unix epoch",
start: LocalDate{101, 1, 1},
end: LocalDate{102, 1, 1},
days: 365,
},
} {
if got := test.start.AddDays(test.days); got != test.end {
t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end)
}
if got := test.end.DaysSince(test.start); got != test.days {
t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days)
}
}
}
func TestDateBefore(t *testing.T) {
for _, test := range []struct {
d1, d2 LocalDate
want bool
}{
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, true},
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, true},
{LocalDate{2016, 1, 30}, LocalDate{2016, 12, 31}, true},
} {
if got := test.d1.Before(test.d2); got != test.want {
t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want)
}
}
}
func TestDateAfter(t *testing.T) {
for _, test := range []struct {
d1, d2 LocalDate
want bool
}{
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, false},
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, false},
} {
if got := test.d1.After(test.d2); got != test.want {
t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want)
}
}
}
func TestTimeToString(t *testing.T) {
for _, test := range []struct {
str string
time LocalTime
roundTrip bool // ParseLocalTime(str).String() == str?
}{
{"13:26:33", LocalTime{13, 26, 33, 0}, true},
{"01:02:03.000023456", LocalTime{1, 2, 3, 23456}, true},
{"00:00:00.000000001", LocalTime{0, 0, 0, 1}, true},
{"13:26:03.1", LocalTime{13, 26, 3, 100000000}, false},
{"13:26:33.0000003", LocalTime{13, 26, 33, 300}, false},
} {
gotTime, err := ParseLocalTime(test.str)
if err != nil {
t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err)
continue
}
if gotTime != test.time {
t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time)
}
if test.roundTrip {
gotStr := test.time.String()
if gotStr != test.str {
t.Errorf("%#v.String() = %q, want %q", test.time, gotStr, test.str)
}
}
}
}
func TestTimeOf(t *testing.T) {
for _, test := range []struct {
time time.Time
want LocalTime
}{
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), LocalTime{15, 8, 43, 1}},
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), LocalTime{0, 0, 0, 0}},
} {
if got := LocalTimeOf(test.time); got != test.want {
t.Errorf("LocalTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
}
}
}
func TestTimeIsValid(t *testing.T) {
for _, test := range []struct {
time LocalTime
want bool
}{
{LocalTime{0, 0, 0, 0}, true},
{LocalTime{23, 0, 0, 0}, true},
{LocalTime{23, 59, 59, 999999999}, true},
{LocalTime{24, 59, 59, 999999999}, false},
{LocalTime{23, 60, 59, 999999999}, false},
{LocalTime{23, 59, 60, 999999999}, false},
{LocalTime{23, 59, 59, 1000000000}, false},
{LocalTime{-1, 0, 0, 0}, false},
{LocalTime{0, -1, 0, 0}, false},
{LocalTime{0, 0, -1, 0}, false},
{LocalTime{0, 0, 0, -1}, false},
} {
got := test.time.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.time, got, test.want)
}
}
}
func TestDateTimeToString(t *testing.T) {
for _, test := range []struct {
str string
dateTime LocalDateTime
roundTrip bool // ParseLocalDateTime(str).String() == str?
}{
{"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, true},
{"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 600}}, true},
{"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, false},
} {
gotDateTime, err := ParseLocalDateTime(test.str)
if err != nil {
t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err)
continue
}
if gotDateTime != test.dateTime {
t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime)
}
if test.roundTrip {
gotStr := test.dateTime.String()
if gotStr != test.str {
t.Errorf("%#v.String() = %q, want %q", test.dateTime, gotStr, test.str)
}
}
}
}
func TestParseDateTimeErrors(t *testing.T) {
for _, str := range []string{
"",
"2016-03-22", // just a date
"13:26:33", // just a time
"2016-03-22 13:26:33", // wrong separating character
"2016-03-22T13:26:33x", // extra at end
} {
if _, err := ParseLocalDateTime(str); err == nil {
t.Errorf("ParseLocalDateTime(%q) succeeded, want error", str)
}
}
}
func TestDateTimeOf(t *testing.T) {
for _, test := range []struct {
time time.Time
want LocalDateTime
}{
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local),
LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}}},
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}}},
} {
if got := LocalDateTimeOf(test.time); got != test.want {
t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
}
}
}
func TestDateTimeIsValid(t *testing.T) {
// No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid.
for _, test := range []struct {
dt LocalDateTime
want bool
}{
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{0, 0, 0, 0}}, true},
{LocalDateTime{LocalDate{2016, -3, 20}, LocalTime{0, 0, 0, 0}}, false},
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{24, 0, 0, 0}}, false},
} {
got := test.dt.IsValid()
if got != test.want {
t.Errorf("%#v: got %t, want %t", test.dt, got, test.want)
}
}
}
func TestDateTimeIn(t *testing.T) {
dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}}
got := dt.In(time.UTC)
want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC)
if !got.Equal(want) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestDateTimeBefore(t *testing.T) {
d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct {
dt1, dt2 LocalDateTime
want bool
}{
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, true},
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, true},
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, false},
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
} {
if got := test.dt1.Before(test.dt2); got != test.want {
t.Errorf("%v.Before(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
}
}
}
func TestDateTimeAfter(t *testing.T) {
d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct {
dt1, dt2 LocalDateTime
want bool
}{
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, false},
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, false},
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, true},
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
} {
if got := test.dt1.After(test.dt2); got != test.want {
t.Errorf("%v.After(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
}
}
}
func TestMarshalJSON(t *testing.T) {
for _, test := range []struct {
value interface{}
want string
}{
{LocalDate{1987, 4, 15}, `"1987-04-15"`},
{LocalTime{18, 54, 2, 0}, `"18:54:02"`},
{LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}, `"1987-04-15T18:54:02"`},
} {
bgot, err := json.Marshal(test.value)
if err != nil {
t.Fatal(err)
}
if got := string(bgot); got != test.want {
t.Errorf("%#v: got %s, want %s", test.value, got, test.want)
}
}
}
func TestUnmarshalJSON(t *testing.T) {
var d LocalDate
var tm LocalTime
var dt LocalDateTime
for _, test := range []struct {
data string
ptr interface{}
want interface{}
}{
{`"1987-04-15"`, &d, &LocalDate{1987, 4, 15}},
{`"1987-04-\u0031\u0035"`, &d, &LocalDate{1987, 4, 15}},
{`"18:54:02"`, &tm, &LocalTime{18, 54, 2, 0}},
{`"1987-04-15T18:54:02"`, &dt, &LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}},
} {
if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil {
t.Fatalf("%s: %v", test.data, err)
}
if !cmpEqual(test.ptr, test.want) {
t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want)
}
}
for _, bad := range []string{"", `""`, `"bad"`, `"1987-04-15x"`,
`19870415`, // a JSON number
`11987-04-15x`, // not a JSON string
} {
if json.Unmarshal([]byte(bad), &d) == nil {
t.Errorf("%q, LocalDate: got nil, want error", bad)
}
if json.Unmarshal([]byte(bad), &tm) == nil {
t.Errorf("%q, LocalTime: got nil, want error", bad)
}
if json.Unmarshal([]byte(bad), &dt) == nil {
t.Errorf("%q, LocalDateTime: got nil, want error", bad)
}
}
}
+149 -60
View File
@@ -68,6 +68,9 @@ const (
var timeType = reflect.TypeOf(time.Time{}) var timeType = reflect.TypeOf(time.Time{})
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem() var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
var localDateType = reflect.TypeOf(LocalDate{})
var localTimeType = reflect.TypeOf(LocalTime{})
var localDateTimeType = reflect.TypeOf(LocalDateTime{})
// 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 {
@@ -85,29 +88,31 @@ func isPrimitive(mtype reflect.Type) bool {
case reflect.String: case reflect.String:
return true return true
case reflect.Struct: case reflect.Struct:
return mtype == timeType || isCustomMarshaler(mtype) return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType || isCustomMarshaler(mtype)
default: default:
return false return false
} }
} }
// Check if the given marshal type maps to a Tree slice // Check if the given marshal type maps to a Tree slice or array
func isTreeSlice(mtype reflect.Type) bool { func isTreeSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Slice:
return !isOtherSlice(mtype)
default:
return false
}
}
// Check if the given marshal type maps to a non-Tree slice
func isOtherSlice(mtype reflect.Type) bool {
switch mtype.Kind() { switch mtype.Kind() {
case reflect.Ptr: case reflect.Ptr:
return isOtherSlice(mtype.Elem()) return isTreeSequence(mtype.Elem())
case reflect.Slice: case reflect.Slice, reflect.Array:
return isPrimitive(mtype.Elem()) || isOtherSlice(mtype.Elem()) return isTree(mtype.Elem())
default:
return false
}
}
// Check if the given marshal type maps to a non-Tree slice or array
func isOtherSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isOtherSequence(mtype.Elem())
case reflect.Slice, reflect.Array:
return !isTreeSequence(mtype)
default: default:
return false return false
} }
@@ -116,6 +121,8 @@ func isOtherSlice(mtype reflect.Type) bool {
// Check if the given marshal type maps to a Tree // Check if the given marshal type maps to a Tree
func isTree(mtype reflect.Type) bool { func isTree(mtype reflect.Type) bool {
switch mtype.Kind() { switch mtype.Kind() {
case reflect.Ptr:
return isTree(mtype.Elem())
case reflect.Map: case reflect.Map:
return true return true
case reflect.Struct: case reflect.Struct:
@@ -170,7 +177,7 @@ Tree primitive types and corresponding marshal types:
float64 float32, float64, pointers to same float64 float32, float64, pointers to same
string string, pointers to same string string, pointers to same
bool bool, pointers to same bool bool, pointers to same
time.Time time.Time{}, pointers to same time.LocalTime time.LocalTime{}, pointers to same
For additional flexibility, use the Encoder API. For additional flexibility, use the Encoder API.
*/ */
@@ -295,7 +302,7 @@ func (e *Encoder) marshal(v interface{}) ([]byte, error) {
} }
var buf bytes.Buffer var buf bytes.Buffer
_, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order) _, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order, false)
return buf.Bytes(), err return buf.Bytes(), err
} }
@@ -313,20 +320,25 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
tval := e.nextTree() tval := e.nextTree()
switch mtype.Kind() { switch mtype.Kind() {
case reflect.Struct: case reflect.Struct:
for i := 0; i < mtype.NumField(); i++ { switch mval.Interface().(type) {
mtypef, mvalf := mtype.Field(i), mval.Field(i) case Tree:
opts := tomlOptions(mtypef, e.annotation) reflect.ValueOf(tval).Elem().Set(mval)
if opts.include && (!opts.omitempty || !isZero(mvalf)) { default:
val, err := e.valueToToml(mtypef.Type, mvalf) for i := 0; i < mtype.NumField(); i++ {
if err != nil { mtypef, mvalf := mtype.Field(i), mval.Field(i)
return nil, err opts := tomlOptions(mtypef, e.annotation)
} if opts.include && ((mtypef.Type.Kind() != reflect.Interface && !opts.omitempty) || !isZero(mvalf)) {
val, err := e.valueToToml(mtypef.Type, mvalf)
if err != nil {
return nil, err
}
tval.SetWithOptions(opts.name, SetOptions{ tval.SetWithOptions(opts.name, SetOptions{
Comment: opts.comment, Comment: opts.comment,
Commented: opts.commented, Commented: opts.commented,
Multiline: opts.multiline, Multiline: opts.multiline,
}, val) }, val)
}
} }
} }
case reflect.Map: case reflect.Map:
@@ -351,12 +363,15 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
} }
for _, key := range keys { for _, key := range keys {
mvalf := mval.MapIndex(key) mvalf := mval.MapIndex(key)
if (mtype.Elem().Kind() == reflect.Ptr || mtype.Elem().Kind() == reflect.Interface) && mvalf.IsNil() {
continue
}
val, err := e.valueToToml(mtype.Elem(), mvalf) val, err := e.valueToToml(mtype.Elem(), mvalf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if e.quoteMapKeys { if e.quoteMapKeys {
keyStr, err := tomlValueStringRepresentation(key.String(), "", e.arraysOneElementPerLine) keyStr, err := tomlValueStringRepresentation(key.String(), "", "", e.arraysOneElementPerLine)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -384,6 +399,9 @@ func (e *Encoder) valueToTreeSlice(mtype reflect.Type, mval reflect.Value) ([]*T
// Convert given marshal slice to slice of toml values // Convert given marshal slice to slice of toml values
func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (interface{}, error) { func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
if mtype.Elem().Kind() == reflect.Interface {
return nil, fmt.Errorf("marshal can't handle []interface{}")
}
tval := make([]interface{}, mval.Len(), mval.Len()) tval := make([]interface{}, mval.Len(), mval.Len())
for i := 0; i < mval.Len(); i++ { for i := 0; i < mval.Len(); i++ {
val, err := e.valueToToml(mtype.Elem(), mval.Index(i)) val, err := e.valueToToml(mtype.Elem(), mval.Index(i))
@@ -401,14 +419,17 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
if mtype.Kind() == reflect.Ptr { if mtype.Kind() == reflect.Ptr {
return e.valueToToml(mtype.Elem(), mval.Elem()) return e.valueToToml(mtype.Elem(), mval.Elem())
} }
if mtype.Kind() == reflect.Interface {
return e.valueToToml(mval.Elem().Type(), mval.Elem())
}
switch { switch {
case isCustomMarshaler(mtype): case isCustomMarshaler(mtype):
return callCustomMarshaler(mval) return callCustomMarshaler(mval)
case isTree(mtype): case isTree(mtype):
return e.valueToTree(mtype, mval) return e.valueToTree(mtype, mval)
case isTreeSlice(mtype): case isTreeSequence(mtype):
return e.valueToTreeSlice(mtype, mval) return e.valueToTreeSlice(mtype, mval)
case isOtherSlice(mtype): case isOtherSequence(mtype):
return e.valueToOtherSlice(mtype, mval) return e.valueToOtherSlice(mtype, mval)
default: default:
switch mtype.Kind() { switch mtype.Kind() {
@@ -426,7 +447,7 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
case reflect.String: case reflect.String:
return mval.String(), nil return mval.String(), nil
case reflect.Struct: case reflect.Struct:
return mval.Interface().(time.Time), nil return mval.Interface(), nil
default: default:
return nil, fmt.Errorf("Marshal can't handle %v(%v)", mtype, mtype.Kind()) return nil, fmt.Errorf("Marshal can't handle %v(%v)", mtype, mtype.Kind())
} }
@@ -554,11 +575,17 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
mval = reflect.New(mtype).Elem() mval = reflect.New(mtype).Elem()
} }
for i := 0; i < mtype.NumField(); i++ { switch mval.Interface().(type) {
mtypef := mtype.Field(i) case Tree:
an := annotation{tag: d.tagName} mval.Set(reflect.ValueOf(tval).Elem())
opts := tomlOptions(mtypef, an) default:
if opts.include { for i := 0; i < mtype.NumField(); i++ {
mtypef := mtype.Field(i)
an := annotation{tag: d.tagName}
opts := tomlOptions(mtypef, an)
if !opts.include {
continue
}
baseKey := opts.name baseKey := opts.name
keysToTry := []string{ keysToTry := []string{
baseKey, baseKey,
@@ -568,20 +595,22 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
} }
found := false found := false
for _, key := range keysToTry { if tval != nil {
exists := tval.Has(key) for _, key := range keysToTry {
if !exists { exists := tval.Has(key)
continue if !exists {
continue
}
val := tval.Get(key)
fval := mval.Field(i)
mvalf, err := d.valueFromToml(mtypef.Type, val, &fval)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.Field(i).Set(mvalf)
found = true
break
} }
val := tval.Get(key)
fval := mval.Field(i)
mvalf, err := d.valueFromToml(mtypef.Type, val, &fval)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.Field(i).Set(mvalf)
found = true
break
} }
if !found && opts.defaultValue != "" { if !found && opts.defaultValue != "" {
@@ -617,9 +646,13 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
mval.Field(i).Set(reflect.ValueOf(val)) mval.Field(i).Set(reflect.ValueOf(val))
} }
// save the old behavior above and try to check anonymous structs // save the old behavior above and try to check structs
if !found && opts.defaultValue == "" && mtypef.Anonymous && mtypef.Type.Kind() == reflect.Struct { if !found && opts.defaultValue == "" && mtypef.Type.Kind() == reflect.Struct {
v, err := d.valueFromTree(mtypef.Type, tval, nil) tmpTval := tval
if !mtypef.Anonymous {
tmpTval = nil
}
v, err := d.valueFromTree(mtypef.Type, tmpTval, nil)
if err != nil { if err != nil {
return v, err return v, err
} }
@@ -685,22 +718,71 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
if isTree(mtype) { if isTree(mtype) {
return d.valueFromTree(mtype, t, mval11) return d.valueFromTree(mtype, t, mval11)
} }
if mtype.Kind() == reflect.Interface {
if mval1 == nil || mval1.IsNil() {
return d.valueFromTree(reflect.TypeOf(map[string]interface{}{}), t, nil)
} else {
return d.valueFromToml(mval1.Elem().Type(), t, nil)
}
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a tree", tval, tval) return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a tree", tval, tval)
case []*Tree: case []*Tree:
if isTreeSlice(mtype) { if isTreeSequence(mtype) {
return d.valueFromTreeSlice(mtype, t) return d.valueFromTreeSlice(mtype, t)
} }
if mtype.Kind() == reflect.Interface {
if mval1 == nil || mval1.IsNil() {
return d.valueFromTreeSlice(reflect.TypeOf([]map[string]interface{}{}), t)
} else {
ival := mval1.Elem()
return d.valueFromToml(mval1.Elem().Type(), t, &ival)
}
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to trees", tval, tval) return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to trees", tval, tval)
case []interface{}: case []interface{}:
if isOtherSlice(mtype) { if isOtherSequence(mtype) {
return d.valueFromOtherSlice(mtype, t) return d.valueFromOtherSlice(mtype, t)
} }
if mtype.Kind() == reflect.Interface {
if mval1 == nil || mval1.IsNil() {
return d.valueFromOtherSlice(reflect.TypeOf([]interface{}{}), t)
} else {
ival := mval1.Elem()
return d.valueFromToml(mval1.Elem().Type(), t, &ival)
}
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a slice", tval, tval) return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a slice", tval, tval)
default: default:
switch mtype.Kind() { switch mtype.Kind() {
case reflect.Bool, reflect.Struct: case reflect.Bool, reflect.Struct:
val := reflect.ValueOf(tval) val := reflect.ValueOf(tval)
// if this passes for when mtype is reflect.Struct, tval is a time.Time
switch val.Type() {
case localDateType:
localDate := val.Interface().(LocalDate)
switch mtype {
case timeType:
return reflect.ValueOf(time.Date(localDate.Year, localDate.Month, localDate.Day, 0, 0, 0, 0, time.Local)), nil
}
case localDateTimeType:
localDateTime := val.Interface().(LocalDateTime)
switch mtype {
case timeType:
return reflect.ValueOf(time.Date(
localDateTime.Date.Year,
localDateTime.Date.Month,
localDateTime.Date.Day,
localDateTime.Time.Hour,
localDateTime.Time.Minute,
localDateTime.Time.Second,
localDateTime.Time.Nanosecond,
time.Local)), nil
}
}
// if this passes for when mtype is reflect.Struct, tval is a time.LocalTime
if !val.Type().ConvertibleTo(mtype) { if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
} }
@@ -755,6 +837,13 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
} }
return val.Convert(mtype), nil return val.Convert(mtype), nil
case reflect.Interface:
if mval1 == nil || mval1.IsNil() {
return reflect.ValueOf(tval), nil
} else {
ival := mval1.Elem()
return d.valueFromToml(mval1.Elem().Type(), t, &ival)
}
default: default:
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind()) return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind())
} }
@@ -764,7 +853,7 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) { func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) {
var melem *reflect.Value var melem *reflect.Value
if mval1 != nil && !mval1.IsNil() && mtype.Elem().Kind() == reflect.Struct { if mval1 != nil && !mval1.IsNil() && (mtype.Elem().Kind() == reflect.Struct || mtype.Elem().Kind() == reflect.Interface) {
elem := mval1.Elem() elem := mval1.Elem()
melem = &elem melem = &elem
} }
+1081 -8
View File
File diff suppressed because it is too large Load Diff
+38 -5
View File
@@ -318,6 +318,36 @@ func (p *tomlParser) parseRvalue() interface{} {
layout = strings.Replace(layout, "T", " ", 1) layout = strings.Replace(layout, "T", " ", 1)
} }
val, err := time.ParseInLocation(layout, tok.val, time.UTC) val, err := time.ParseInLocation(layout, tok.val, time.UTC)
if err != nil {
p.raiseError(tok, "%s", err)
}
return val
case tokenLocalDate:
v := strings.Replace(tok.val, " ", "T", -1)
isDateTime := false
isTime := false
for _, c := range v {
if c == 'T' || c == 't' {
isDateTime = true
break
}
if c == ':' {
isTime = true
break
}
}
var val interface{}
var err error
if isDateTime {
val, err = ParseLocalDateTime(v)
} else if isTime {
val, err = ParseLocalTime(v)
} else {
val, err = ParseLocalDate(v)
}
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
@@ -360,12 +390,15 @@ Loop:
} }
key := p.getToken() key := p.getToken()
p.assume(tokenEqual) p.assume(tokenEqual)
value := p.parseRvalue()
tree.Set(key.val, value) parsedKey, err := parseKey(key.val)
case tokenComma: if err != nil {
if previous == nil { p.raiseError(key, "invalid key: %s", err)
p.raiseError(follow, "inline table cannot start with a comma")
} }
value := p.parseRvalue()
tree.SetPath(parsedKey, value)
case tokenComma:
if tokenIsComma(previous) { if tokenIsComma(previous) {
p.raiseError(follow, "need field between two commas in inline table") p.raiseError(follow, "need field between two commas in inline table")
} }
+174 -6
View File
@@ -197,7 +197,7 @@ func TestFloatsWithExponents(t *testing.T) {
tree, err := Load("a = 5e+22\nb = 5E+22\nc = -5e+22\nd = -5e-22\ne = 6.626e-34") tree, err := Load("a = 5e+22\nb = 5E+22\nc = -5e+22\nd = -5e-22\ne = 6.626e-34")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
"a": float64(5e+22), "a": float64(5e+22),
"b": float64(5E+22), "b": float64(5e+22),
"c": float64(-5e+22), "c": float64(-5e+22),
"d": float64(-5e-22), "d": float64(-5e-22),
"e": float64(6.626e-34), "e": float64(6.626e-34),
@@ -225,10 +225,74 @@ func TestDateNano(t *testing.T) {
}) })
} }
func TestDateSpaceDelimiter(t *testing.T) { func TestLocalDateTime(t *testing.T) {
tree, err := Load("odt4 = 1979-05-27 07:32:00Z") tree, err := Load("a = 1979-05-27T07:32:00")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
"odt4": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC), "a": LocalDateTime{
Date: LocalDate{
Year: 1979,
Month: 5,
Day: 27,
},
Time: LocalTime{
Hour: 7,
Minute: 32,
Second: 0,
Nanosecond: 0,
}},
})
}
func TestLocalDateTimeNano(t *testing.T) {
tree, err := Load("a = 1979-05-27T07:32:00.999999")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalDateTime{
Date: LocalDate{
Year: 1979,
Month: 5,
Day: 27,
},
Time: LocalTime{
Hour: 7,
Minute: 32,
Second: 0,
Nanosecond: 999999000,
}},
})
}
func TestLocalDate(t *testing.T) {
tree, err := Load("a = 1979-05-27")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalDate{
Year: 1979,
Month: 5,
Day: 27,
},
})
}
func TestLocalTime(t *testing.T) {
tree, err := Load("a = 07:32:00")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalTime{
Hour: 7,
Minute: 32,
Second: 0,
Nanosecond: 0,
},
})
}
func TestLocalTimeNano(t *testing.T) {
tree, err := Load("a = 00:32:00.999999")
assertTree(t, tree, err, map[string]interface{}{
"a": LocalTime{
Hour: 0,
Minute: 32,
Second: 0,
Nanosecond: 999999000,
},
}) })
} }
@@ -517,6 +581,39 @@ func TestDoubleInlineGroup(t *testing.T) {
}) })
} }
func TestNestedInlineGroup(t *testing.T) {
tree, err := Load("out = {block0 = {x = 99, y = 100}, block1 = {p = \"999\", q = \"1000\"}}")
assertTree(t, tree, err, map[string]interface{}{
"out": map[string]interface{}{
"block0": map[string]interface{}{
"x": int64(99),
"y": int64(100),
},
"block1": map[string]interface{}{
"p": "999",
"q": "1000",
},
},
})
}
func TestArrayInNestedInlineGroup(t *testing.T) {
tree, err := Load(`image = {name = "xxx", palette = {id = 100, colors = ["red", "blue", "green"]}}`)
assertTree(t, tree, err, map[string]interface{}{
"image": map[string]interface{}{
"name": "xxx",
"palette": map[string]interface{}{
"id": int64(100),
"colors": []string{
"red",
"blue",
"green",
},
},
},
})
}
func TestExampleInlineGroup(t *testing.T) { func TestExampleInlineGroup(t *testing.T) {
tree, err := Load(`name = { first = "Tom", last = "Preston-Werner" } tree, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }`) point = { x = 1, y = 2 }`)
@@ -532,6 +629,33 @@ point = { x = 1, y = 2 }`)
}) })
} }
func TestInlineGroupBareKeysUnderscore(t *testing.T) {
tree, err := Load(`foo = { _bar = "buz" }`)
assertTree(t, tree, err, map[string]interface{}{
"foo": map[string]interface{}{
"_bar": "buz",
},
})
}
func TestInlineGroupBareKeysDash(t *testing.T) {
tree, err := Load(`foo = { -bar = "buz" }`)
assertTree(t, tree, err, map[string]interface{}{
"foo": map[string]interface{}{
"-bar": "buz",
},
})
}
func TestInlineGroupKeyQuoted(t *testing.T) {
tree, err := Load(`foo = { "bar" = "buz" }`)
assertTree(t, tree, err, map[string]interface{}{
"foo": map[string]interface{}{
"bar": "buz",
},
})
}
func TestExampleInlineGroupInArray(t *testing.T) { func TestExampleInlineGroupInArray(t *testing.T) {
tree, err := Load(`points = [{ x = 1, y = 2 }]`) tree, err := Load(`points = [{ x = 1, y = 2 }]`)
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
@@ -560,7 +684,7 @@ func TestInlineTableCommaExpected(t *testing.T) {
func TestInlineTableCommaStart(t *testing.T) { func TestInlineTableCommaStart(t *testing.T) {
_, err := Load("foo = {, hello = 53}") _, err := Load("foo = {, hello = 53}")
if err.Error() != "(1, 8): inline table cannot start with a comma" { if err.Error() != "(1, 8): unexpected token type in inline table: keys cannot contain , character" {
t.Error("Bad error message:", err.Error()) t.Error("Bad error message:", err.Error())
} }
} }
@@ -773,7 +897,7 @@ func TestTomlValueStringRepresentation(t *testing.T) {
"[\"gamma\",\"delta\"]"}, "[\"gamma\",\"delta\"]"},
{nil, ""}, {nil, ""},
} { } {
result, err := tomlValueStringRepresentation(item.Value, "", false) result, err := tomlValueStringRepresentation(item.Value, "", "", false)
if err != nil { if err != nil {
t.Errorf("Test %d - unexpected error: %s", idx, err) t.Errorf("Test %d - unexpected error: %s", idx, err)
} }
@@ -916,6 +1040,13 @@ func TestMapKeyIsNum(t *testing.T) {
} }
} }
func TestInvalidKeyInlineTable(t *testing.T) {
_, err := Load("table={invalid..key = 1}")
if err.Error() != "(1, 8): invalid key: expecting key part after dot" {
t.Error("Bad error message:", err.Error())
}
}
func TestDottedKeys(t *testing.T) { func TestDottedKeys(t *testing.T) {
tree, err := Load(` tree, err := Load(`
name = "Orange" name = "Orange"
@@ -944,3 +1075,40 @@ func TestInvalidDottedKeyEmptyGroup(t *testing.T) {
t.Fatalf("invalid error message: %s", err) t.Fatalf("invalid error message: %s", err)
} }
} }
func TestAccidentalNewlines(t *testing.T) {
expected := "The quick brown fox jumps over the lazy dog."
tree, err := Load(`str1 = "The quick brown fox jumps over the lazy dog."
str2 = """
The quick brown \
fox jumps over \
the lazy dog."""
str3 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := tree.Get("str1")
if got != expected {
t.Errorf("expected '%s', got '%s'", expected, got)
}
got = tree.Get("str2")
if got != expected {
t.Errorf("expected '%s', got '%s'", expected, got)
}
got = tree.Get("str3")
if got != expected {
t.Errorf("expected '%s', got '%s'", expected, got)
}
}
+3 -10
View File
@@ -2,7 +2,6 @@ package toml
import ( import (
"fmt" "fmt"
"strconv"
"unicode" "unicode"
) )
@@ -35,6 +34,7 @@ const (
tokenDoubleLeftBracket tokenDoubleLeftBracket
tokenDoubleRightBracket tokenDoubleRightBracket
tokenDate tokenDate
tokenLocalDate
tokenKeyGroup tokenKeyGroup
tokenKeyGroupArray tokenKeyGroupArray
tokenComma tokenComma
@@ -68,7 +68,8 @@ var tokenTypeNames = []string{
")", ")",
"]]", "]]",
"[[", "[[",
"Date", "LocalDate",
"LocalDate",
"KeyGroup", "KeyGroup",
"KeyGroupArray", "KeyGroupArray",
",", ",",
@@ -95,14 +96,6 @@ func (tt tokenType) String() string {
return "Unknown" return "Unknown"
} }
func (t token) Int() int {
if result, err := strconv.Atoi(t.val); err != nil {
panic(err)
} else {
return result
}
}
func (t token) String() string { func (t token) String() string {
switch t.typ { switch t.typ {
case tokenEOF: case tokenEOF:
+2 -1
View File
@@ -25,7 +25,8 @@ func TestTokenStringer(t *testing.T) {
{tokenRightParen, ")"}, {tokenRightParen, ")"},
{tokenDoubleLeftBracket, "]]"}, {tokenDoubleLeftBracket, "]]"},
{tokenDoubleRightBracket, "[["}, {tokenDoubleRightBracket, "[["},
{tokenDate, "Date"}, {tokenDate, "LocalDate"},
{tokenLocalDate, "LocalDate"},
{tokenKeyGroup, "KeyGroup"}, {tokenKeyGroup, "KeyGroup"},
{tokenKeyGroupArray, "KeyGroupArray"}, {tokenKeyGroupArray, "KeyGroupArray"},
{tokenComma, ","}, {tokenComma, ","},
+4
View File
@@ -222,8 +222,12 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
switch v := value.(type) { switch v := value.(type) {
case *Tree: case *Tree:
v.comment = opts.Comment v.comment = opts.Comment
v.commented = opts.Commented
toInsert = value toInsert = value
case []*Tree: case []*Tree:
for i := range v {
v[i].commented = opts.Commented
}
toInsert = value toInsert = value
case *tomlValue: case *tomlValue:
v.comment = opts.Comment v.comment = opts.Comment
+33 -21
View File
@@ -28,9 +28,10 @@ type sortNode struct {
// Encodes a string to a TOML-compliant multi-line string value // Encodes a string to a TOML-compliant multi-line string value
// This function is a clone of the existing encodeTomlString function, except that whitespace characters // This function is a clone of the existing encodeTomlString function, except that whitespace characters
// are preserved. Quotation marks and backslashes are also not escaped. // are preserved. Quotation marks and backslashes are also not escaped.
func encodeMultilineTomlString(value string) string { func encodeMultilineTomlString(value string, commented string) string {
var b bytes.Buffer var b bytes.Buffer
b.WriteString(commented)
for _, rr := range value { for _, rr := range value {
switch rr { switch rr {
case '\b': case '\b':
@@ -38,7 +39,7 @@ func encodeMultilineTomlString(value string) string {
case '\t': case '\t':
b.WriteString("\t") b.WriteString("\t")
case '\n': case '\n':
b.WriteString("\n") b.WriteString("\n" + commented)
case '\f': case '\f':
b.WriteString(`\f`) b.WriteString(`\f`)
case '\r': case '\r':
@@ -91,7 +92,7 @@ func encodeTomlString(value string) string {
return b.String() return b.String()
} }
func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElementPerLine bool) (string, error) { func tomlValueStringRepresentation(v interface{}, commented string, indent string, arraysOneElementPerLine bool) (string, error) {
// this interface check is added to dereference the change made in the writeTo function. // this interface check is added to dereference the change made in the writeTo function.
// That change was made to allow this function to see formatting options. // That change was made to allow this function to see formatting options.
tv, ok := v.(*tomlValue) tv, ok := v.(*tomlValue)
@@ -123,12 +124,12 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil
case string: case string:
if tv.multiline { if tv.multiline {
return "\"\"\"\n" + encodeMultilineTomlString(value) + "\"\"\"", nil return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", nil
} }
return "\"" + encodeTomlString(value) + "\"", nil return "\"" + encodeTomlString(value) + "\"", nil
case []byte: case []byte:
b, _ := v.([]byte) b, _ := v.([]byte)
return tomlValueStringRepresentation(string(b), indent, arraysOneElementPerLine) return tomlValueStringRepresentation(string(b), commented, indent, arraysOneElementPerLine)
case bool: case bool:
if value { if value {
return "true", nil return "true", nil
@@ -136,6 +137,12 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
return "false", nil return "false", nil
case time.Time: case time.Time:
return value.Format(time.RFC3339), nil return value.Format(time.RFC3339), nil
case LocalDate:
return value.String(), nil
case LocalDateTime:
return value.String(), nil
case LocalTime:
return value.String(), nil
case nil: case nil:
return "", nil return "", nil
} }
@@ -146,7 +153,7 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
var values []string var values []string
for i := 0; i < rv.Len(); i++ { for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface() item := rv.Index(i).Interface()
itemRepr, err := tomlValueStringRepresentation(item, indent, arraysOneElementPerLine) itemRepr, err := tomlValueStringRepresentation(item, commented, indent, arraysOneElementPerLine)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -160,12 +167,12 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
for _, value := range values { for _, value := range values {
stringBuffer.WriteString(valueIndent) stringBuffer.WriteString(valueIndent)
stringBuffer.WriteString(value) stringBuffer.WriteString(commented + value)
stringBuffer.WriteString(`,`) stringBuffer.WriteString(`,`)
stringBuffer.WriteString("\n") stringBuffer.WriteString("\n")
} }
stringBuffer.WriteString(indent + "]") stringBuffer.WriteString(indent + commented + "]")
return stringBuffer.String(), nil return stringBuffer.String(), nil
} }
@@ -264,10 +271,10 @@ func sortAlphabetical(t *Tree) (vals []sortNode) {
} }
func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) { func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) {
return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical) return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical, false)
} }
func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder) (int64, error) { func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder, parentCommented bool) (int64, error) {
var orderedVals []sortNode var orderedVals []sortNode
switch ord { switch ord {
@@ -287,10 +294,6 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
if keyspace != "" { if keyspace != "" {
combinedKey = keyspace + "." + combinedKey combinedKey = keyspace + "." + combinedKey
} }
var commented string
if t.commented {
commented = "# "
}
switch node := v.(type) { switch node := v.(type) {
// node has to be of those two types given how keys are sorted above // node has to be of those two types given how keys are sorted above
@@ -311,24 +314,33 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
return bytesCount, errc return bytesCount, errc
} }
} }
var commented string
if parentCommented || t.commented || tv.commented {
commented = "# "
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n") writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
bytesCount += int64(writtenBytesCount) bytesCount += int64(writtenBytesCount)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
bytesCount, err = node.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord) bytesCount, err = node.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord, parentCommented || t.commented || tv.commented)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
case []*Tree: case []*Tree:
for _, subTree := range node { for _, subTree := range node {
var commented string
if parentCommented || t.commented || subTree.commented {
commented = "# "
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n") writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
bytesCount += int64(writtenBytesCount) bytesCount += int64(writtenBytesCount)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
bytesCount, err = subTree.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord) bytesCount, err = subTree.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord, parentCommented || t.commented || subTree.commented)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
@@ -341,7 +353,11 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k]) return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
} }
repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine) var commented string
if parentCommented || t.commented || v.commented {
commented = "# "
}
repr, err := tomlValueStringRepresentation(v, commented, indent, arraysOneElementPerLine)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
@@ -359,10 +375,6 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
} }
} }
var commented string
if v.commented {
commented = "# "
}
quotedKey := quoteKeyIfNeeded(k) quotedKey := quoteKeyIfNeeded(k)
writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n") writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
bytesCount += int64(writtenBytesCount) bytesCount += int64(writtenBytesCount)