Compare commits

...

27 Commits

Author SHA1 Message Date
Thomas Pelletier 8410c965c2 Suggest using go-toml v2 2021-06-03 22:04:06 -04:00
Mikhail f. Shiryaev d083470585 Add Encoder.CompactComments to omit extra new line (#541) 2021-05-12 09:22:40 -04:00
Mikhail f. Shiryaev c893dbf25c Fix empty trees line counting (#539)
Refs #450
2021-05-11 08:50:05 -04:00
dependabot-preview[bot] 2a1df71375 Upgrade to GitHub-native Dependabot (#531)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-04-29 19:33:19 -04:00
Ikko Ashimine a2f5197638 Fix typo in README.md (#513)
availble -> available
2021-04-16 08:03:59 -04:00
Thomas Pelletier bb65137dc4 Move v2 warning into a comment 2021-04-10 19:54:52 -04:00
Thomas Pelletier 99782c87cf Add v2 warning in bug report template 2021-04-10 19:50:55 -04:00
Thomas Pelletier ce6fbd7bc0 Support literal multiline marshal (#485)
Use struct tag `multiline:"true" literal:"true"` to enable it.
2021-03-25 20:57:38 -04:00
Sapphire Becker b59c12a70d Remove go-spew dependency (#483) 2021-03-10 20:18:32 -05:00
Thomas Pelletier 6a307ac0d0 Update CI for Go 1.16 (#482)
Fixes #479
2021-03-04 09:58:18 -05:00
Thomas Pelletier a2e5256180 CI should also run on master
Otherwise codecov diff is incorrect.
2021-02-06 08:03:04 -05:00
Thomas Pelletier 5163266f16 Create codeql-analysis.yml (#473) 2021-02-05 08:33:55 -05:00
Vincent Serpoul b4f0a950bf Value string representation public function (#469)
Fixes #468
2021-02-03 08:48:53 -05:00
Thomas Pelletier ef48fb2be1 Expose MarshalOrder (#470)
It is needed as argument to some already public function.

Fixes #459
Ref #469
2021-02-02 12:12:10 -05:00
Vincent Serpoul c9a09d8695 Provide Tree and treeValue public aliases (#467)
Provide public aliases for Tree and treeValue to give more                                                       
control when manipulating TOML documents. This is a stop-gap
measure until we redesign the interface in v2.

Fix #466
2021-01-29 08:31:09 -05:00
y-yagi 3430b0f086 Update docs to reference pkg.go.dev (#465) 2021-01-25 08:31:50 -05:00
Richard Patel a713a3eccc Improved default tag for durations (#464) 2021-01-21 16:47:51 -05:00
Thomas Pelletier 652b9f8232 Cleanup (#462)
* Remove feature request template

Those should now start in discussions

https://github.com/pelletier/go-toml/discussions

* Update lincese year

* ci: do not run coverage on master

It only makes sense to report it for diffs on pull requests.
2021-01-06 20:48:20 -05:00
Thomas Pelletier ba1b12be14 Fix ToMap for tables in nested mixed-type arrays (#461)
Co-authored-by: Micah Stetson <micah@schoolsplp.com>
2021-01-06 20:34:25 -05:00
Stanisław Barzowski 2e01f733df [README] There are 3 cli tools, not 2. (#454) 2020-11-24 13:14:26 -05:00
Micah Stetson 1bd9461acb Fix ToMap for tables in mixed-type arrays (#453) 2020-11-14 21:15:35 -05:00
Thomas Pelletier 5b4e7e5dcc Remove underscore regexps (#448)
* Remove underscore regexps

Fixes #440.

```
benchmark                    old ns/op     new ns/op     delta
BenchmarkUnmarshalToml-8     269582        257032        -4.66%

benchmark                    old allocs     new allocs     delta
BenchmarkUnmarshalToml-8     2650           2650           +0.00%

benchmark                    old bytes     new bytes     delta
BenchmarkUnmarshalToml-8     127761        127030        -0.57%
```
2020-10-11 19:27:08 -04:00
Thomas Pelletier b4905040a8 TOML 1.0.0-rc.3 (#449)
No spec change since 1.0.0-rc.1.
2020-10-11 16:12:23 -04:00
Thomas Pelletier 5c66c78bc5 Remove date regexp (#447)
* Remove date regexp

Hand-roll the date matching logic to avoid trying to match a regexp on
every integer.

```
benchmark                    old ns/op     new ns/op     delta
BenchmarkUnmarshalToml-8     293449        272134        -7.26%

benchmark                    old allocs     new allocs     delta
BenchmarkUnmarshalToml-8     2746           2650           -3.50%

benchmark                    old bytes     new bytes     delta
BenchmarkUnmarshalToml-8     133604        127548        -4.53%
```

* Remove fuzzit

The company has been acquired by GitLab and shutting down.
2020-10-11 15:31:33 -04:00
Allen f9ba08244d Do not allow T-prefix on local dates (#446)
Fixes #442
2020-10-09 10:55:11 -04:00
Thomas Pelletier e6908614ee toml.Unmarshaler supports leaf nodes (#444)
Fixes #437
2020-09-13 18:46:13 -04:00
Cameron Moore a7448fe8de Fix date lexer to only support 4-digit year (#443)
Fixes #441
2020-09-12 18:04:04 -04:00
27 changed files with 1227 additions and 307 deletions
+11 -2
View File
@@ -1,9 +1,18 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
--- ---
<!--
‼️ Main development focus is on the upcoming go-toml v2 ⚠️
As a result, v1.x bugs will likely not see a fix on a v1.x version.
However, reporting the bug is the best way to ensure that it will be fixed in v2.
See https://github.com/pelletier/go-toml/discussions/506.
-->
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@@ -14,7 +23,7 @@ Steps to reproduce the behavior. Including TOML files.
A clear and concise description of what you expected to happen, if other than "should work". A clear and concise description of what you expected to happen, if other than "should work".
**Versions** **Versions**
- go-toml: version (git sha) - go-toml: version (or git sha)
- go: version - go: version
- operating system: e.g. macOS, Windows, Linux - operating system: e.g. macOS, Windows, Linux
-17
View File
@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+8
View File
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "13:00"
open-pull-requests-limit: 10
+67
View File
@@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '26 19 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+2 -2
View File
@@ -28,7 +28,7 @@ improve the documentation. Fix a typo, clarify an interface, add an
example, anything goes! example, anything goes!
The documentation is present in the [README][readme] and thorough the The documentation is present in the [README][readme] and thorough the
source code. On release, it gets updated on [GoDoc][godoc]. To make a source code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a
change to the documentation, create a pull request with your proposed change to the documentation, create a pull request with your proposed
changes. For simple changes like that, the easiest way to go is probably changes. For simple changes like that, the easiest way to go is probably
the "Fork this project and edit the file" button on Github, displayed at the "Fork this project and edit the file" button on Github, displayed at
@@ -123,7 +123,7 @@ Checklist:
[issues-tracker]: https://github.com/pelletier/go-toml/issues [issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md [bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
[godoc]: https://godoc.org/github.com/pelletier/go-toml [pkg.go.dev]: https://pkg.go.dev/github.com/pelletier/go-toml
[readme]: ./README.md [readme]: ./README.md
[fork]: https://help.github.com/articles/fork-a-repo [fork]: https://help.github.com/articles/fork-a-repo
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request [pull-request]: https://help.github.com/en/articles/creating-a-pull-request
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton Copyright (c) 2013 - 2021 Thomas Pelletier, Eric Anderton
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+31 -6
View File
@@ -1,17 +1,42 @@
# go-toml # go-toml
Go library for the [TOML](https://github.com/mojombo/toml) format. Go library for the [TOML](https://toml.io/) format.
This library supports TOML version This library supports TOML version
[v1.0.0-rc.1](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v1.0.0-rc.1.md) [v1.0.0-rc.3](https://toml.io/en/v1.0.0-rc.3)
[![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml) [![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml.svg)](https://pkg.go.dev/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)
[![Build Status](https://dev.azure.com/pelletierthomas/go-toml-ci/_apis/build/status/pelletier.go-toml?branchName=master)](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master) [![Build Status](https://dev.azure.com/pelletierthomas/go-toml-ci/_apis/build/status/pelletier.go-toml?branchName=master)](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/pelletier/go-toml/branch/master/graph/badge.svg)](https://codecov.io/gh/pelletier/go-toml) [![codecov](https://codecov.io/gh/pelletier/go-toml/branch/master/graph/badge.svg)](https://codecov.io/gh/pelletier/go-toml)
[![Go Report Card](https://goreportcard.com/badge/github.com/pelletier/go-toml)](https://goreportcard.com/report/github.com/pelletier/go-toml) [![Go Report Card](https://goreportcard.com/badge/github.com/pelletier/go-toml)](https://goreportcard.com/report/github.com/pelletier/go-toml)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield)
## Development status
**️ Consider go-toml v2!**
The next version of go-toml is in [active development][v2-dev], and
[nearing completion][v2-map].
Though technically in beta, v2 is already more tested, [fixes bugs][v1-bugs],
and [much faster][v2-bench]. If you only need reading and writing TOML documents
(majority of cases), those features are implemented and the API unlikely to
change.
The remaining features (Document structure editing and tooling) will be added
shortly. While pull-requests are welcome on v1, no active development is
expected on it. When v2.0.0 is released, v1 will be deprecated.
👉 [go-toml v2][v2]
[v2]: https://github.com/pelletier/go-toml/tree/v2
[v2-map]: https://github.com/pelletier/go-toml/discussions/506
[v2-dev]: https://github.com/pelletier/go-toml/tree/v2
[v1-bugs]: https://github.com/pelletier/go-toml/issues?q=is%3Aissue+is%3Aopen+label%3Av2-fixed
[v2-bench]: https://github.com/pelletier/go-toml/tree/v2#benchmarks
## Features ## Features
Go-toml provides the following features for using data parsed from TOML documents: Go-toml provides the following features for using data parsed from TOML documents:
@@ -81,11 +106,11 @@ for ii, item := range results.Values() {
## Documentation ## Documentation
The documentation and additional examples are available at The documentation and additional examples are available at
[godoc.org](http://godoc.org/github.com/pelletier/go-toml). [pkg.go.dev](https://pkg.go.dev/github.com/pelletier/go-toml).
## Tools ## Tools
Go-toml provides two handy command line tools: Go-toml provides three handy command line tools:
* `tomll`: Reads TOML files and lints them. * `tomll`: Reads TOML files and lints them.
@@ -109,7 +134,7 @@ Go-toml provides two handy command line tools:
### Docker image ### Docker image
Those tools are also availble as a Docker image from Those tools are also available as a Docker image from
[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to [dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to
use `tomljson`: use `tomljson`:
+16 -58
View File
@@ -2,30 +2,6 @@ trigger:
- master - master
stages: stages:
- stage: fuzzit
displayName: "Run Fuzzit"
dependsOn: []
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
jobs:
- job: submit
displayName: "Submit"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.15"
inputs:
version: "1.15"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
- task: Bash@3
inputs:
filePath: './fuzzit.sh'
env:
TYPE: fuzzing
FUZZIT_API_KEY: $(FUZZIT_API_KEY)
- stage: run_checks - stage: run_checks
displayName: "Check" displayName: "Check"
dependsOn: [] dependsOn: []
@@ -36,9 +12,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.15" displayName: "Install Go 1.16"
inputs: inputs:
version: "1.15" version: "1.16"
- task: Go@0 - task: Go@0
displayName: "go fmt ./..." displayName: "go fmt ./..."
inputs: inputs:
@@ -51,9 +27,9 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.15" displayName: "Install Go 1.16"
inputs: inputs:
version: "1.15" version: "1.16"
- task: Go@0 - task: Go@0
displayName: "Generate coverage" displayName: "Generate coverage"
inputs: inputs:
@@ -71,37 +47,28 @@ stages:
vmImage: ubuntu-latest vmImage: ubuntu-latest
steps: steps:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go 1.15" displayName: "Install Go 1.16"
inputs: inputs:
version: "1.15" version: "1.16"
- 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:
filePath: './benchmark.sh' filePath: './benchmark.sh'
arguments: "master $(Build.Repository.Uri)" arguments: "master $(Build.Repository.Uri)"
- job: fuzzing
displayName: "fuzzing"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.15"
inputs:
version: "1.15"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
- task: Bash@3
inputs:
filePath: './fuzzit.sh'
env:
TYPE: local-regression
- job: go_unit_tests - job: go_unit_tests
displayName: "unit tests" displayName: "unit tests"
strategy: strategy:
matrix: matrix:
linux 1.16:
goVersion: '1.16'
imageName: 'ubuntu-latest'
mac 1.16:
goVersion: '1.16'
imageName: 'macOS-latest'
windows 1.16:
goVersion: '1.16'
imageName: 'windows-latest'
linux 1.15: linux 1.15:
goVersion: '1.15' goVersion: '1.15'
imageName: 'ubuntu-latest' imageName: 'ubuntu-latest'
@@ -111,15 +78,6 @@ stages:
windows 1.15: windows 1.15:
goVersion: '1.15' goVersion: '1.15'
imageName: 'windows-latest' imageName: 'windows-latest'
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'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
@@ -155,7 +113,7 @@ stages:
- task: GoTool@0 - task: GoTool@0
displayName: "Install Go" displayName: "Install Go"
inputs: inputs:
version: 1.15 version: 1.16
- task: Bash@3 - task: Bash@3
inputs: inputs:
targetType: inline targetType: inline
+1
View File
@@ -137,6 +137,7 @@ func BenchmarkUnmarshalToml(b *testing.B) {
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
target := benchmarkDoc{} target := benchmarkDoc{}
-2
View File
@@ -1,7 +1,5 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
set -xe
# go-fuzz doesn't support modules yet, so ensure we do everything
# in the old style GOPATH way
export GO111MODULE="off"
# install go-fuzz
go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
# target name can only contain lower-case letters (a-z), digits (0-9) and a dash (-)
# to add another target, make sure to create it with `fuzzit create target`
# before using `fuzzit create job`
TARGET=toml-fuzzer
go-fuzz-build -libfuzzer -o ${TARGET}.a github.com/pelletier/go-toml
clang -fsanitize=fuzzer ${TARGET}.a -o ${TARGET}
# install fuzzit for talking to fuzzit.dev service
# or latest version:
# https://github.com/fuzzitdev/fuzzit/releases/latest/download/fuzzit_Linux_x86_64
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.52/fuzzit_Linux_x86_64
chmod a+x fuzzit
# TODO: change kkowalczyk to go-toml and create toml-fuzzer target there
./fuzzit create job --type $TYPE go-toml/${TARGET} ${TARGET}
-2
View File
@@ -1,5 +1,3 @@
module github.com/pelletier/go-toml module github.com/pelletier/go-toml
go 1.12 go 1.12
require github.com/davecgh/go-spew v1.1.1
-19
View File
@@ -1,19 +0,0 @@
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
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=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+265 -41
View File
@@ -9,13 +9,10 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"regexp"
"strconv" "strconv"
"strings" "strings"
) )
var dateRegexp *regexp.Regexp
// Define state functions // Define state functions
type tomlLexStateFn func() tomlLexStateFn type tomlLexStateFn func() tomlLexStateFn
@@ -216,18 +213,12 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
break break
} }
possibleDate := l.peekString(35) if next == '+' || next == '-' {
dateSubmatches := dateRegexp.FindStringSubmatch(possibleDate) return l.lexNumber
if dateSubmatches != nil && dateSubmatches[0] != "" {
l.fastForward(len(dateSubmatches[0]))
if dateSubmatches[2] == "" { // no timezone information => local date
return l.lexLocalDate
}
return l.lexDate
} }
if next == '+' || next == '-' || isDigit(next) { if isDigit(next) {
return l.lexNumber return l.lexDateTimeOrNumber
} }
return l.errorf("no value can start with %c", next) return l.errorf("no value can start with %c", next)
@@ -237,6 +228,32 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
return nil return nil
} }
func (l *tomlLexer) lexDateTimeOrNumber() tomlLexStateFn {
// Could be either a date/time, or a digit.
// The options for date/times are:
// YYYY-... => date or date-time
// HH:... => time
// Anything else should be a number.
lookAhead := l.peekString(5)
if len(lookAhead) < 3 {
return l.lexNumber()
}
for idx, r := range lookAhead {
if !isDigit(r) {
if idx == 2 && r == ':' {
return l.lexDateTimeOrTime()
}
if idx == 4 && r == '-' {
return l.lexDateTimeOrTime()
}
return l.lexNumber()
}
}
return l.lexNumber()
}
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn { func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
l.next() l.next()
l.emit(tokenLeftCurlyBrace) l.emit(tokenLeftCurlyBrace)
@@ -254,15 +271,246 @@ func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexDate() tomlLexStateFn { func (l *tomlLexer) lexDateTimeOrTime() tomlLexStateFn {
l.emit(tokenDate) // 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
// we already know those two are digits
l.next()
l.next()
// Got 2 digits. At that point it could be either a time or a date(-time).
r := l.next()
if r == ':' {
return l.lexTime()
}
return l.lexDateTime()
}
func (l *tomlLexer) lexDateTime() tomlLexStateFn {
// This state accepts an offset date-time, a local date-time, or a local date.
//
// v--- cursor
// 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
// date
// already checked by lexRvalue
l.next() // digit
l.next() // -
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid month digit in date: %c", r)
}
}
r := l.next()
if r != '-' {
return l.errorf("expected - to separate month of a date, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid day digit in date: %c", r)
}
}
l.emit(tokenLocalDate)
r = l.peek()
if r == eof {
return l.lexRvalue return l.lexRvalue
} }
func (l *tomlLexer) lexLocalDate() tomlLexStateFn { if r != ' ' && r != 'T' {
l.emit(tokenLocalDate) return l.errorf("incorrect date/time separation character: %c", r)
}
if r == ' ' {
lookAhead := l.peekString(3)[1:]
if len(lookAhead) < 2 {
return l.lexRvalue return l.lexRvalue
} }
for _, r := range lookAhead {
if !isDigit(r) {
return l.lexRvalue
}
}
}
l.skip() // skip the T or ' '
// time
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid hour digit in time: %c", r)
}
}
r = l.next()
if r != ':' {
return l.errorf("time hour/minute separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid minute digit in time: %c", r)
}
}
r = l.next()
if r != ':' {
return l.errorf("time minute/second separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid second digit in time: %c", r)
}
}
r = l.peek()
if r == '.' {
l.next()
r := l.next()
if !isDigit(r) {
return l.errorf("expected at least one digit in time's fraction, not %c", r)
}
for {
r := l.peek()
if !isDigit(r) {
break
}
l.next()
}
}
l.emit(tokenLocalTime)
return l.lexTimeOffset
}
func (l *tomlLexer) lexTimeOffset() tomlLexStateFn {
// potential offset
// Z
// -07:00
// +07:00
// nothing
r := l.peek()
if r == 'Z' {
l.next()
l.emit(tokenTimeOffset)
} else if r == '+' || r == '-' {
l.next()
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid hour digit in time offset: %c", r)
}
}
r = l.next()
if r != ':' {
return l.errorf("time offset hour/minute separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid minute digit in time offset: %c", r)
}
}
l.emit(tokenTimeOffset)
}
return l.lexRvalue
}
func (l *tomlLexer) lexTime() tomlLexStateFn {
// v--- cursor
// 07:32:00
// 00:32:00.999999
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid minute digit in time: %c", r)
}
}
r := l.next()
if r != ':' {
return l.errorf("time minute/second separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid second digit in time: %c", r)
}
}
r = l.peek()
if r == '.' {
l.next()
r := l.next()
if !isDigit(r) {
return l.errorf("expected at least one digit in time's fraction, not %c", r)
}
for {
r := l.peek()
if !isDigit(r) {
break
}
l.next()
}
}
l.emit(tokenLocalTime)
return l.lexRvalue
}
func (l *tomlLexer) lexTrue() tomlLexStateFn { func (l *tomlLexer) lexTrue() tomlLexStateFn {
l.fastForward(4) l.fastForward(4)
@@ -767,30 +1015,6 @@ func (l *tomlLexer) run() {
} }
} }
func init() {
// 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
func lexToml(inputBytes []byte) []token { func lexToml(inputBytes []byte) []token {
runes := bytes.Runes(inputBytes) runes := bytes.Runes(inputBytes)
+301 -32
View File
@@ -1,17 +1,63 @@
package toml package toml
import ( import (
"bytes"
"fmt"
"reflect" "reflect"
"testing" "testing"
"text/tabwriter"
) )
func testFlow(t *testing.T, input string, expectedFlow []token) { func testFlow(t *testing.T, input string, expectedFlow []token) {
tokens := lexToml([]byte(input)) tokens := lexToml([]byte(input))
if !reflect.DeepEqual(tokens, expectedFlow) { if !reflect.DeepEqual(tokens, expectedFlow) {
t.Fatalf("Different flows.\nExpected:\n%v\nGot:\n%v", expectedFlow, tokens) diffFlowsColumnsFatal(t, expectedFlow, tokens)
} }
} }
func diffFlowsColumnsFatal(t *testing.T, expectedFlow []token, actualFlow []token) {
max := len(expectedFlow)
if len(actualFlow) > max {
max = len(actualFlow)
}
b := &bytes.Buffer{}
w := tabwriter.NewWriter(b, 0, 0, 1, ' ', tabwriter.Debug)
fmt.Fprintln(w, "expected\tT\tP\tactual\tT\tP\tdiff")
for i := 0; i < max; i++ {
expected := ""
expectedType := ""
expectedPos := ""
if i < len(expectedFlow) {
expected = fmt.Sprintf("%s", expectedFlow[i])
expectedType = fmt.Sprintf("%s", expectedFlow[i].typ)
expectedPos = expectedFlow[i].Position.String()
}
actual := ""
actualType := ""
actualPos := ""
if i < len(actualFlow) {
actual = fmt.Sprintf("%s", actualFlow[i])
actualType = fmt.Sprintf("%s", actualFlow[i].typ)
actualPos = actualFlow[i].Position.String()
}
different := ""
if i >= len(expectedFlow) {
different = "+"
} else if i >= len(actualFlow) {
different = "-"
} else if !reflect.DeepEqual(expectedFlow[i], actualFlow[i]) {
different = "x"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", expected, expectedType, expectedPos, actual, actualType, actualPos, different)
}
w.Flush()
t.Errorf("Different flows:\n%s", b.String())
}
func TestValidKeyGroup(t *testing.T) { func TestValidKeyGroup(t *testing.T) {
testFlow(t, "[hello world]", []token{ testFlow(t, "[hello world]", []token{
{Position{1, 1}, tokenLeftBracket, "["}, {Position{1, 1}, tokenLeftBracket, "["},
@@ -298,58 +344,281 @@ func TestKeyEqualArrayBoolsWithComments(t *testing.T) {
}) })
} }
func TestDateRegexp(t *testing.T) {
cases := map[string]string{
"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",
}
for name, value := range cases {
if dateRegexp.FindString(value) == "" {
t.Error("failed date regexp test", name)
}
}
if dateRegexp.FindString("1979-05-27 07:32:00Z") == "" {
t.Error("space delimiter lexing")
}
}
func TestKeyEqualDate(t *testing.T) { func TestKeyEqualDate(t *testing.T) {
t.Run("local date time", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T07:32:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenEOF, ""},
})
})
t.Run("local date time space", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 07:32:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenEOF, ""},
})
})
t.Run("local date time fraction", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00:32:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenEOF, ""},
})
})
t.Run("local date time fraction space", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenEOF, ""},
})
})
t.Run("offset date-time utc", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{ testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="}, {Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T07:32:00Z"}, {Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenTimeOffset, "Z"},
{Position{1, 27}, tokenEOF, ""}, {Position{1, 27}, tokenEOF, ""},
}) })
})
t.Run("offset date-time -07:00", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00:32:00-07:00", []token{ testFlow(t, "foo = 1979-05-27T00:32:00-07:00", []token{
{Position{1, 1}, tokenKey, "foo"}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="}, {Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00-07:00"}, {Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenTimeOffset, "-07:00"},
{Position{1, 32}, tokenEOF, ""}, {Position{1, 32}, tokenEOF, ""},
}) })
})
t.Run("offset date-time fractions -07:00", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00:32:00.999999-07:00", []token{ testFlow(t, "foo = 1979-05-27T00:32:00.999999-07:00", []token{
{Position{1, 1}, tokenKey, "foo"}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="}, {Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00.999999-07:00"}, {Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenTimeOffset, "-07:00"},
{Position{1, 39}, tokenEOF, ""}, {Position{1, 39}, tokenEOF, ""},
}) })
})
t.Run("offset date-time space separated utc", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 07:32:00Z", []token{ testFlow(t, "foo = 1979-05-27 07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"}, {Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="}, {Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27 07:32:00Z"}, {Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenTimeOffset, "Z"},
{Position{1, 27}, tokenEOF, ""}, {Position{1, 27}, tokenEOF, ""},
}) })
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenTimeOffset, "-07:00"},
{Position{1, 32}, tokenEOF, ""},
})
})
t.Run("offset date-time space separated fraction offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00.999999-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenTimeOffset, "-07:00"},
{Position{1, 39}, tokenEOF, ""},
})
})
t.Run("local date", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 17}, tokenEOF, ""},
})
})
t.Run("local time", func(t *testing.T) {
testFlow(t, "foo = 07:32:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalTime, "07:32:00"},
{Position{1, 15}, tokenEOF, ""},
})
})
t.Run("local time fraction", func(t *testing.T) {
testFlow(t, "foo = 00:32:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 22}, tokenEOF, ""},
})
})
t.Run("local time invalid minute digit", func(t *testing.T) {
testFlow(t, "foo = 00:3x:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "invalid minute digit in time: x"},
})
})
t.Run("local time invalid minute/second digit", func(t *testing.T) {
testFlow(t, "foo = 00:30x00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "time minute/second separator should be :, not x"},
})
})
t.Run("local time invalid second digit", func(t *testing.T) {
testFlow(t, "foo = 00:30:x0.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "invalid second digit in time: x"},
})
})
t.Run("local time invalid second digit", func(t *testing.T) {
testFlow(t, "foo = 00:30:00.F", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "expected at least one digit in time's fraction, not F"},
})
})
t.Run("local date-time invalid minute digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:3x:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "invalid minute digit in time: x"},
})
})
t.Run("local date-time invalid hour digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T0x:30:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "invalid hour digit in time: x"},
})
})
t.Run("local date-time invalid hour digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00x30:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "time hour/minute separator should be :, not x"},
})
})
t.Run("local date-time invalid minute/second digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:30x00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "time minute/second separator should be :, not x"},
})
})
t.Run("local date-time invalid second digit", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:30:x0.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "invalid second digit in time: x"},
})
})
t.Run("local date-time invalid fraction", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:30:00.F", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenError, "expected at least one digit in time's fraction, not F"},
})
})
t.Run("local date-time invalid month-date separator", func(t *testing.T) {
testFlow(t, "foo = 1979-05X27 00:30:00.F", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenError, "expected - to separate month of a date, not X"},
})
})
t.Run("local date-time extra whitespace", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 ", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 19}, tokenEOF, ""},
})
})
t.Run("local date-time extra whitespace", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 ", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 22}, tokenEOF, ""},
})
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-0x:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenError, "invalid hour digit in time offset: x"},
})
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-07x00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenError, "time offset hour/minute separator should be :, not x"},
})
})
t.Run("offset date-time space separated offset", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00-07:x0", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00"},
{Position{1, 26}, tokenError, "invalid minute digit in time offset: x"},
})
})
} }
func TestFloatEndingWithDot(t *testing.T) { func TestFloatEndingWithDot(t *testing.T) {
+45 -6
View File
@@ -18,6 +18,7 @@ const (
tagFieldComment = "comment" tagFieldComment = "comment"
tagCommented = "commented" tagCommented = "commented"
tagMultiline = "multiline" tagMultiline = "multiline"
tagLiteral = "literal"
tagDefault = "default" tagDefault = "default"
) )
@@ -27,6 +28,7 @@ type tomlOpts struct {
comment string comment string
commented bool commented bool
multiline bool multiline bool
literal bool
include bool include bool
omitempty bool omitempty bool
defaultValue string defaultValue string
@@ -46,6 +48,7 @@ type annotation struct {
comment string comment string
commented string commented string
multiline string multiline string
literal string
defaultValue string defaultValue string
} }
@@ -54,15 +57,16 @@ var annotationDefault = annotation{
comment: tagFieldComment, comment: tagFieldComment,
commented: tagCommented, commented: tagCommented,
multiline: tagMultiline, multiline: tagMultiline,
literal: tagLiteral,
defaultValue: tagDefault, defaultValue: tagDefault,
} }
type marshalOrder int type MarshalOrder int
// Orders the Encoder can write the fields to the output stream. // Orders the Encoder can write the fields to the output stream.
const ( const (
// Sort fields alphabetically. // Sort fields alphabetically.
OrderAlphabetical marshalOrder = iota + 1 OrderAlphabetical MarshalOrder = iota + 1
// Preserve the order the fields are encountered. For example, the order of fields in // Preserve the order the fields are encountered. For example, the order of fields in
// a struct. // a struct.
OrderPreserve OrderPreserve
@@ -256,8 +260,9 @@ type Encoder struct {
annotation annotation
line int line int
col int col int
order marshalOrder order MarshalOrder
promoteAnon bool promoteAnon bool
compactComments bool
indentation string indentation string
} }
@@ -317,7 +322,7 @@ func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
} }
// Order allows to change in which order fields will be written to the output stream. // Order allows to change in which order fields will be written to the output stream.
func (e *Encoder) Order(ord marshalOrder) *Encoder { func (e *Encoder) Order(ord MarshalOrder) *Encoder {
e.order = ord e.order = ord
return e return e
} }
@@ -365,6 +370,12 @@ func (e *Encoder) PromoteAnonymous(promote bool) *Encoder {
return e return e
} }
// CompactComments removes the new line before each comment in the tree.
func (e *Encoder) CompactComments(cc bool) *Encoder {
e.compactComments = cc
return e
}
func (e *Encoder) marshal(v interface{}) ([]byte, error) { func (e *Encoder) marshal(v interface{}) ([]byte, error) {
// Check if indentation is valid // Check if indentation is valid
for _, char := range e.indentation { for _, char := range e.indentation {
@@ -404,7 +415,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, e.indentation, false) _, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order, e.indentation, e.compactComments, false)
return buf.Bytes(), err return buf.Bytes(), err
} }
@@ -442,6 +453,7 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
Comment: opts.comment, Comment: opts.comment,
Commented: opts.commented, Commented: opts.commented,
Multiline: opts.multiline, Multiline: opts.multiline,
Literal: opts.literal,
}, val) }, val)
} }
} }
@@ -586,6 +598,7 @@ func (e *Encoder) wrapTomlValue(val interface{}, parent *Tree) interface{} {
_, isTree := val.(*Tree) _, isTree := val.(*Tree)
_, isTreeS := val.([]*Tree) _, isTreeS := val.([]*Tree)
if isTree || isTreeS { if isTree || isTreeS {
e.line++
return val return val
} }
@@ -830,7 +843,21 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
case reflect.Int32: case reflect.Int32:
val, err = strconv.ParseInt(opts.defaultValue, 10, 32) val, err = strconv.ParseInt(opts.defaultValue, 10, 32)
case reflect.Int64: case reflect.Int64:
// Check if the provided number has a non-numeric extension.
var hasExtension bool
if len(opts.defaultValue) > 0 {
lastChar := opts.defaultValue[len(opts.defaultValue)-1]
if lastChar < '0' || lastChar > '9' {
hasExtension = true
}
}
// If the value is a time.Duration with extension, parse as duration.
// If the value is an int64 or a time.Duration without extension, parse as number.
if hasExtension && mvalf.Type().String() == "time.Duration" {
val, err = time.ParseDuration(opts.defaultValue)
} else {
val, err = strconv.ParseInt(opts.defaultValue, 10, 64) val, err = strconv.ParseInt(opts.defaultValue, 10, 64)
}
case reflect.Float32: case reflect.Float32:
val, err = strconv.ParseFloat(opts.defaultValue, 32) val, err = strconv.ParseFloat(opts.defaultValue, 32)
case reflect.Float64: case reflect.Float64:
@@ -1004,8 +1031,18 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
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:
d.visitor.visit() d.visitor.visit()
mvalPtr := reflect.New(mtype)
// Check if pointer to value implements the Unmarshaler interface.
if isCustomUnmarshaler(mvalPtr.Type()) {
if err := callCustomUnmarshaler(mvalPtr, tval); err != nil {
return reflect.ValueOf(nil), fmt.Errorf("unmarshal toml: %v", err)
}
return mvalPtr.Elem(), nil
}
// Check if pointer to value implements the encoding.TextUnmarshaler. // Check if pointer to value implements the encoding.TextUnmarshaler.
if mvalPtr := reflect.New(mtype); isTextUnmarshaler(mvalPtr.Type()) && !isTimeType(mtype) { if isTextUnmarshaler(mvalPtr.Type()) && !isTimeType(mtype) {
if err := d.unmarshalText(tval, mvalPtr); err != nil { if err := d.unmarshalText(tval, mvalPtr); err != nil {
return reflect.ValueOf(nil), fmt.Errorf("unmarshal text: %v", err) return reflect.ValueOf(nil), fmt.Errorf("unmarshal text: %v", err)
} }
@@ -1144,6 +1181,7 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
} }
commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented)) commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented))
multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline)) multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline))
literal, _ := strconv.ParseBool(vf.Tag.Get(an.literal))
defaultValue := vf.Tag.Get(tagDefault) defaultValue := vf.Tag.Get(tagDefault)
result := tomlOpts{ result := tomlOpts{
name: vf.Name, name: vf.Name,
@@ -1151,6 +1189,7 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
comment: comment, comment: comment,
commented: commented, commented: commented,
multiline: multiline, multiline: multiline,
literal: literal,
include: true, include: true,
omitempty: false, omitempty: false,
defaultValue: defaultValue, defaultValue: defaultValue,
+143
View File
@@ -3,6 +3,7 @@ package toml
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -1293,6 +1294,32 @@ NonCommented = "Not commented line"
} }
} }
func TestMarshalMultilineLiteral(t *testing.T) {
type Doc struct {
Value string `multiline:"true" literal:"true"`
}
d := Doc{
Value: "hello\nworld\ttest\nend",
}
expected := []byte(`Value = '''
hello
world test
end
'''
`)
b, err := Marshal(d)
if err != nil {
t.Fatal("unexpected error")
}
if !bytes.Equal(b, expected) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, b)
}
}
func TestMarshalNonPrimitiveTypeCommented(t *testing.T) { func TestMarshalNonPrimitiveTypeCommented(t *testing.T) {
expectedToml := []byte(` expectedToml := []byte(`
# [CommentedMapField] # [CommentedMapField]
@@ -1449,6 +1476,47 @@ commented out"""
} }
} }
func TestCompactComments(t *testing.T) {
expected := []byte(`
[first-section]
# comment for first-key
first-key = 1
# comment for second-key
second-key = "value"
# comment for commented third-key
# third-key = []
[second-section]
# comment for first-key
first-key = 2
# comment for second-key
second-key = "another value"
# comment for commented third-key
# third-key = ["value1", "value2"]
`)
type Settings struct {
FirstKey int `toml:"first-key" comment:"comment for first-key"`
SecondKey string `toml:"second-key" comment:"comment for second-key"`
ThirdKey []string `toml:"third-key" comment:"comment for commented third-key" commented:"true"`
}
type Config struct {
FirstSection Settings `toml:"first-section"`
SecondSection Settings `toml:"second-section"`
}
data := Config{
FirstSection: Settings{1, "value", []string{}},
SecondSection: Settings{2, "another value", []string{"value1", "value2"}},
}
buf := new(bytes.Buffer)
if err := NewEncoder(buf).CompactComments(true).Encode(data); err != nil {
t.Fatal(err)
}
if !bytes.Equal(expected, buf.Bytes()) {
t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, buf.Bytes())
}
}
type mapsTestStruct struct { type mapsTestStruct struct {
Simple map[string]string Simple map[string]string
Paths map[string]string Paths map[string]string
@@ -2299,6 +2367,8 @@ func TestUnmarshalDefault(t *testing.T) {
Int64Field int64 `default:"-64"` Int64Field int64 `default:"-64"`
Float32Field float32 `default:"32.1"` Float32Field float32 `default:"32.1"`
Float64Field float64 `default:"64.1"` Float64Field float64 `default:"64.1"`
DurationField time.Duration `default:"120ms"`
DurationField2 time.Duration `default:"120000000"`
NonEmbeddedStruct struct { NonEmbeddedStruct struct {
StringField string `default:"b"` StringField string `default:"b"`
} }
@@ -2352,6 +2422,12 @@ func TestUnmarshalDefault(t *testing.T) {
if doc.Float64Field != 64.1 { if doc.Float64Field != 64.1 {
t.Errorf("Float64Field should be 64.1, not %f", doc.Float64Field) t.Errorf("Float64Field should be 64.1, not %f", doc.Float64Field)
} }
if doc.DurationField != 120*time.Millisecond {
t.Errorf("DurationField should be 120ms, not %s", doc.DurationField.String())
}
if doc.DurationField2 != 120*time.Millisecond {
t.Errorf("DurationField2 should be 120000000, not %d", doc.DurationField2)
}
if doc.NonEmbeddedStruct.StringField != "b" { if doc.NonEmbeddedStruct.StringField != "b" {
t.Errorf("StringField should be \"b\", not %s", doc.NonEmbeddedStruct.StringField) t.Errorf("StringField should be \"b\", not %s", doc.NonEmbeddedStruct.StringField)
} }
@@ -2407,6 +2483,17 @@ func TestUnmarshalDefaultFailureFloat64(t *testing.T) {
} }
} }
func TestUnmarshalDefaultFailureDuration(t *testing.T) {
var doc struct {
Field time.Duration `default:"blah"`
}
err := Unmarshal([]byte(``), &doc)
if err == nil {
t.Fatal("should error")
}
}
func TestUnmarshalDefaultFailureUnsupported(t *testing.T) { func TestUnmarshalDefaultFailureUnsupported(t *testing.T) {
var doc struct { var doc struct {
Field struct{} `default:"blah"` Field struct{} `default:"blah"`
@@ -3976,3 +4063,59 @@ func TestGithubIssue431(t *testing.T) {
t.Errorf("UnmarshalTOML should not have been called") t.Errorf("UnmarshalTOML should not have been called")
} }
} }
type durationString struct {
time.Duration
}
func (d *durationString) UnmarshalTOML(v interface{}) error {
d.Duration = 10 * time.Second
return nil
}
type config437Error struct {
}
func (e *config437Error) UnmarshalTOML(v interface{}) error {
return errors.New("expected")
}
type config437 struct {
HTTP struct {
PingTimeout durationString `toml:"PingTimeout"`
ErrorField config437Error
} `toml:"HTTP"`
}
func TestGithubIssue437(t *testing.T) {
src := `
[HTTP]
PingTimeout = "32m"
`
cfg := &config437{}
cfg.HTTP.PingTimeout = durationString{time.Second}
r := strings.NewReader(src)
err := NewDecoder(r).Decode(cfg)
if err != nil {
t.Fatalf("unexpected errors %s", err)
}
expected := durationString{10 * time.Second}
if cfg.HTTP.PingTimeout != expected {
t.Fatalf("expected '%s', got '%s'", expected, cfg.HTTP.PingTimeout)
}
}
func TestLeafUnmarshalerError(t *testing.T) {
src := `
[HTTP]
ErrorField = "foo"
`
cfg := &config437{}
r := strings.NewReader(src)
err := NewDecoder(r).Decode(cfg)
if err == nil {
t.Fatalf("error was expected")
}
}
+54 -39
View File
@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -231,19 +230,38 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
return p.parseStart return p.parseStart
} }
var numberUnderscoreInvalidRegexp *regexp.Regexp var errInvalidUnderscore = errors.New("invalid use of _ in number")
var hexNumberUnderscoreInvalidRegexp *regexp.Regexp
func numberContainsInvalidUnderscore(value string) error { func numberContainsInvalidUnderscore(value string) error {
if numberUnderscoreInvalidRegexp.MatchString(value) { // For large numbers, you may use underscores between digits to enhance
return errors.New("invalid use of _ in number") // readability. Each underscore must be surrounded by at least one digit on
// each side.
hasBefore := false
for idx, r := range value {
if r == '_' {
if !hasBefore || idx+1 >= len(value) {
// can't end with an underscore
return errInvalidUnderscore
}
}
hasBefore = isDigit(r)
} }
return nil return nil
} }
var errInvalidUnderscoreHex = errors.New("invalid use of _ in hex number")
func hexNumberContainsInvalidUnderscore(value string) error { func hexNumberContainsInvalidUnderscore(value string) error {
if hexNumberUnderscoreInvalidRegexp.MatchString(value) { hasBefore := false
return errors.New("invalid use of _ in hex number") for idx, r := range value {
if r == '_' {
if !hasBefore || idx+1 >= len(value) {
// can't end with an underscore
return errInvalidUnderscoreHex
}
}
hasBefore = isHexDigit(r)
} }
return nil return nil
} }
@@ -322,42 +340,44 @@ func (p *tomlParser) parseRvalue() interface{} {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
return val return val
case tokenDate: case tokenLocalTime:
layout := time.RFC3339Nano val, err := ParseLocalTime(tok.val)
if !strings.Contains(tok.val, "T") {
layout = strings.Replace(layout, "T", " ", 1)
}
val, err := time.ParseInLocation(layout, tok.val, time.UTC)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
return val return val
case tokenLocalDate: case tokenLocalDate:
v := strings.Replace(tok.val, " ", "T", -1) // a local date may be followed by:
isDateTime := false // * nothing: this is a local date
isTime := false // * a local time: this is a local date-time
for _, c := range v {
if c == 'T' || c == 't' { next := p.peek()
isDateTime = true if next == nil || next.typ != tokenLocalTime {
break val, err := ParseLocalDate(tok.val)
} if err != nil {
if c == ':' { p.raiseError(tok, "%s", err)
isTime = true
break
} }
return val
} }
var val interface{} localDate := tok
var err error localTime := p.getToken()
if isDateTime { next = p.peek()
val, err = ParseLocalDateTime(v) if next == nil || next.typ != tokenTimeOffset {
} else if isTime { v := localDate.val + "T" + localTime.val
val, err = ParseLocalTime(v) val, err := ParseLocalDateTime(v)
} else { if err != nil {
val, err = ParseLocalDate(v) p.raiseError(tok, "%s", err)
}
return val
} }
offset := p.getToken()
layout := time.RFC3339Nano
v := localDate.val + "T" + localTime.val + offset.val
val, err := time.ParseInLocation(layout, v, time.UTC)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
@@ -370,10 +390,10 @@ func (p *tomlParser) parseRvalue() interface{} {
p.raiseError(tok, "cannot have multiple equals for the same key") p.raiseError(tok, "cannot have multiple equals for the same key")
case tokenError: case tokenError:
p.raiseError(tok, "%s", tok) p.raiseError(tok, "%s", tok)
default:
panic(fmt.Errorf("unhandled token: %v", tok))
} }
p.raiseError(tok, "never reached")
return nil return nil
} }
@@ -486,8 +506,3 @@ func parseToml(flow []token) *Tree {
parser.run() parser.run()
return result return result
} }
func init() {
numberUnderscoreInvalidRegexp = regexp.MustCompile(`([^\d]_|_[^\d])|_$|^_`)
hexNumberUnderscoreInvalidRegexp = regexp.MustCompile(`(^0x_)|([^\da-f]_|_[^\da-f])|_$|^_`)
}
+29 -3
View File
@@ -6,8 +6,6 @@ import (
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/davecgh/go-spew/spew"
) )
func assertSubTree(t *testing.T, path []string, tree *Tree, err error, ref map[string]interface{}) { func assertSubTree(t *testing.T, path []string, tree *Tree, err error, ref map[string]interface{}) {
@@ -39,7 +37,7 @@ func assertSubTree(t *testing.T, path []string, tree *Tree, err error, ref map[s
} }
func assertTree(t *testing.T, tree *Tree, err error, ref map[string]interface{}) { func assertTree(t *testing.T, tree *Tree, err error, ref map[string]interface{}) {
t.Log("Asserting tree:\n", spew.Sdump(tree)) t.Logf("Asserting tree:\n (%T)(%p)(%+v)", tree, tree, tree)
assertSubTree(t, []string{}, tree, err, ref) assertSubTree(t, []string{}, tree, err, ref)
t.Log("Finished tree assertion.") t.Log("Finished tree assertion.")
} }
@@ -274,6 +272,34 @@ func TestLocalDate(t *testing.T) {
}) })
} }
func TestLocalDateError(t *testing.T) {
_, err := Load("a = 2020-09-31")
if err == nil {
t.Fatalf("should error")
}
}
func TestLocalTimeError(t *testing.T) {
_, err := Load("a = 07:99:00")
if err == nil {
t.Fatalf("should error")
}
}
func TestLocalDateTimeError(t *testing.T) {
_, err := Load("a = 2020-09-31T07:99:00")
if err == nil {
t.Fatalf("should error")
}
}
func TestDateTimeOffsetError(t *testing.T) {
_, err := Load("a = 2020-09-31T07:99:00Z")
if err == nil {
t.Fatalf("should error")
}
}
func TestLocalTime(t *testing.T) { func TestLocalTime(t *testing.T) {
tree, err := Load("a = 07:32:00") tree, err := Load("a = 07:32:00")
assertTree(t, tree, err, map[string]interface{}{ assertTree(t, tree, err, map[string]interface{}{
+4 -2
View File
@@ -30,8 +30,9 @@ const (
tokenRightParen tokenRightParen
tokenDoubleLeftBracket tokenDoubleLeftBracket
tokenDoubleRightBracket tokenDoubleRightBracket
tokenDate
tokenLocalDate tokenLocalDate
tokenLocalTime
tokenTimeOffset
tokenKeyGroup tokenKeyGroup
tokenKeyGroupArray tokenKeyGroupArray
tokenComma tokenComma
@@ -66,7 +67,8 @@ var tokenTypeNames = []string{
"]]", "]]",
"[[", "[[",
"LocalDate", "LocalDate",
"LocalDate", "LocalTime",
"TimeOffset",
"KeyGroup", "KeyGroup",
"KeyGroupArray", "KeyGroupArray",
",", ",",
+2 -1
View File
@@ -25,8 +25,9 @@ func TestTokenStringer(t *testing.T) {
{tokenRightParen, ")"}, {tokenRightParen, ")"},
{tokenDoubleLeftBracket, "]]"}, {tokenDoubleLeftBracket, "]]"},
{tokenDoubleRightBracket, "[["}, {tokenDoubleRightBracket, "[["},
{tokenDate, "LocalDate"},
{tokenLocalDate, "LocalDate"}, {tokenLocalDate, "LocalDate"},
{tokenLocalTime, "LocalTime"},
{tokenTimeOffset, "TimeOffset"},
{tokenKeyGroup, "KeyGroup"}, {tokenKeyGroup, "KeyGroup"},
{tokenKeyGroupArray, "KeyGroupArray"}, {tokenKeyGroupArray, "KeyGroupArray"},
{tokenComma, ","}, {tokenComma, ","},
+4
View File
@@ -15,6 +15,7 @@ type tomlValue struct {
comment string comment string
commented bool commented bool
multiline bool multiline bool
literal bool
position Position position Position
} }
@@ -314,6 +315,7 @@ type SetOptions struct {
Comment string Comment string
Commented bool Commented bool
Multiline bool Multiline bool
Literal bool
} }
// SetWithOptions is the same as Set, but allows you to provide formatting // SetWithOptions is the same as Set, but allows you to provide formatting
@@ -362,12 +364,14 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
v.comment = opts.Comment v.comment = opts.Comment
v.commented = opts.Commented v.commented = opts.Commented
v.multiline = opts.Multiline v.multiline = opts.Multiline
v.literal = opts.Literal
toInsert = v toInsert = v
default: default:
toInsert = &tomlValue{value: value, toInsert = &tomlValue{value: value,
comment: opts.Comment, comment: opts.Comment,
commented: opts.Commented, commented: opts.Commented,
multiline: opts.Multiline, multiline: opts.Multiline,
literal: opts.Literal,
position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}} position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}}
} }
+1 -3
View File
@@ -8,8 +8,6 @@ import (
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/davecgh/go-spew/spew"
) )
func testgenInvalid(t *testing.T, input string) { func testgenInvalid(t *testing.T, input string) {
@@ -56,7 +54,7 @@ func testgenValid(t *testing.T, input string, jsonRef string) {
} }
if !reflect.DeepEqual(jsonExpected, jsonTest) { if !reflect.DeepEqual(jsonExpected, jsonTest) {
t.Logf("Diff:\n%s", spew.Sdump(jsonExpected, jsonTest)) t.Logf("Diff:\n%#+v\n%#+v", jsonExpected, jsonTest)
t.Fatal("parsed TOML tree is different than expected structure") t.Fatal("parsed TOML tree is different than expected structure")
} }
} }
+71
View File
@@ -0,0 +1,71 @@
package toml
// PubTOMLValue wrapping tomlValue in order to access all properties from outside.
type PubTOMLValue = tomlValue
func (ptv *PubTOMLValue) Value() interface{} {
return ptv.value
}
func (ptv *PubTOMLValue) Comment() string {
return ptv.comment
}
func (ptv *PubTOMLValue) Commented() bool {
return ptv.commented
}
func (ptv *PubTOMLValue) Multiline() bool {
return ptv.multiline
}
func (ptv *PubTOMLValue) Position() Position {
return ptv.position
}
func (ptv *PubTOMLValue) SetValue(v interface{}) {
ptv.value = v
}
func (ptv *PubTOMLValue) SetComment(s string) {
ptv.comment = s
}
func (ptv *PubTOMLValue) SetCommented(c bool) {
ptv.commented = c
}
func (ptv *PubTOMLValue) SetMultiline(m bool) {
ptv.multiline = m
}
func (ptv *PubTOMLValue) SetPosition(p Position) {
ptv.position = p
}
// PubTree wrapping Tree in order to access all properties from outside.
type PubTree = Tree
func (pt *PubTree) Values() map[string]interface{} {
return pt.values
}
func (pt *PubTree) Comment() string {
return pt.comment
}
func (pt *PubTree) Commented() bool {
return pt.commented
}
func (pt *PubTree) Inline() bool {
return pt.inline
}
func (pt *PubTree) SetValues(v map[string]interface{}) {
pt.values = v
}
func (pt *PubTree) SetComment(c string) {
pt.comment = c
}
func (pt *PubTree) SetCommented(c bool) {
pt.commented = c
}
func (pt *PubTree) SetInline(i bool) {
pt.inline = i
}
+44 -9
View File
@@ -103,7 +103,7 @@ func encodeTomlString(value string) string {
return b.String() return b.String()
} }
func tomlTreeStringRepresentation(t *Tree, ord marshalOrder) (string, error) { func tomlTreeStringRepresentation(t *Tree, ord MarshalOrder) (string, error) {
var orderedVals []sortNode var orderedVals []sortNode
switch ord { switch ord {
case OrderPreserve: case OrderPreserve:
@@ -126,7 +126,7 @@ func tomlTreeStringRepresentation(t *Tree, ord marshalOrder) (string, error) {
return "{ " + strings.Join(values, ", ") + " }", nil return "{ " + strings.Join(values, ", ") + " }", nil
} }
func tomlValueStringRepresentation(v interface{}, commented string, indent string, ord marshalOrder, arraysOneElementPerLine bool) (string, error) { func tomlValueStringRepresentation(v interface{}, commented string, indent string, ord MarshalOrder, 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)
@@ -158,8 +158,16 @@ func tomlValueStringRepresentation(v interface{}, commented string, indent strin
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 {
if tv.literal {
b := strings.Builder{}
b.WriteString("'''\n")
b.Write([]byte(value))
b.WriteString("\n'''")
return b.String(), nil
} else {
return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", 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)
@@ -218,7 +226,9 @@ func tomlValueStringRepresentation(v interface{}, commented string, indent strin
} }
func getTreeArrayLine(trees []*Tree) (line int) { func getTreeArrayLine(trees []*Tree) (line int) {
// get lowest line number that is not 0 // Prevent returning 0 for empty trees
line = int(^uint(0) >> 1)
// get lowest line number >= 0
for _, tv := range trees { for _, tv := range trees {
if tv.position.Line < line || line == 0 { if tv.position.Line < line || line == 0 {
line = tv.position.Line line = tv.position.Line
@@ -307,10 +317,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, " ", false) return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical, " ", false, false)
} }
func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder, indentString string, parentCommented bool) (int64, error) { func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord MarshalOrder, indentString string, compactComments, parentCommented bool) (int64, error) {
var orderedVals []sortNode var orderedVals []sortNode
switch ord { switch ord {
@@ -360,7 +370,7 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || tv.commented) bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || tv.commented)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
@@ -376,7 +386,7 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
return bytesCount, err return bytesCount, err
} }
bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || subTree.commented) bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || subTree.commented)
if err != nil { if err != nil {
return bytesCount, err return bytesCount, err
} }
@@ -404,7 +414,14 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
if strings.HasPrefix(comment, "#") { if strings.HasPrefix(comment, "#") {
start = "" start = ""
} }
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment, "\n") if !compactComments {
writtenBytesCountComment, errc := writeStrings(w, "\n")
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
writtenBytesCountComment, errc := writeStrings(w, indent, start, comment, "\n")
bytesCount += int64(writtenBytesCountComment) bytesCount += int64(writtenBytesCountComment)
if errc != nil { if errc != nil {
return bytesCount, errc return bytesCount, errc
@@ -510,8 +527,26 @@ func (t *Tree) ToMap() map[string]interface{} {
case *Tree: case *Tree:
result[k] = node.ToMap() result[k] = node.ToMap()
case *tomlValue: case *tomlValue:
result[k] = node.value result[k] = tomlValueToGo(node.value)
} }
} }
return result return result
} }
func tomlValueToGo(v interface{}) interface{} {
if tree, ok := v.(*Tree); ok {
return tree.ToMap()
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Slice {
return v
}
values := make([]interface{}, rv.Len())
for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface()
values[i] = tomlValueToGo(item)
}
return values
}
+85
View File
@@ -295,6 +295,42 @@ func TestTreeWriteToMapWithArrayOfInlineTables(t *testing.T) {
testMaps(t, treeMap, expected) testMaps(t, treeMap, expected)
} }
func TestTreeWriteToMapWithTableInMixedArray(t *testing.T) {
tree, _ := Load(`a = [
"foo",
[
"bar",
{baz = "quux"},
],
[
{a = "b"},
{c = "d"},
],
]`)
expected := map[string]interface{}{
"a": []interface{}{
"foo",
[]interface{}{
"bar",
map[string]interface{}{
"baz": "quux",
},
},
[]interface{}{
map[string]interface{}{
"a": "b",
},
map[string]interface{}{
"c": "d",
},
},
},
}
treeMap := tree.ToMap()
testMaps(t, treeMap, expected)
}
func TestTreeWriteToFloat(t *testing.T) { func TestTreeWriteToFloat(t *testing.T) {
tree, err := Load(`a = 3.0`) tree, err := Load(`a = 3.0`)
if err != nil { if err != nil {
@@ -328,6 +364,55 @@ c = nan`
} }
} }
func TestOrderedEmptyTrees(t *testing.T) {
type val struct {
Key string `toml:"key"`
}
type structure struct {
First val `toml:"first"`
Empty []val `toml:"empty"`
}
input := structure{First: val{Key: "value"}}
buf := new(bytes.Buffer)
err := NewEncoder(buf).Order(OrderPreserve).Encode(input)
if err != nil {
t.Fatal("failed to encode input")
}
expected := `
[first]
key = "value"
`
if expected != buf.String() {
t.Fatal("expected and encoded body aren't equal: ", expected, buf.String())
}
}
func TestOrderedNonIncreasedLine(t *testing.T) {
type NiceMap map[string]string
type Manifest struct {
NiceMap `toml:"dependencies"`
Build struct {
BuildCommand string `toml:"build-command"`
} `toml:"build"`
}
test := &Manifest{}
test.Build.BuildCommand = "test"
buf := new(bytes.Buffer)
if err := NewEncoder(buf).Order(OrderPreserve).Encode(test); err != nil {
panic(err)
}
expected := `
[dependencies]
[build]
build-command = "test"
`
if expected != buf.String() {
t.Fatal("expected and encoded body aren't equal: ", expected, buf.String())
}
}
func TestIssue290(t *testing.T) { func TestIssue290(t *testing.T) {
tomlString := tomlString :=
`[table] `[table]
+6
View File
@@ -0,0 +1,6 @@
package toml
// ValueStringRepresentation transforms an interface{} value into its toml string representation.
func ValueStringRepresentation(v interface{}, commented string, indent string, ord MarshalOrder, arraysOneElementPerLine bool) (string, error) {
return tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine)
}