Compare commits

...

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

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

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

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

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

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

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

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

Benchmarks were run from the benchmark/ directory using:

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

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