Compare commits

...

96 Commits

Author SHA1 Message Date
Thomas Pelletier 8e44986c28 Configure issues templates
Ask people to go to /discussions for anything that is not a bug report.
2021-06-14 10:56:19 -04:00
Thomas Pelletier 837c1d09ee Clarify license and comply with Apache 2.0 (#554) 2021-06-08 13:57:30 -04:00
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
Thomas Pelletier 16c9a8bdc0 Mention support to v1.0.0-rc.1 2020-05-17 17:56:44 -04:00
x-hgg-x f99d6bbca1 Fix query test
- replaced  assertArrayContainsInAnyOrder with a version which tests the exact order for more strict testing
2020-05-17 20:42:51 +08:00
Jim Tittsler 8784f9c73a Fix typos 2020-05-17 20:41:16 +08:00
x-hgg-x a60e466129 Fix index and slice expressions for query (#405)
* Fix index and slice expressions for query

Support negative step for slice expressions
2020-05-14 14:21:51 +08:00
dependabot-preview[bot] 44aed552fd Bump gopkg.in/yaml.v2 from 2.2.8 to 2.3.0 (#408) 2020-05-14 06:20:36 +00:00
AllenX2018 1479e10663 fix issue #406 2020-05-08 18:43:08 +08:00
x-hgg-x 9ba7363552 Allow spaces when using dotted keys in assignment (#402)
Fixes #401
2020-05-07 08:11:29 -04:00
x-hgg-x 96ff402934 Fix marshaling nested arrays of tables (#395)
Fixes #369
2020-05-07 08:09:23 -04:00
x-hgg-x 249d0eaf46 Restore test for accidental whitespaces (#403) 2020-05-06 23:15:34 -04:00
x-hgg-x 19eb8cf036 Fix various quoted keys bugs (#400)
Fixes #396 #397 #398 #399
2020-05-06 23:13:18 -04:00
Allen c5fbd3eba6 Add support of mixed-type array (#376)
Fixes #357
2020-05-06 23:07:57 -04:00
x-hgg-x 9ccd9bbc7a Fix unmarshaler error when a custom marshaler function is defined (#383)
Fixes #382
2020-05-04 15:05:45 -04:00
x-hgg-x e7d1a179ae Support custom unmarshaler (#394)
Co-authored-by: Thomas Pelletier <pelletier.thomas@gmail.com>
2020-05-04 13:33:55 -04:00
x-hgg-x 71a8bd4c61 Prevent automatic conversion between int and float when unmarshaling (#390)
Fixes #389
Co-authored-by: Thomas Pelletier <pelletier.thomas@gmail.com>
2020-05-04 13:30:08 -04:00
x-hgg-x 34782191ba Add more supported default values types for unmarshaling (#392)
Fixes #391
2020-05-04 13:26:07 -04:00
x-hgg-x 7fbde32684 Fix overflow checking when unmarshaling (#388)
Fixes #387
2020-05-04 13:22:43 -04:00
x-hgg-x 82a6a1977d Add indentation setting for Encoder (#386)
Fixes #371
2020-05-04 13:21:22 -04:00
x-hgg-x cc3100c329 Fix unmarshaling arrays (#385)
Fixes #384
2020-05-04 13:19:19 -04:00
x-hgg-x f1ba6388fb Fix inline table loading errors (#381)
Fixes #379 #380
2020-05-04 13:13:55 -04:00
Allen d05497900e Forbid adding keys to exist inline table (#378) 2020-05-04 13:06:37 -04:00
Oncilla e29a498ed5 unmarshal: support encoding.TextUnmarshaler (#375)
* unmarshal: support encoding.TextUnmarshaler

This PR adds support for decoding fields of primitive types that implement
encoding.TextUnmarshaler by calling the custom method.

Fields in anonymous structs are not supported at this point.

Co-authored-by: Lorenz Bauer <lmb@users.noreply.github.com>
2020-05-04 12:49:37 -04:00
Oncilla 2b8e33f503 marshal: support encoding.TextMarshaler (#374)
With this PR the encoder now supports encoding.TextMarshaler.
Additionally, a bug is fixed, where the encoder does not notice a pointer
field that implements the toml.Marshaler interface.

fixes #373
2020-04-28 07:29:00 -04:00
Oncilla d3c92c5999 unmarshal: add strict mode (#372)
This PR adds a strict mode to the Decoder. It can be enabled with the
`Strict` method.

In the strict mode, the decoder fails if any fields that were part
of the input do not have a corresponding field in the struct.

Fixes #277
2020-04-28 07:24:56 -04:00
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
Oncilla d1e0fc37ce marshal: do not encode embedded structs as sub-table (#368)
Currently, the marshalling code encodes the embedded structs as sub-tables.
This is a bit unexpected, as it differs from what encoding/json does in
that case: https://play.golang.org/p/KDPaGtrijV1

Unmarshalling code handles this scenario gracefully.

This PR adapts the encoder to behave like encoding/json.
Fields in an embedded struct are promoted to the top level table.
In case the embedded struct is named in the tag, it will still
encode as a sub-table.

The added PromoteAnonymous option on the Encoder allows configuring
the old behavior, where anonymous structs are encoded as sub-tables.

On duplicate keys, the behavior of encoding/json is mimicked:
Fields from anonymous structs are shadowed by regular fields.

An example is added to show the affects of setting PromoteAnonymous.
2020-04-25 11:25:56 -04:00
Allen 947ab3f90a add test for trailing comma in inline table (#366)
Fixes #359
2020-04-24 21:43:46 -04:00
Allen e9e8265313 Add support for tab in basic string value and quoted key (#364) 2020-04-24 21:41:25 -04:00
Allen a30fd2239c Escape adjacent quotation marks marshaling in multiline string (#365) 2020-04-24 21:38:04 -04:00
jixiuf 323fe5d063 fix #356 Unmarshal support []string ,[]int ... (#361)
* fix #356 Unmarshal support []string ,[]int ...

* try make codecov happy.
2020-04-21 22:48:12 -04:00
Riya John 24d4446802 Add float to test case to check leading zeroes in exponent parts (#363)
* add float to test case to check leading zeroes in exponent parts

* add testcase for query pkg
2020-04-21 22:45:49 -04:00
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
Allen e872682c78 Fix unmarshal error with tab in multi-line basic string (#355)
Fixes #354
2020-04-15 08:46:56 -04:00
Allen 145b18309a dont't panic when marshal from nil or unmarshal to nil interface or pointer (#353) 2020-04-15 08:41:18 -04:00
Allen 8e8d2a6aad Support unmarshal into toml.Tree (#347)
Fixes #333
2020-04-03 07:10:45 -04:00
Thomas Pelletier 3f7178ffd6 CI computes manifest for built binaries (#346) 2020-03-30 11:59:45 -04:00
Thomas Pelletier 9fd5922321 Empty commit to trigger release 2020-03-30 10:59:59 -04:00
Thomas Pelletier 610cf85ed6 CI Build Binaries (#345) 2020-03-30 10:51:26 -04:00
Thomas Pelletier 99f8a2a010 Fix codecov on pull requests (#344) 2020-03-25 13:21:25 -04:00
Thomas Pelletier 556d384d4c Bump go version to 1.14 and 1.13 (#343) 2020-03-25 12:52:59 -04:00
Allen eb7280e4a7 Add interface{} support (#341) 2020-03-25 11:12:18 -04:00
Allen 7ee1118b4b Fix unmarshaling of nested structs (#340)
Fixes #339
2020-03-23 11:23:21 -04:00
Allen a12e102214 Fix multiline + non-primitive commenting (#336)
Fixes #216
2020-03-16 22:51:47 -04:00
jinleiw ad60b7e437 Add support for nested interface{} unmarshal (#335)
Co-authored-by: jlwang <jlwang@sysnew.com>

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

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

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-20 21:21:26 -05:00
dependabot-preview[bot] c4efb7477c Bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.5 (#322) 2019-11-06 15:49:39 +00:00
52 changed files with 5411 additions and 780 deletions
+11 -2
View File
@@ -1,9 +1,18 @@
---
name: Bug report
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**
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".
**Versions**
- go-toml: version (git sha)
- go-toml: version (or git sha)
- go: version
- operating system: e.g. macOS, Windows, Linux
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions and discussions
url: https://github.com/pelletier/go-toml/discussions
about: Please ask and answer questions here.
-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!
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
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
@@ -123,7 +123,7 @@ Checklist:
[issues-tracker]: https://github.com/pelletier/go-toml/issues
[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
[fork]: https://help.github.com/articles/fork-a-repo
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request
+227 -1
View File
@@ -1,6 +1,16 @@
The bulk of github.com/pelletier/go-toml is distributed under the MIT license
(see below), with the exception of localtime.go and localtime.test.go.
Those two files have been copied over from Google's civil library at revision
ed46f5086358513cf8c25f8e3f022cb838a49d66, and are distributed under the Apache
2.0 license (see below).
github.com/pelletier/go-toml:
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
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +29,219 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
localtime.go, localtime_test.go:
Originals:
https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil.go
https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil_test.go
Changes:
* Renamed files from civil* to localtime*.
* Package changed from civil to toml.
* 'Local' prefix added to all structs.
License:
https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/LICENSE
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+29
View File
@@ -0,0 +1,29 @@
export CGO_ENABLED=0
go := go
go.goos ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f1)
go.goarch ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f2)
out.tools := tomll tomljson jsontoml
out.dist := $(out.tools:=_$(go.goos)_$(go.goarch).tar.xz)
sources := $(wildcard **/*.go)
.PHONY:
tools: $(out.tools)
$(out.tools): $(sources)
GOOS=$(go.goos) GOARCH=$(go.goarch) $(go) build ./cmd/$@
.PHONY:
dist: $(out.dist)
$(out.dist):%_$(go.goos)_$(go.goarch).tar.xz: %
if [ "$(go.goos)" = "windows" ]; then \
tar -cJf $@ $^.exe; \
else \
tar -cJf $@ $^; \
fi
.PHONY:
clean:
rm -rf $(out.tools) $(out.dist)
+37 -12
View File
@@ -1,24 +1,49 @@
# 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
[v0.5.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.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)
[![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)
[![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)
## 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
Go-toml provides the following features for using data parsed from TOML documents:
* Load TOML documents from files and string data
* Easily navigate TOML structure using Tree
* Mashaling and unmarshaling to and from data structures
* Marshaling and unmarshaling to and from data structures
* Line & column position data for all parsed elements
* [Query support similar to JSON-Path](query/)
* Syntax errors contain line and column numbers
@@ -74,20 +99,20 @@ Or use a query:
q, _ := query.Compile("$..[user,password]")
results := q.Execute(config)
for ii, item := range results.Values() {
fmt.Println("Query result %d: %v", ii, item)
fmt.Printf("Query result %d: %v\n", ii, item)
}
```
## Documentation
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
Go-toml provides two handy command line tools:
Go-toml provides three handy command line tools:
* `tomll`: Reads TOML files and lint them.
* `tomll`: Reads TOML files and lints them.
```
go install github.com/pelletier/go-toml/cmd/tomll
@@ -99,9 +124,9 @@ Go-toml provides two handy command line tools:
go install github.com/pelletier/go-toml/cmd/tomljson
tomljson --help
```
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
```
go install github.com/pelletier/go-toml/cmd/jsontoml
jsontoml --help
@@ -109,7 +134,7 @@ Go-toml provides two handy command line tools:
### 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
use `tomljson`:
@@ -148,4 +173,4 @@ this document. The last two major versions of Go are supported
## License
The MIT License (MIT). Read [LICENSE](LICENSE).
The MIT License (MIT) + Apache 2.0. Read [LICENSE](LICENSE).
+87 -66
View File
@@ -2,30 +2,6 @@ trigger:
- master
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.13"
inputs:
version: "1.13"
- 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
displayName: "Check"
dependsOn: []
@@ -36,9 +12,9 @@ stages:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
displayName: "Install Go 1.16"
inputs:
version: "1.13"
version: "1.16"
- task: Go@0
displayName: "go fmt ./..."
inputs:
@@ -51,9 +27,9 @@ stages:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
displayName: "Install Go 1.16"
inputs:
version: "1.13"
version: "1.16"
- task: Go@0
displayName: "Generate coverage"
inputs:
@@ -62,62 +38,46 @@ stages:
- task: Bash@3
inputs:
targetType: 'inline'
script: 'bash <(curl -s https://codecov.io/bash) -t $(CODECOV_TOKEN)'
script: 'bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}'
env:
CODECOV_TOKEN: $(CODECOV_TOKEN)
- job: benchmark
displayName: "benchmark"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
displayName: "Install Go 1.16"
inputs:
version: "1.13"
version: "1.16"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- task: Bash@3
inputs:
filePath: './benchmark.sh'
arguments: "master $(Build.Repository.Uri)"
- job: fuzzing
displayName: "fuzzing"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- 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
displayName: "unit tests"
strategy:
matrix:
linux 1.13:
goVersion: '1.13'
linux 1.16:
goVersion: '1.16'
imageName: 'ubuntu-latest'
mac 1.13:
goVersion: '1.13'
imageName: 'macos-10.13'
windows 1.13:
goVersion: '1.13'
imageName: 'vs2017-win2016'
linux 1.12:
goVersion: '1.12'
mac 1.16:
goVersion: '1.16'
imageName: 'macOS-latest'
windows 1.16:
goVersion: '1.16'
imageName: 'windows-latest'
linux 1.15:
goVersion: '1.15'
imageName: 'ubuntu-latest'
mac 1.12:
goVersion: '1.12'
imageName: 'macos-10.13'
windows 1.12:
goVersion: '1.12'
imageName: 'vs2017-win2016'
mac 1.15:
goVersion: '1.15'
imageName: 'macOS-latest'
windows 1.15:
goVersion: '1.15'
imageName: 'windows-latest'
pool:
vmImage: $(imageName)
steps:
@@ -130,6 +90,67 @@ stages:
inputs:
command: 'test'
arguments: './...'
- stage: build_binaries
displayName: "Build binaries"
dependsOn: run_checks
jobs:
- job: build_binary
displayName: "Build binary"
strategy:
matrix:
linux_amd64:
GOOS: linux
GOARCH: amd64
darwin_amd64:
GOOS: darwin
GOARCH: amd64
windows_amd64:
GOOS: windows
GOARCH: amd64
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go"
inputs:
version: 1.16
- task: Bash@3
inputs:
targetType: inline
script: "make dist"
env:
go.goos: $(GOOS)
go.goarch: $(GOARCH)
- task: CopyFiles@2
inputs:
sourceFolder: '$(Build.SourcesDirectory)'
contents: '*.tar.xz'
TargetFolder: '$(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: binaries
- stage: build_binaries_manifest
displayName: "Build binaries manifest"
dependsOn: build_binaries
jobs:
- job: build_manifest
displayName: "Build binaries manifest"
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'binaries'
downloadPath: '$(Build.SourcesDirectory)'
- task: Bash@3
inputs:
targetType: inline
script: "cd binaries && sha256sum --binary *.tar.xz | tee $(Build.ArtifactStagingDirectory)/sha256sums.txt"
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: manifest
- stage: build_docker_image
displayName: "Build Docker image"
@@ -164,4 +185,4 @@ stages:
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
buildContext: '.'
tags: 'latest'
tags: 'latest'
+4
View File
@@ -20,11 +20,15 @@ git clone ${reference_git} ${ref_tempdir} >/dev/null 2>/dev/null
pushd ${ref_tempdir} >/dev/null
git checkout ${reference_ref} >/dev/null 2>/dev/null
go test -bench=. -benchmem | tee ${ref_benchmark}
cd benchmark
go test -bench=. -benchmem | tee -a ${ref_benchmark}
popd >/dev/null
echo ""
echo "=== local"
go test -bench=. -benchmem | tee ${local_benchmark}
cd benchmark
go test -bench=. -benchmem | tee -a ${local_benchmark}
echo ""
echo "=== diff"
@@ -1,4 +1,4 @@
package toml
package benchmark
import (
"bytes"
@@ -8,7 +8,8 @@ import (
"time"
burntsushi "github.com/BurntSushi/toml"
yaml "gopkg.in/yaml.v2"
"github.com/pelletier/go-toml"
"gopkg.in/yaml.v2"
)
type benchmarkDoc struct {
@@ -124,7 +125,7 @@ func BenchmarkParseToml(b *testing.B) {
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := LoadReader(bytes.NewReader(fileBytes))
_, err := toml.LoadReader(bytes.NewReader(fileBytes))
if err != nil {
b.Fatal(err)
}
@@ -136,10 +137,11 @@ func BenchmarkUnmarshalToml(b *testing.B) {
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
target := benchmarkDoc{}
err := Unmarshal(bytes, &target)
err := toml.Unmarshal(bytes, &target)
if err != nil {
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=
+64
View File
@@ -5,6 +5,7 @@ package toml_test
import (
"fmt"
"log"
"os"
toml "github.com/pelletier/go-toml"
)
@@ -104,3 +105,66 @@ func ExampleUnmarshal() {
// Output:
// user= pelletier
}
func ExampleEncoder_anonymous() {
type Credentials struct {
User string `toml:"user"`
Password string `toml:"password"`
}
type Protocol struct {
Name string `toml:"name"`
}
type Config struct {
Version int `toml:"version"`
Credentials
Protocol `toml:"Protocol"`
}
config := Config{
Version: 2,
Credentials: Credentials{
User: "pelletier",
Password: "mypassword",
},
Protocol: Protocol{
Name: "tcp",
},
}
fmt.Println("Default:")
fmt.Println("---------------")
def := toml.NewEncoder(os.Stdout)
if err := def.Encode(config); err != nil {
log.Fatal(err)
}
fmt.Println("---------------")
fmt.Println("With promotion:")
fmt.Println("---------------")
prom := toml.NewEncoder(os.Stdout).PromoteAnonymous(true)
if err := prom.Encode(config); err != nil {
log.Fatal(err)
}
// Output:
// Default:
// ---------------
// password = "mypassword"
// user = "pelletier"
// version = 2
//
// [Protocol]
// name = "tcp"
// ---------------
// With promotion:
// ---------------
// version = 2
//
// [Credentials]
// password = "mypassword"
// user = "pelletier"
//
// [Protocol]
// name = "tcp"
}
+1
View File
@@ -27,3 +27,4 @@ enabled = true
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported
+1
View File
@@ -27,3 +27,4 @@ enabled = true
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported
-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
go 1.12
require (
github.com/BurntSushi/toml v0.3.1
github.com/davecgh/go-spew v1.1.1
gopkg.in/yaml.v2 v2.2.4
)
-11
View File
@@ -1,11 +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=
+1 -2
View File
@@ -5,7 +5,6 @@ package toml
import (
"errors"
"fmt"
"unicode"
)
// Convert the bare key group string to an array.
@@ -109,5 +108,5 @@ func parseKey(key string) ([]string, error) {
}
func isValidBareChar(r rune) bool {
return isAlphanumeric(r) || r == '-' || unicode.IsNumber(r)
return isAlphanumeric(r) || r == '-' || isDigit(r)
}
+341 -90
View File
@@ -9,13 +9,10 @@ import (
"bytes"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
var dateRegexp *regexp.Regexp
// Define state functions
type tomlLexStateFn func() tomlLexStateFn
@@ -26,7 +23,7 @@ type tomlLexer struct {
currentTokenStart int
currentTokenStop int
tokens []token
depth int
brackets []rune
line int
col int
endbufferLine int
@@ -123,6 +120,8 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
for {
next := l.peek()
switch next {
case '}': // after '{'
return l.lexRightCurlyBrace
case '[':
return l.lexTableKey
case '#':
@@ -140,10 +139,6 @@ func (l *tomlLexer) lexVoid() tomlLexStateFn {
l.skip()
}
if l.depth > 0 {
return l.lexRvalue
}
if isKeyStartChar(next) {
return l.lexKey
}
@@ -167,10 +162,8 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
case '=':
return l.lexEqual
case '[':
l.depth++
return l.lexLeftBracket
case ']':
l.depth--
return l.lexRightBracket
case '{':
return l.lexLeftCurlyBrace
@@ -188,12 +181,10 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
fallthrough
case '\n':
l.skip()
if l.depth == 0 {
return l.lexVoid
if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '[' {
return l.lexRvalue
}
return l.lexRvalue
case '_':
return l.errorf("cannot start number with underscore")
return l.lexVoid
}
if l.follow("true") {
@@ -222,22 +213,12 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
break
}
possibleDate := l.peekString(35)
dateSubmatches := dateRegexp.FindStringSubmatch(possibleDate)
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 next == '+' || next == '-' {
return l.lexNumber
}
if isAlphanumeric(next) {
return l.lexKey
if isDigit(next) {
return l.lexDateTimeOrNumber
}
return l.errorf("no value can start with %c", next)
@@ -247,28 +228,290 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
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 {
l.next()
l.emit(tokenLeftCurlyBrace)
l.brackets = append(l.brackets, '{')
return l.lexVoid
}
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
l.next()
l.emit(tokenRightCurlyBrace)
return l.lexVoid
}
func (l *tomlLexer) lexDate() tomlLexStateFn {
l.emit(tokenDate)
if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '{' {
return l.errorf("cannot have '}' here")
}
l.brackets = l.brackets[:len(l.brackets)-1]
return l.lexRvalue
}
func (l *tomlLexer) lexLocalDate() tomlLexStateFn {
func (l *tomlLexer) lexDateTimeOrTime() tomlLexStateFn {
// 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
}
func (l *tomlLexer) lexTime() tomlLexStateFn {
// v--- cursor
// 07:32:00
// 00:32:00.999999
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid minute digit in time: %c", r)
}
}
r := l.next()
if r != ':' {
return l.errorf("time minute/second separator should be :, not %c", r)
}
for i := 0; i < 2; i++ {
r := l.next()
if !isDigit(r) {
return l.errorf("invalid second digit in time: %c", r)
}
}
r = l.peek()
if r == '.' {
l.next()
r := l.next()
if !isDigit(r) {
return l.errorf("expected at least one digit in time's fraction, not %c", r)
}
for {
r := l.peek()
if !isDigit(r) {
break
}
l.next()
}
}
l.emit(tokenLocalTime)
return l.lexRvalue
}
func (l *tomlLexer) lexTrue() tomlLexStateFn {
l.fastForward(4)
l.emit(tokenTrue)
@@ -302,13 +545,16 @@ func (l *tomlLexer) lexEqual() tomlLexStateFn {
func (l *tomlLexer) lexComma() tomlLexStateFn {
l.next()
l.emit(tokenComma)
if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '{' {
return l.lexVoid
}
return l.lexRvalue
}
// Parse the key and emits its value without escape sequences.
// bare keys, basic string keys and literal string keys are supported.
func (l *tomlLexer) lexKey() tomlLexStateFn {
growingString := ""
var sb strings.Builder
for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() {
if r == '"' {
@@ -317,7 +563,9 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil {
return l.errorf(err.Error())
}
growingString += "\"" + str + "\""
sb.WriteString("\"")
sb.WriteString(str)
sb.WriteString("\"")
l.next()
continue
} else if r == '\'' {
@@ -326,22 +574,45 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil {
return l.errorf(err.Error())
}
growingString += "'" + str + "'"
sb.WriteString("'")
sb.WriteString(str)
sb.WriteString("'")
l.next()
continue
} else if r == '\n' {
return l.errorf("keys cannot contain new lines")
} else if isSpace(r) {
break
var str strings.Builder
str.WriteString(" ")
// skip trailing whitespace
l.next()
for r = l.peek(); isSpace(r); r = l.peek() {
str.WriteRune(r)
l.next()
}
// break loop if not a dot
if r != '.' {
break
}
str.WriteString(".")
// skip trailing whitespace after dot
l.next()
for r = l.peek(); isSpace(r); r = l.peek() {
str.WriteRune(r)
l.next()
}
sb.WriteString(str.String())
continue
} else if r == '.' {
// skip
} else if !isValidBareChar(r) {
return l.errorf("keys cannot contain %c character", r)
}
growingString += string(r)
sb.WriteRune(r)
l.next()
}
l.emitWithValue(tokenKey, growingString)
l.emitWithValue(tokenKey, sb.String())
return l.lexVoid
}
@@ -361,11 +632,12 @@ func (l *tomlLexer) lexComment(previousState tomlLexStateFn) tomlLexStateFn {
func (l *tomlLexer) lexLeftBracket() tomlLexStateFn {
l.next()
l.emit(tokenLeftBracket)
l.brackets = append(l.brackets, '[')
return l.lexRvalue
}
func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) {
growingString := ""
var sb strings.Builder
if discardLeadingNewLine {
if l.follow("\r\n") {
@@ -379,14 +651,14 @@ func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNe
// find end of string
for {
if l.follow(terminator) {
return growingString, nil
return sb.String(), nil
}
next := l.peek()
if next == eof {
break
}
growingString += string(l.next())
sb.WriteRune(l.next())
}
return "", errors.New("unclosed string")
@@ -420,7 +692,7 @@ func (l *tomlLexer) lexLiteralString() tomlLexStateFn {
// Terminator is the substring indicating the end of the token.
// The resulting string does not include the terminator.
func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) {
growingString := ""
var sb strings.Builder
if discardLeadingNewLine {
if l.follow("\r\n") {
@@ -433,7 +705,7 @@ func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine,
for {
if l.follow(terminator) {
return growingString, nil
return sb.String(), nil
}
if l.follow("\\") {
@@ -451,72 +723,72 @@ func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine,
l.next()
}
case '"':
growingString += "\""
sb.WriteString("\"")
l.next()
case 'n':
growingString += "\n"
sb.WriteString("\n")
l.next()
case 'b':
growingString += "\b"
sb.WriteString("\b")
l.next()
case 'f':
growingString += "\f"
sb.WriteString("\f")
l.next()
case '/':
growingString += "/"
sb.WriteString("/")
l.next()
case 't':
growingString += "\t"
sb.WriteString("\t")
l.next()
case 'r':
growingString += "\r"
sb.WriteString("\r")
l.next()
case '\\':
growingString += "\\"
sb.WriteString("\\")
l.next()
case 'u':
l.next()
code := ""
var code strings.Builder
for i := 0; i < 4; i++ {
c := l.peek()
if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape")
}
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 {
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':
l.next()
code := ""
var code strings.Builder
for i := 0; i < 8; i++ {
c := l.peek()
if !isHexDigit(c) {
return "", errors.New("unfinished unicode escape")
}
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 {
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:
return "", errors.New("invalid escape sequence: \\" + string(l.peek()))
}
} else {
r := l.peek()
if 0x00 <= r && r <= 0x1F && !(acceptNewLines && (r == '\n' || r == '\r')) {
if 0x00 <= r && r <= 0x1F && r != '\t' && !(acceptNewLines && (r == '\n' || r == '\r')) {
return "", fmt.Errorf("unescaped control character %U", r)
}
l.next()
growingString += string(r)
sb.WriteRune(r)
}
if l.peek() == eof {
@@ -543,7 +815,6 @@ func (l *tomlLexer) lexString() tomlLexStateFn {
}
str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines)
if err != nil {
return l.errorf(err.Error())
}
@@ -615,6 +886,10 @@ func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn {
func (l *tomlLexer) lexRightBracket() tomlLexStateFn {
l.next()
l.emit(tokenRightBracket)
if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '[' {
return l.errorf("cannot have ']' here")
}
l.brackets = l.brackets[:len(l.brackets)-1]
return l.lexRvalue
}
@@ -740,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
func lexToml(inputBytes []byte) []token {
runes := bytes.Runes(inputBytes)
+479 -55
View File
@@ -1,17 +1,63 @@
package toml
import (
"bytes"
"fmt"
"reflect"
"testing"
"text/tabwriter"
)
func testFlow(t *testing.T, input string, expectedFlow []token) {
tokens := lexToml([]byte(input))
if !reflect.DeepEqual(tokens, expectedFlow) {
t.Fatal("Different flows. Expected\n", expectedFlow, "\nGot:\n", 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) {
testFlow(t, "[hello world]", []token{
{Position{1, 1}, tokenLeftBracket, "["},
@@ -22,11 +68,20 @@ func TestValidKeyGroup(t *testing.T) {
}
func TestNestedQuotedUnicodeKeyGroup(t *testing.T) {
testFlow(t, `[ j . "ʞ" . l ]`, []token{
testFlow(t, `[ j . "ʞ" . l . 'ɯ' ]`, []token{
{Position{1, 1}, tokenLeftBracket, "["},
{Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l `},
{Position{1, 15}, tokenRightBracket, "]"},
{Position{1, 16}, tokenEOF, ""},
{Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l . 'ɯ' `},
{Position{1, 21}, tokenRightBracket, "]"},
{Position{1, 22}, tokenEOF, ""},
})
}
func TestNestedQuotedUnicodeKeyAssign(t *testing.T) {
testFlow(t, ` j . "ʞ" . l . 'ɯ' = 3`, []token{
{Position{1, 2}, tokenKey, `j . "ʞ" . l . 'ɯ'`},
{Position{1, 20}, tokenEqual, "="},
{Position{1, 22}, tokenInteger, "3"},
{Position{1, 23}, tokenEOF, ""},
})
}
@@ -105,9 +160,9 @@ func TestBasicKeyWithUppercaseMix(t *testing.T) {
}
func TestBasicKeyWithInternationalCharacters(t *testing.T) {
testFlow(t, "héllÖ", []token{
{Position{1, 1}, tokenKey, "héllÖ"},
{Position{1, 6}, tokenEOF, ""},
testFlow(t, "'héllÖ'", []token{
{Position{1, 1}, tokenKey, "'héllÖ'"},
{Position{1, 8}, tokenEOF, ""},
})
}
@@ -289,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) {
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T07:32:00Z"},
{Position{1, 27}, tokenEOF, ""},
t.Run("local date time", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T07:32:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenEOF, ""},
})
})
testFlow(t, "foo = 1979-05-27T00:32:00-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00-07:00"},
{Position{1, 32}, tokenEOF, ""},
t.Run("local date time space", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 07:32:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "07:32:00"},
{Position{1, 26}, tokenEOF, ""},
})
})
testFlow(t, "foo = 1979-05-27T00:32:00.999999-07:00", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27T00:32:00.999999-07:00"},
{Position{1, 39}, tokenEOF, ""},
t.Run("local date time fraction", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T00:32:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenEOF, ""},
})
})
testFlow(t, "foo = 1979-05-27 07:32:00Z", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenDate, "1979-05-27 07:32:00Z"},
{Position{1, 27}, tokenEOF, ""},
t.Run("local date time fraction space", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27 00:32:00.999999", []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLocalDate, "1979-05-27"},
{Position{1, 18}, tokenLocalTime, "00:32:00.999999"},
{Position{1, 33}, tokenEOF, ""},
})
})
t.Run("offset date-time utc", func(t *testing.T) {
testFlow(t, "foo = 1979-05-27T07:32:00Z", []token{
{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"},
})
})
}
@@ -654,6 +932,13 @@ func TestMultilineString(t *testing.T) {
{Position{6, 9}, tokenEOF, ""},
})
testFlow(t, `foo = """hello world"""`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 10}, tokenString, "hello\tworld"},
{Position{1, 24}, tokenEOF, ""},
})
testFlow(t, "key2 = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"", []token{
{Position{1, 1}, tokenKey, "key2"},
{Position{1, 6}, tokenEqual, "="},
@@ -691,6 +976,7 @@ func TestUnicodeString(t *testing.T) {
{Position{1, 22}, tokenEOF, ""},
})
}
func TestEscapeInString(t *testing.T) {
testFlow(t, `foo = "\b\f\/"`, []token{
{Position{1, 1}, tokenKey, "foo"},
@@ -700,6 +986,15 @@ func TestEscapeInString(t *testing.T) {
})
}
func TestTabInString(t *testing.T) {
testFlow(t, `foo = "hello world"`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 8}, tokenString, "hello\tworld"},
{Position{1, 20}, tokenEOF, ""},
})
}
func TestKeyGroupArray(t *testing.T) {
testFlow(t, "[[foo]]", []token{
{Position{1, 1}, tokenDoubleLeftBracket, "[["},
@@ -718,6 +1013,15 @@ func TestQuotedKey(t *testing.T) {
})
}
func TestQuotedKeyTab(t *testing.T) {
testFlow(t, "\"num\tber\" = 123", []token{
{Position{1, 1}, tokenKey, "\"num\tber\""},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "123"},
{Position{1, 16}, tokenEOF, ""},
})
}
func TestKeyNewline(t *testing.T) {
testFlow(t, "a\n= 4", []token{
{Position{1, 1}, tokenError, "keys cannot contain new lines"},
@@ -747,6 +1051,16 @@ func TestLexUnknownRvalue(t *testing.T) {
})
}
func TestLexInlineTableEmpty(t *testing.T) {
testFlow(t, `foo = {}`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 8}, tokenRightCurlyBrace, "}"},
{Position{1, 9}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKey(t *testing.T) {
testFlow(t, `foo = { bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
@@ -773,6 +1087,116 @@ func TestLexInlineTableBareKeyDash(t *testing.T) {
})
}
func TestLexInlineTableBareKeyInArray(t *testing.T) {
testFlow(t, `foo = [{ -bar_ = "baz" }]`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftBracket, "["},
{Position{1, 8}, tokenLeftCurlyBrace, "{"},
{Position{1, 10}, tokenKey, "-bar_"},
{Position{1, 16}, tokenEqual, "="},
{Position{1, 19}, tokenString, "baz"},
{Position{1, 24}, tokenRightCurlyBrace, "}"},
{Position{1, 25}, tokenRightBracket, "]"},
{Position{1, 26}, tokenEOF, ""},
})
}
func TestLexInlineTableError1(t *testing.T) {
testFlow(t, `foo = { 123 = 0 ]`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "123"},
{Position{1, 13}, tokenEqual, "="},
{Position{1, 15}, tokenInteger, "0"},
{Position{1, 17}, tokenRightBracket, "]"},
{Position{1, 18}, tokenError, "cannot have ']' here"},
})
}
func TestLexInlineTableError2(t *testing.T) {
testFlow(t, `foo = { 123 = 0 }}`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "123"},
{Position{1, 13}, tokenEqual, "="},
{Position{1, 15}, tokenInteger, "0"},
{Position{1, 17}, tokenRightCurlyBrace, "}"},
{Position{1, 18}, tokenRightCurlyBrace, "}"},
{Position{1, 19}, tokenError, "cannot have '}' here"},
})
}
func TestLexInlineTableDottedKey1(t *testing.T) {
testFlow(t, `foo = { a = 0, 123.45abc = 0 }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "a"},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "0"},
{Position{1, 14}, tokenComma, ","},
{Position{1, 16}, tokenKey, "123.45abc"},
{Position{1, 26}, tokenEqual, "="},
{Position{1, 28}, tokenInteger, "0"},
{Position{1, 30}, tokenRightCurlyBrace, "}"},
{Position{1, 31}, tokenEOF, ""},
})
}
func TestLexInlineTableDottedKey2(t *testing.T) {
testFlow(t, `foo = { a = 0, '123'.'45abc' = 0 }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "a"},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "0"},
{Position{1, 14}, tokenComma, ","},
{Position{1, 16}, tokenKey, "'123'.'45abc'"},
{Position{1, 30}, tokenEqual, "="},
{Position{1, 32}, tokenInteger, "0"},
{Position{1, 34}, tokenRightCurlyBrace, "}"},
{Position{1, 35}, tokenEOF, ""},
})
}
func TestLexInlineTableDottedKey3(t *testing.T) {
testFlow(t, `foo = { a = 0, "123"."45ʎǝʞ" = 0 }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "a"},
{Position{1, 11}, tokenEqual, "="},
{Position{1, 13}, tokenInteger, "0"},
{Position{1, 14}, tokenComma, ","},
{Position{1, 16}, tokenKey, `"123"."45ʎǝʞ"`},
{Position{1, 30}, tokenEqual, "="},
{Position{1, 32}, tokenInteger, "0"},
{Position{1, 34}, tokenRightCurlyBrace, "}"},
{Position{1, 35}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyWithComma(t *testing.T) {
testFlow(t, `foo = { -bar1 = "baz", -bar_ = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
{Position{1, 5}, tokenEqual, "="},
{Position{1, 7}, tokenLeftCurlyBrace, "{"},
{Position{1, 9}, tokenKey, "-bar1"},
{Position{1, 15}, tokenEqual, "="},
{Position{1, 18}, tokenString, "baz"},
{Position{1, 22}, tokenComma, ","},
{Position{1, 24}, tokenKey, "-bar_"},
{Position{1, 30}, tokenEqual, "="},
{Position{1, 33}, tokenString, "baz"},
{Position{1, 38}, tokenRightCurlyBrace, "}"},
{Position{1, 39}, tokenEOF, ""},
})
}
func TestLexInlineTableBareKeyUnderscore(t *testing.T) {
testFlow(t, `foo = { _bar = "baz" }`, []token{
{Position{1, 1}, tokenKey, "foo"},
+8 -2
View File
@@ -1,6 +1,12 @@
// Implementation of TOML's local date/time.
// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go
// to avoid pulling all the Google dependencies.
//
// Copied over from Google's civil to avoid pulling all the Google dependencies.
// Originals:
// https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil.go
// Changes:
// * Renamed files from civil* to localtime*.
// * Package changed from civil to toml.
// * 'Local' prefix added to all structs.
//
// Copyright 2016 Google LLC
//
+10
View File
@@ -1,3 +1,13 @@
// Implementation of TOML's local date/time.
//
// Copied over from Google's civil to avoid pulling all the Google dependencies.
// Originals:
// https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil_test.go
// Changes:
// * Renamed files from civil* to localtime*.
// * Package changed from civil to toml.
// * 'Local' prefix added to all structs.
//
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
+530 -90
View File
@@ -2,6 +2,7 @@ package toml
import (
"bytes"
"encoding"
"errors"
"fmt"
"io"
@@ -17,14 +18,17 @@ const (
tagFieldComment = "comment"
tagCommented = "commented"
tagMultiline = "multiline"
tagLiteral = "literal"
tagDefault = "default"
)
type tomlOpts struct {
name string
nameFromTag bool
comment string
commented bool
multiline bool
literal bool
include bool
omitempty bool
defaultValue string
@@ -44,6 +48,7 @@ type annotation struct {
comment string
commented string
multiline string
literal string
defaultValue string
}
@@ -52,15 +57,16 @@ var annotationDefault = annotation{
comment: tagFieldComment,
commented: tagCommented,
multiline: tagMultiline,
literal: tagLiteral,
defaultValue: tagDefault,
}
type marshalOrder int
type MarshalOrder int
// Orders the Encoder can write the fields to the output stream.
const (
// 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
// a struct.
OrderPreserve
@@ -68,9 +74,13 @@ const (
var timeType = reflect.TypeOf(time.Time{})
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
var unmarshalerType = reflect.TypeOf(new(Unmarshaler)).Elem()
var textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
var localDateType = reflect.TypeOf(LocalDate{})
var localTimeType = reflect.TypeOf(LocalTime{})
var localDateTimeType = reflect.TypeOf(LocalDateTime{})
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
// Check if the given marshal type maps to a Tree primitive
func isPrimitive(mtype reflect.Type) bool {
@@ -88,12 +98,16 @@ func isPrimitive(mtype reflect.Type) bool {
case reflect.String:
return true
case reflect.Struct:
return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType || isCustomMarshaler(mtype)
return isTimeType(mtype)
default:
return false
}
}
func isTimeType(mtype reflect.Type) bool {
return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType
}
// Check if the given marshal type maps to a Tree slice or array
func isTreeSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
@@ -106,6 +120,30 @@ func isTreeSequence(mtype reflect.Type) bool {
}
}
// Check if the given marshal type maps to a slice or array of a custom marshaler type
func isCustomMarshalerSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isCustomMarshalerSequence(mtype.Elem())
case reflect.Slice, reflect.Array:
return isCustomMarshaler(mtype.Elem()) || isCustomMarshaler(reflect.New(mtype.Elem()).Type())
default:
return false
}
}
// Check if the given marshal type maps to a slice or array of a text marshaler type
func isTextMarshalerSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isTextMarshalerSequence(mtype.Elem())
case reflect.Slice, reflect.Array:
return isTextMarshaler(mtype.Elem()) || isTextMarshaler(reflect.New(mtype.Elem()).Type())
default:
return false
}
}
// Check if the given marshal type maps to a non-Tree slice or array
func isOtherSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
@@ -140,12 +178,42 @@ func callCustomMarshaler(mval reflect.Value) ([]byte, error) {
return mval.Interface().(Marshaler).MarshalTOML()
}
func isTextMarshaler(mtype reflect.Type) bool {
return mtype.Implements(textMarshalerType) && !isTimeType(mtype)
}
func callTextMarshaler(mval reflect.Value) ([]byte, error) {
return mval.Interface().(encoding.TextMarshaler).MarshalText()
}
func isCustomUnmarshaler(mtype reflect.Type) bool {
return mtype.Implements(unmarshalerType)
}
func callCustomUnmarshaler(mval reflect.Value, tval interface{}) error {
return mval.Interface().(Unmarshaler).UnmarshalTOML(tval)
}
func isTextUnmarshaler(mtype reflect.Type) bool {
return mtype.Implements(textUnmarshalerType)
}
func callTextUnmarshaler(mval reflect.Value, text []byte) error {
return mval.Interface().(encoding.TextUnmarshaler).UnmarshalText(text)
}
// Marshaler is the interface implemented by types that
// can marshal themselves into valid TOML.
type Marshaler interface {
MarshalTOML() ([]byte, error)
}
// Unmarshaler is the interface implemented by types that
// can unmarshal a TOML description of themselves.
type Unmarshaler interface {
UnmarshalTOML(interface{}) error
}
/*
Marshal returns the TOML encoding of v. Behavior is similar to the Go json
encoder, except that there is no concept of a Marshaler interface or MarshalTOML
@@ -190,20 +258,24 @@ type Encoder struct {
w io.Writer
encOpts
annotation
line int
col int
order marshalOrder
line int
col int
order MarshalOrder
promoteAnon bool
compactComments bool
indentation string
}
// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
encOpts: encOptsDefaults,
annotation: annotationDefault,
line: 0,
col: 1,
order: OrderAlphabetical,
w: w,
encOpts: encOptsDefaults,
annotation: annotationDefault,
line: 0,
col: 1,
order: OrderAlphabetical,
indentation: " ",
}
}
@@ -250,11 +322,17 @@ func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
}
// 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
return e
}
// Indentation allows to change indentation when marshalling.
func (e *Encoder) Indentation(indent string) *Encoder {
e.indentation = indent
return e
}
// SetTagName allows changing default tag "toml"
func (e *Encoder) SetTagName(v string) *Encoder {
e.tag = v
@@ -279,8 +357,37 @@ func (e *Encoder) SetTagMultiline(v string) *Encoder {
return e
}
// PromoteAnonymous allows to change how anonymous struct fields are marshaled.
// Usually, they are marshaled as if the inner exported fields were fields in
// the outer struct. However, if an anonymous struct field is given a name in
// its TOML tag, it is treated like a regular struct field with that name.
// rather than being anonymous.
//
// In case anonymous promotion is enabled, all anonymous structs are promoted
// and treated like regular struct fields.
func (e *Encoder) PromoteAnonymous(promote bool) *Encoder {
e.promoteAnon = promote
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) {
// Check if indentation is valid
for _, char := range e.indentation {
if !isSpace(char) {
return []byte{}, fmt.Errorf("invalid indentation: must only contains space or tab characters")
}
}
mtype := reflect.TypeOf(v)
if mtype == nil {
return []byte{}, errors.New("nil cannot be marshaled to TOML")
}
switch mtype.Kind() {
case reflect.Struct, reflect.Map:
@@ -288,6 +395,9 @@ func (e *Encoder) marshal(v interface{}) ([]byte, error) {
if mtype.Elem().Kind() != reflect.Struct {
return []byte{}, errors.New("Only pointer to struct can be marshaled to TOML")
}
if reflect.ValueOf(v).IsNil() {
return []byte{}, errors.New("nil pointer cannot be marshaled to TOML")
}
default:
return []byte{}, errors.New("Only a struct or map can be marshaled to TOML")
}
@@ -296,13 +406,16 @@ func (e *Encoder) marshal(v interface{}) ([]byte, error) {
if isCustomMarshaler(mtype) {
return callCustomMarshaler(sval)
}
if isTextMarshaler(mtype) {
return callTextMarshaler(sval)
}
t, err := e.valueToTree(mtype, sval)
if err != nil {
return []byte{}, err
}
var buf bytes.Buffer
_, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order)
_, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order, e.indentation, e.compactComments, false)
return buf.Bytes(), err
}
@@ -320,20 +433,30 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
tval := e.nextTree()
switch mtype.Kind() {
case reflect.Struct:
for i := 0; i < mtype.NumField(); i++ {
mtypef, mvalf := mtype.Field(i), mval.Field(i)
opts := tomlOptions(mtypef, e.annotation)
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
val, err := e.valueToToml(mtypef.Type, mvalf)
if err != nil {
return nil, err
switch mval.Interface().(type) {
case Tree:
reflect.ValueOf(tval).Elem().Set(mval)
default:
for i := 0; i < mtype.NumField(); i++ {
mtypef, mvalf := mtype.Field(i), mval.Field(i)
opts := tomlOptions(mtypef, e.annotation)
if opts.include && ((mtypef.Type.Kind() != reflect.Interface && !opts.omitempty) || !isZero(mvalf)) {
val, err := e.valueToToml(mtypef.Type, mvalf)
if err != nil {
return nil, err
}
if tree, ok := val.(*Tree); ok && mtypef.Anonymous && !opts.nameFromTag && !e.promoteAnon {
e.appendTree(tval, tree)
} else {
val = e.wrapTomlValue(val, tval)
tval.SetPathWithOptions([]string{opts.name}, SetOptions{
Comment: opts.comment,
Commented: opts.commented,
Multiline: opts.multiline,
Literal: opts.literal,
}, val)
}
}
tval.SetWithOptions(opts.name, SetOptions{
Comment: opts.comment,
Commented: opts.commented,
Multiline: opts.multiline,
}, val)
}
}
case reflect.Map:
@@ -358,18 +481,22 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
}
for _, key := range keys {
mvalf := mval.MapIndex(key)
if (mtype.Elem().Kind() == reflect.Ptr || mtype.Elem().Kind() == reflect.Interface) && mvalf.IsNil() {
continue
}
val, err := e.valueToToml(mtype.Elem(), mvalf)
if err != nil {
return nil, err
}
val = e.wrapTomlValue(val, tval)
if e.quoteMapKeys {
keyStr, err := tomlValueStringRepresentation(key.String(), "", e.arraysOneElementPerLine)
keyStr, err := tomlValueStringRepresentation(key.String(), "", "", e.order, e.arraysOneElementPerLine)
if err != nil {
return nil, err
}
tval.SetPath([]string{keyStr}, val)
} else {
tval.Set(key.String(), val)
tval.SetPath([]string{key.String()}, val)
}
}
}
@@ -404,19 +531,32 @@ func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (int
// Convert given marshal value to toml value
func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
e.line++
if mtype.Kind() == reflect.Ptr {
return e.valueToToml(mtype.Elem(), mval.Elem())
switch {
case isCustomMarshaler(mtype):
return callCustomMarshaler(mval)
case isTextMarshaler(mtype):
b, err := callTextMarshaler(mval)
return string(b), err
default:
return e.valueToToml(mtype.Elem(), mval.Elem())
}
}
if mtype.Kind() == reflect.Interface {
return e.valueToToml(mval.Elem().Type(), mval.Elem())
}
switch {
case isCustomMarshaler(mtype):
return callCustomMarshaler(mval)
case isTextMarshaler(mtype):
b, err := callTextMarshaler(mval)
return string(b), err
case isTree(mtype):
return e.valueToTree(mtype, mval)
case isOtherSequence(mtype), isCustomMarshalerSequence(mtype), isTextMarshalerSequence(mtype):
return e.valueToOtherSlice(mtype, mval)
case isTreeSequence(mtype):
return e.valueToTreeSlice(mtype, mval)
case isOtherSequence(mtype):
return e.valueToOtherSlice(mtype, mval)
default:
switch mtype.Kind() {
case reflect.Bool:
@@ -440,6 +580,39 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
}
}
func (e *Encoder) appendTree(t, o *Tree) error {
for key, value := range o.values {
if _, ok := t.values[key]; ok {
continue
}
if tomlValue, ok := value.(*tomlValue); ok {
tomlValue.position.Col = t.position.Col
}
t.values[key] = value
}
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.
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
// sub-structs, and only definite types can be unmarshaled.
@@ -492,6 +665,8 @@ type Decoder struct {
tval *Tree
encOpts
tagName string
strict bool
visitor visitorState
}
// NewDecoder returns a new decoder that reads from r.
@@ -522,8 +697,18 @@ func (d *Decoder) SetTagName(v string) *Decoder {
return d
}
// Strict allows changing to strict decoding. Any fields that are found in the
// input data and do not have a corresponding struct member cause an error.
func (d *Decoder) Strict(strict bool) *Decoder {
d.strict = strict
return d
}
func (d *Decoder) unmarshal(v interface{}) error {
mtype := reflect.TypeOf(v)
if mtype == nil {
return errors.New("nil cannot be unmarshaled from TOML")
}
if mtype.Kind() != reflect.Ptr {
return errors.New("only a pointer to struct or map can be unmarshaled from TOML")
}
@@ -532,16 +717,29 @@ func (d *Decoder) unmarshal(v interface{}) error {
switch elem.Kind() {
case reflect.Struct, reflect.Map:
case reflect.Interface:
elem = mapStringInterfaceType
default:
return errors.New("only a pointer to struct or map can be unmarshaled from TOML")
}
if reflect.ValueOf(v).IsNil() {
return errors.New("nil pointer cannot be unmarshaled from TOML")
}
vv := reflect.ValueOf(v).Elem()
if d.strict {
d.visitor = newVisitorState(d.tval)
}
sval, err := d.valueFromTree(elem, d.tval, &vv)
if err != nil {
return err
}
if err := d.visitor.validate(); err != nil {
return err
}
reflect.ValueOf(v).Elem().Set(sval)
return nil
}
@@ -552,6 +750,21 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
if mtype.Kind() == reflect.Ptr {
return d.unwrapPointer(mtype, tval, mval1)
}
// Check if pointer to value implements the Unmarshaler interface.
if mvalPtr := reflect.New(mtype); isCustomUnmarshaler(mvalPtr.Type()) {
d.visitor.visitAll()
if tval == nil {
return mvalPtr.Elem(), nil
}
if err := callCustomUnmarshaler(mvalPtr, tval.ToMap()); err != nil {
return reflect.ValueOf(nil), fmt.Errorf("unmarshal toml: %v", err)
}
return mvalPtr.Elem(), nil
}
var mval reflect.Value
switch mtype.Kind() {
case reflect.Struct:
@@ -561,11 +774,17 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
mval = reflect.New(mtype).Elem()
}
for i := 0; i < mtype.NumField(); i++ {
mtypef := mtype.Field(i)
an := annotation{tag: d.tagName}
opts := tomlOptions(mtypef, an)
if opts.include {
switch mval.Interface().(type) {
case Tree:
mval.Set(reflect.ValueOf(tval).Elem())
default:
for i := 0; i < mtype.NumField(); i++ {
mtypef := mtype.Field(i)
an := annotation{tag: d.tagName}
opts := tomlOptions(mtypef, an)
if !opts.include {
continue
}
baseKey := opts.name
keysToTry := []string{
baseKey,
@@ -575,20 +794,25 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
}
found := false
for _, key := range keysToTry {
exists := tval.Has(key)
if !exists {
continue
if tval != nil {
for _, key := range keysToTry {
exists := tval.HasPath([]string{key})
if !exists {
continue
}
d.visitor.push(key)
val := tval.GetPath([]string{key})
fval := mval.Field(i)
mvalf, err := d.valueFromToml(mtypef.Type, val, &fval)
if err != nil {
return mval, formatError(err, tval.GetPositionPath([]string{key}))
}
mval.Field(i).Set(mvalf)
found = true
d.visitor.pop()
break
}
val := tval.Get(key)
fval := mval.Field(i)
mvalf, err := d.valueFromToml(mtypef.Type, val, &fval)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.Field(i).Set(mvalf)
found = true
break
}
if !found && opts.defaultValue != "" {
@@ -596,37 +820,66 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
var val interface{}
var err error
switch mvalf.Kind() {
case reflect.Bool:
val, err = strconv.ParseBool(opts.defaultValue)
if err != nil {
return mval.Field(i), err
}
case reflect.Int:
val, err = strconv.Atoi(opts.defaultValue)
if err != nil {
return mval.Field(i), err
}
case reflect.String:
val = opts.defaultValue
case reflect.Bool:
val, err = strconv.ParseBool(opts.defaultValue)
case reflect.Uint:
val, err = strconv.ParseUint(opts.defaultValue, 10, 0)
case reflect.Uint8:
val, err = strconv.ParseUint(opts.defaultValue, 10, 8)
case reflect.Uint16:
val, err = strconv.ParseUint(opts.defaultValue, 10, 16)
case reflect.Uint32:
val, err = strconv.ParseUint(opts.defaultValue, 10, 32)
case reflect.Uint64:
val, err = strconv.ParseUint(opts.defaultValue, 10, 64)
case reflect.Int:
val, err = strconv.ParseInt(opts.defaultValue, 10, 0)
case reflect.Int8:
val, err = strconv.ParseInt(opts.defaultValue, 10, 8)
case reflect.Int16:
val, err = strconv.ParseInt(opts.defaultValue, 10, 16)
case reflect.Int32:
val, err = strconv.ParseInt(opts.defaultValue, 10, 32)
case reflect.Int64:
val, err = strconv.ParseInt(opts.defaultValue, 10, 64)
if err != nil {
return mval.Field(i), err
// 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:
val, err = strconv.ParseFloat(opts.defaultValue, 32)
case reflect.Float64:
val, err = strconv.ParseFloat(opts.defaultValue, 64)
if err != nil {
return mval.Field(i), err
}
default:
return mval.Field(i), fmt.Errorf("unsuported field type for default option")
return mvalf, fmt.Errorf("unsupported field type for default option")
}
mval.Field(i).Set(reflect.ValueOf(val))
if err != nil {
return mvalf, err
}
mvalf.Set(reflect.ValueOf(val).Convert(mvalf.Type()))
}
// save the old behavior above and try to check anonymous structs
if !found && opts.defaultValue == "" && mtypef.Anonymous && mtypef.Type.Kind() == reflect.Struct {
v, err := d.valueFromTree(mtypef.Type, tval, nil)
// save the old behavior above and try to check structs
if !found && opts.defaultValue == "" && mtypef.Type.Kind() == reflect.Struct {
tmpTval := tval
if !mtypef.Anonymous {
tmpTval = nil
}
fval := mval.Field(i)
v, err := d.valueFromTree(mtypef.Type, tmpTval, &fval)
if err != nil {
return v, err
}
@@ -637,13 +890,15 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
case reflect.Map:
mval = reflect.MakeMap(mtype)
for _, key := range tval.Keys() {
d.visitor.push(key)
// TODO: path splits key
val := tval.GetPath([]string{key})
mvalf, err := d.valueFromToml(mtype.Elem(), val, nil)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
return mval, formatError(err, tval.GetPositionPath([]string{key}))
}
mval.SetMapIndex(reflect.ValueOf(key).Convert(mtype.Key()), mvalf)
d.visitor.pop()
}
}
return mval, nil
@@ -651,22 +906,52 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.V
// Convert toml value to marshal struct/map slice, using marshal type
func (d *Decoder) valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
mval, err := makeSliceOrArray(mtype, len(tval))
if err != nil {
return mval, err
}
for i := 0; i < len(tval); i++ {
d.visitor.push(strconv.Itoa(i))
val, err := d.valueFromTree(mtype.Elem(), tval[i], nil)
if err != nil {
return mval, err
}
mval.Index(i).Set(val)
d.visitor.pop()
}
return mval, nil
}
// Convert toml value to marshal primitive slice, using marshal type
func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) {
mval, err := makeSliceOrArray(mtype, len(tval))
if err != nil {
return mval, err
}
for i := 0; i < len(tval); i++ {
val, err := d.valueFromToml(mtype.Elem(), tval[i], nil)
if err != nil {
return mval, err
}
mval.Index(i).Set(val)
}
return mval, nil
}
// Convert toml value to marshal primitive slice, using marshal type
func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
for i := 0; i < len(tval); i++ {
val, err := d.valueFromToml(mtype.Elem(), tval[i], nil)
func (d *Decoder) valueFromOtherSliceI(mtype reflect.Type, tval interface{}) (reflect.Value, error) {
val := reflect.ValueOf(tval)
length := val.Len()
mval, err := makeSliceOrArray(mtype, length)
if err != nil {
return mval, err
}
for i := 0; i < length; i++ {
val, err := d.valueFromToml(mtype.Elem(), val.Index(i).Interface(), nil)
if err != nil {
return mval, err
}
@@ -675,6 +960,21 @@ func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (r
return mval, nil
}
// Create a new slice or a new array with specified length
func makeSliceOrArray(mtype reflect.Type, tLength int) (reflect.Value, error) {
var mval reflect.Value
switch mtype.Kind() {
case reflect.Slice:
mval = reflect.MakeSlice(mtype, tLength, tLength)
case reflect.Array:
mval = reflect.New(reflect.ArrayOf(mtype.Len(), mtype.Elem())).Elem()
if tLength > mtype.Len() {
return mval, fmt.Errorf("unmarshal: TOML array length (%v) exceeds destination array length (%v)", tLength, mtype.Len())
}
}
return mval, nil
}
// Convert toml value to marshal value, using marshal type. When mval1 is non-nil
// and the given type is a struct value, merge fields into it.
func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) {
@@ -692,18 +992,63 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
if isTree(mtype) {
return d.valueFromTree(mtype, t, mval11)
}
if mtype.Kind() == reflect.Interface {
if mval1 == nil || mval1.IsNil() {
return d.valueFromTree(reflect.TypeOf(map[string]interface{}{}), t, nil)
} else {
return d.valueFromToml(mval1.Elem().Type(), t, nil)
}
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a tree", tval, tval)
case []*Tree:
if isTreeSequence(mtype) {
return d.valueFromTreeSlice(mtype, t)
}
if mtype.Kind() == reflect.Interface {
if mval1 == nil || mval1.IsNil() {
return d.valueFromTreeSlice(reflect.TypeOf([]map[string]interface{}{}), t)
} else {
ival := mval1.Elem()
return d.valueFromToml(mval1.Elem().Type(), t, &ival)
}
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to trees", tval, tval)
case []interface{}:
d.visitor.visit()
if isOtherSequence(mtype) {
return d.valueFromOtherSlice(mtype, t)
}
if mtype.Kind() == reflect.Interface {
if mval1 == nil || mval1.IsNil() {
return d.valueFromOtherSlice(reflect.TypeOf([]interface{}{}), t)
} else {
ival := mval1.Elem()
return d.valueFromToml(mval1.Elem().Type(), t, &ival)
}
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a slice", tval, tval)
default:
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.
if isTextUnmarshaler(mvalPtr.Type()) && !isTimeType(mtype) {
if err := d.unmarshalText(tval, mvalPtr); err != nil {
return reflect.ValueOf(nil), fmt.Errorf("unmarshal text: %v", err)
}
return mvalPtr.Elem(), nil
}
switch mtype.Kind() {
case reflect.Bool, reflect.Struct:
val := reflect.ValueOf(tval)
@@ -754,38 +1099,50 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
}
return reflect.ValueOf(d), nil
}
if !val.Type().ConvertibleTo(mtype) {
if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Float64 {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Convert(mtype).Int()) {
if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Convert(reflect.TypeOf(int64(0))).Int()) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
val := reflect.ValueOf(tval)
if !val.Type().ConvertibleTo(mtype) {
if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Float64 {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if val.Convert(reflect.TypeOf(int(1))).Int() < 0 {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) is negative so does not fit in %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowUint(uint64(val.Convert(mtype).Uint())) {
if reflect.Indirect(reflect.New(mtype)).OverflowUint(val.Convert(reflect.TypeOf(uint64(0))).Uint()) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
case reflect.Float32, reflect.Float64:
val := reflect.ValueOf(tval)
if !val.Type().ConvertibleTo(mtype) {
if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Int64 {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Convert(mtype).Float()) {
if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Convert(reflect.TypeOf(float64(0))).Float()) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
return val.Convert(mtype), nil
case reflect.Interface:
if mval1 == nil || mval1.IsNil() {
return reflect.ValueOf(tval), nil
} else {
ival := mval1.Elem()
return d.valueFromToml(mval1.Elem().Type(), t, &ival)
}
case reflect.Slice, reflect.Array:
if isOtherSequence(mtype) && isOtherSequence(reflect.TypeOf(t)) {
return d.valueFromOtherSliceI(mtype, t)
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind())
default:
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind())
}
@@ -795,7 +1152,7 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) {
var melem *reflect.Value
if mval1 != nil && !mval1.IsNil() && mtype.Elem().Kind() == reflect.Struct {
if mval1 != nil && !mval1.IsNil() && (mtype.Elem().Kind() == reflect.Struct || mtype.Elem().Kind() == reflect.Interface) {
elem := mval1.Elem()
melem = &elem
}
@@ -809,6 +1166,12 @@ func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *ref
return mval, nil
}
func (d *Decoder) unmarshalText(tval interface{}, mval reflect.Value) error {
var buf bytes.Buffer
fmt.Fprint(&buf, tval)
return callTextUnmarshaler(mval, buf.Bytes())
}
func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
tag := vf.Tag.Get(an.tag)
parse := strings.Split(tag, ",")
@@ -818,12 +1181,15 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
}
commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented))
multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline))
literal, _ := strconv.ParseBool(vf.Tag.Get(an.literal))
defaultValue := vf.Tag.Get(tagDefault)
result := tomlOpts{
name: vf.Name,
nameFromTag: false,
comment: comment,
commented: commented,
multiline: multiline,
literal: literal,
include: true,
omitempty: false,
defaultValue: defaultValue,
@@ -833,6 +1199,7 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
result.include = false
} else {
result.name = strings.Trim(parse[0], " ")
result.nameFromTag = true
}
}
if vf.PkgPath != "" {
@@ -849,11 +1216,7 @@ func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
func isZero(val reflect.Value) bool {
switch val.Type().Kind() {
case reflect.Map:
fallthrough
case reflect.Array:
fallthrough
case reflect.Slice:
case reflect.Slice, reflect.Array, reflect.Map:
return val.Len() == 0
default:
return reflect.DeepEqual(val.Interface(), reflect.Zero(val.Type()).Interface())
@@ -866,3 +1229,80 @@ func formatError(err error, pos Position) error {
}
return fmt.Errorf("%s: %s", pos, err)
}
// visitorState keeps track of which keys were unmarshaled.
type visitorState struct {
tree *Tree
path []string
keys map[string]struct{}
active bool
}
func newVisitorState(tree *Tree) visitorState {
path, result := []string{}, map[string]struct{}{}
insertKeys(path, result, tree)
return visitorState{
tree: tree,
path: path[:0],
keys: result,
active: true,
}
}
func (s *visitorState) push(key string) {
if s.active {
s.path = append(s.path, key)
}
}
func (s *visitorState) pop() {
if s.active {
s.path = s.path[:len(s.path)-1]
}
}
func (s *visitorState) visit() {
if s.active {
delete(s.keys, strings.Join(s.path, "."))
}
}
func (s *visitorState) visitAll() {
if s.active {
for k := range s.keys {
if strings.HasPrefix(k, strings.Join(s.path, ".")) {
delete(s.keys, k)
}
}
}
}
func (s *visitorState) validate() error {
if !s.active {
return nil
}
undecoded := make([]string, 0, len(s.keys))
for key := range s.keys {
undecoded = append(undecoded, key)
}
sort.Strings(undecoded)
if len(undecoded) > 0 {
return fmt.Errorf("undecoded keys: %q", undecoded)
}
return nil
}
func insertKeys(path []string, m map[string]struct{}, tree *Tree) {
for k, v := range tree.values {
switch node := v.(type) {
case []*Tree:
for i, item := range node {
insertKeys(append(path, k, strconv.Itoa(i)), m, item)
}
case *Tree:
insertKeys(append(path, k), m, node)
case *tomlValue:
m[strings.Join(append(path, k), ".")] = struct{}{}
}
}
}
+2041 -62
View File
File diff suppressed because it is too large Load Diff
+73 -44
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"math"
"reflect"
"regexp"
"strconv"
"strings"
"time"
@@ -158,6 +157,11 @@ func (p *tomlParser) parseGroup() tomlParserStateFn {
if err := p.tree.createSubTree(keys, startToken.Position); err != nil {
p.raiseError(key, "%s", err)
}
destTree := p.tree.GetPath(keys)
if target, ok := destTree.(*Tree); ok && target != nil && target.inline {
p.raiseError(key, "could not re-define exist inline table or its sub-table : %s",
strings.Join(keys, "."))
}
p.assume(tokenRightBracket)
p.currentTable = keys
return p.parseStart
@@ -201,6 +205,11 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
strings.Join(tableKey, "."))
}
if targetNode.inline {
p.raiseError(key, "could not add key or sub-table to exist inline table or its sub-table : %s",
strings.Join(tableKey, "."))
}
// assign value to the found table
keyVal := parsedKey[len(parsedKey)-1]
localKey := []string{keyVal}
@@ -221,19 +230,38 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
return p.parseStart
}
var numberUnderscoreInvalidRegexp *regexp.Regexp
var hexNumberUnderscoreInvalidRegexp *regexp.Regexp
var errInvalidUnderscore = errors.New("invalid use of _ in number")
func numberContainsInvalidUnderscore(value string) error {
if numberUnderscoreInvalidRegexp.MatchString(value) {
return errors.New("invalid use of _ in number")
// For large numbers, you may use underscores between digits to enhance
// 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
}
var errInvalidUnderscoreHex = errors.New("invalid use of _ in hex number")
func hexNumberContainsInvalidUnderscore(value string) error {
if hexNumberUnderscoreInvalidRegexp.MatchString(value) {
return errors.New("invalid use of _ in hex number")
hasBefore := false
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
}
@@ -312,42 +340,44 @@ func (p *tomlParser) parseRvalue() interface{} {
p.raiseError(tok, "%s", err)
}
return val
case tokenDate:
layout := time.RFC3339Nano
if !strings.Contains(tok.val, "T") {
layout = strings.Replace(layout, "T", " ", 1)
}
val, err := time.ParseInLocation(layout, tok.val, time.UTC)
case tokenLocalTime:
val, err := ParseLocalTime(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
return val
case tokenLocalDate:
v := strings.Replace(tok.val, " ", "T", -1)
isDateTime := false
isTime := false
for _, c := range v {
if c == 'T' || c == 't' {
isDateTime = true
break
}
if c == ':' {
isTime = true
break
// a local date may be followed by:
// * nothing: this is a local date
// * a local time: this is a local date-time
next := p.peek()
if next == nil || next.typ != tokenLocalTime {
val, err := ParseLocalDate(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
return val
}
var val interface{}
var err error
localDate := tok
localTime := p.getToken()
if isDateTime {
val, err = ParseLocalDateTime(v)
} else if isTime {
val, err = ParseLocalTime(v)
} else {
val, err = ParseLocalDate(v)
next = p.peek()
if next == nil || next.typ != tokenTimeOffset {
v := localDate.val + "T" + localTime.val
val, err := ParseLocalDateTime(v)
if err != nil {
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 {
p.raiseError(tok, "%s", err)
}
@@ -360,10 +390,10 @@ func (p *tomlParser) parseRvalue() interface{} {
p.raiseError(tok, "cannot have multiple equals for the same key")
case tokenError:
p.raiseError(tok, "%s", tok)
default:
panic(fmt.Errorf("unhandled token: %v", tok))
}
p.raiseError(tok, "never reached")
return nil
}
@@ -411,12 +441,13 @@ Loop:
if tokenIsComma(previous) {
p.raiseError(previous, "trailing comma at the end of inline table")
}
tree.inline = true
return tree
}
func (p *tomlParser) parseArray() interface{} {
var array []interface{}
arrayType := reflect.TypeOf(nil)
arrayType := reflect.TypeOf(newTree())
for {
follow := p.peek()
if follow == nil || follow.typ == tokenEOF {
@@ -427,11 +458,8 @@ func (p *tomlParser) parseArray() interface{} {
break
}
val := p.parseRvalue()
if arrayType == nil {
arrayType = reflect.TypeOf(val)
}
if reflect.TypeOf(val) != arrayType {
p.raiseError(follow, "mixed types in array")
arrayType = nil
}
array = append(array, val)
follow = p.peek()
@@ -445,6 +473,12 @@ func (p *tomlParser) parseArray() interface{} {
p.getToken()
}
}
// if the array is a mixed-type array or its length is 0,
// don't convert it to a table array
if len(array) <= 0 {
arrayType = nil
}
// An array of Trees is actually an array of inline
// tables, which is a shorthand for a table array. If the
// array was not converted from []interface{} to []*Tree,
@@ -472,8 +506,3 @@ func parseToml(flow []token) *Tree {
parser.run()
return result
}
func init() {
numberUnderscoreInvalidRegexp = regexp.MustCompile(`([^\d]_|_[^\d])|_$|^_`)
hexNumberUnderscoreInvalidRegexp = regexp.MustCompile(`(^0x_)|([^\da-f]_|_[^\da-f])|_$|^_`)
}
+113 -30
View File
@@ -6,8 +6,6 @@ import (
"reflect"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
)
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{}) {
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)
t.Log("Finished tree assertion.")
}
@@ -239,7 +237,8 @@ func TestLocalDateTime(t *testing.T) {
Minute: 32,
Second: 0,
Nanosecond: 0,
}},
},
},
})
}
@@ -257,7 +256,8 @@ func TestLocalDateTimeNano(t *testing.T) {
Minute: 32,
Second: 0,
Nanosecond: 999999000,
}},
},
},
})
}
@@ -272,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) {
tree, err := Load("a = 07:32:00")
assertTree(t, tree, err, map[string]interface{}{
@@ -486,18 +514,6 @@ func TestNestedEmptyArrays(t *testing.T) {
})
}
func TestArrayMixedTypes(t *testing.T) {
_, err := Load("a = [42, 16.0]")
if err.Error() != "(1, 10): mixed types in array" {
t.Error("Bad error message:", err.Error())
}
_, err = Load("a = [42, \"hello\"]")
if err.Error() != "(1, 11): mixed types in array" {
t.Error("Bad error message:", err.Error())
}
}
func TestArrayNestedStrings(t *testing.T) {
tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]")
assertTree(t, tree, err, map[string]interface{}{
@@ -581,6 +597,39 @@ func TestDoubleInlineGroup(t *testing.T) {
})
}
func TestNestedInlineGroup(t *testing.T) {
tree, err := Load("out = {block0 = {x = 99, y = 100}, block1 = {p = \"999\", q = \"1000\"}}")
assertTree(t, tree, err, map[string]interface{}{
"out": map[string]interface{}{
"block0": map[string]interface{}{
"x": int64(99),
"y": int64(100),
},
"block1": map[string]interface{}{
"p": "999",
"q": "1000",
},
},
})
}
func TestArrayInNestedInlineGroup(t *testing.T) {
tree, err := Load(`image = {name = "xxx", palette = {id = 100, colors = ["red", "blue", "green"]}}`)
assertTree(t, tree, err, map[string]interface{}{
"image": map[string]interface{}{
"name": "xxx",
"palette": map[string]interface{}{
"id": int64(100),
"colors": []string{
"red",
"blue",
"green",
},
},
},
})
}
func TestExampleInlineGroup(t *testing.T) {
tree, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }`)
@@ -644,7 +693,7 @@ func TestInlineTableUnterminated(t *testing.T) {
func TestInlineTableCommaExpected(t *testing.T) {
_, err := Load("foo = {hello = 53 test = foo}")
if err.Error() != "(1, 19): comma expected between fields in inline table" {
if err.Error() != "(1, 19): unexpected token type in inline table: no value can start with t" {
t.Error("Bad error message:", err.Error())
}
}
@@ -658,7 +707,42 @@ func TestInlineTableCommaStart(t *testing.T) {
func TestInlineTableDoubleComma(t *testing.T) {
_, err := Load("foo = {hello = 53,, foo = 17}")
if err.Error() != "(1, 19): need field between two commas in inline table" {
if err.Error() != "(1, 19): unexpected token type in inline table: keys cannot contain , character" {
t.Error("Bad error message:", err.Error())
}
}
func TestInlineTableTrailingComma(t *testing.T) {
_, err := Load("foo = {hello = 53, foo = 17,}")
if err.Error() != "(1, 28): trailing comma at the end of inline table" {
t.Error("Bad error message:", err.Error())
}
}
func TestAddKeyToInlineTable(t *testing.T) {
_, err := Load("type = { name = \"Nail\" }\ntype.edible = false")
if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : type" {
t.Error("Bad error message:", err.Error())
}
}
func TestAddSubTableToInlineTable(t *testing.T) {
_, err := Load("a = { b = \"c\" }\na.d.e = \"f\"")
if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : a.d" {
t.Error("Bad error message:", err.Error())
}
}
func TestAddKeyToSubTableOfInlineTable(t *testing.T) {
_, err := Load("a = { b = { c = \"d\" } }\na.b.e = \"f\"")
if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : a.b" {
t.Error("Bad error message:", err.Error())
}
}
func TestReDefineInlineTable(t *testing.T) {
_, err := Load("a = { b = \"c\" }\n[a]\n d = \"e\"")
if err.Error() != "(2, 2): could not re-define exist inline table or its sub-table : a" {
t.Error("Bad error message:", err.Error())
}
}
@@ -750,6 +834,7 @@ func TestParseFile(t *testing.T) {
[]string{"gamma", "delta"},
[]int64{1, 2},
},
"score": 4e-08,
},
})
}
@@ -786,6 +871,7 @@ func TestParseFileCRLF(t *testing.T) {
[]string{"gamma", "delta"},
[]int64{1, 2},
},
"score": 4e-08,
},
})
}
@@ -858,13 +944,11 @@ func TestTomlValueStringRepresentation(t *testing.T) {
{"hello world", "\"hello world\""},
{"\b\t\n\f\r\"\\", "\"\\b\\t\\n\\f\\r\\\"\\\\\""},
{"\x05", "\"\\u0005\""},
{time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
"1979-05-27T07:32:00Z"},
{[]interface{}{"gamma", "delta"},
"[\"gamma\",\"delta\"]"},
{time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC), "1979-05-27T07:32:00Z"},
{[]interface{}{"gamma", "delta"}, "[\"gamma\", \"delta\"]"},
{nil, ""},
} {
result, err := tomlValueStringRepresentation(item.Value, "", false)
result, err := tomlValueStringRepresentation(item.Value, "", "", OrderAlphabetical, false)
if err != nil {
t.Errorf("Test %d - unexpected error: %s", idx, err)
}
@@ -991,7 +1075,7 @@ func TestInvalidFloatParsing(t *testing.T) {
}
_, err = Load("a=_1_2")
if err.Error() != "(1, 3): cannot start number with underscore" {
if err.Error() != "(1, 3): no value can start with _" {
t.Error("Bad error message:", err.Error())
}
}
@@ -1055,11 +1139,10 @@ The quick brown \
the lazy dog."""
str3 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""`)
The quick brown \` + " " + `
fox jumps over \` + " " + `
the lazy dog.\` + " " + `
"""`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
+201
View File
@@ -0,0 +1,201 @@
# Query package
## Overview
Package query performs JSONPath-like queries on a TOML document.
The query path implementation is based loosely on the JSONPath specification:
http://goessner.net/articles/JsonPath/.
The idea behind a query path is to allow quick access to any element, or set
of elements within TOML document, with a single expression.
```go
result, err := query.CompileAndExecute("$.foo.bar.baz", tree)
```
This is roughly equivalent to:
```go
next := tree.Get("foo")
if next != nil {
next = next.Get("bar")
if next != nil {
next = next.Get("baz")
}
}
result := next
```
err is nil if any parsing exception occurs.
If no node in the tree matches the query, result will simply contain an empty list of
items.
As illustrated above, the query path is much more efficient, especially since
the structure of the TOML file can vary. Rather than making assumptions about
a document's structure, a query allows the programmer to make structured
requests into the document, and get zero or more values as a result.
## Query syntax
The syntax of a query begins with a root token, followed by any number
sub-expressions:
```
$
Root of the TOML tree. This must always come first.
.name
Selects child of this node, where 'name' is a TOML key
name.
['name']
Selects child of this node, where 'name' is a string
containing a TOML key name.
[index]
Selcts child array element at 'index'.
..expr
Recursively selects all children, filtered by an a union,
index, or slice expression.
..*
Recursive selection of all nodes at this point in the
tree.
.*
Selects all children of the current node.
[expr,expr]
Union operator - a logical 'or' grouping of two or more
sub-expressions: index, key name, or filter.
[start:end:step]
Slice operator - selects array elements from start to
end-1, at the given step. All three arguments are
optional.
[?(filter)]
Named filter expression - the function 'filter' is
used to filter children at this node.
```
## Query Indexes And Slices
Index expressions perform no bounds checking, and will contribute no
values to the result set if the provided index or index range is invalid.
Negative indexes represent values from the end of the array, counting backwards.
```go
// select the last index of the array named 'foo'
query.CompileAndExecute("$.foo[-1]", tree)
```
Slice expressions are supported, by using ':' to separate a start/end index pair.
```go
// select up to the first five elements in the array
query.CompileAndExecute("$.foo[0:5]", tree)
```
Slice expressions also allow negative indexes for the start and stop
arguments.
```go
// select all array elements except the last one.
query.CompileAndExecute("$.foo[0:-1]", tree)
```
Slice expressions may have an optional stride/step parameter:
```go
// select every other element
query.CompileAndExecute("$.foo[0::2]", tree)
```
Slice start and end parameters are also optional:
```go
// these are all equivalent and select all the values in the array
query.CompileAndExecute("$.foo[:]", tree)
query.CompileAndExecute("$.foo[::]", tree)
query.CompileAndExecute("$.foo[::1]", tree)
query.CompileAndExecute("$.foo[0:]", tree)
query.CompileAndExecute("$.foo[0::]", tree)
query.CompileAndExecute("$.foo[0::1]", tree)
```
## Query Filters
Query filters are used within a Union [,] or single Filter [] expression.
A filter only allows nodes that qualify through to the next expression,
and/or into the result set.
```go
// returns children of foo that are permitted by the 'bar' filter.
query.CompileAndExecute("$.foo[?(bar)]", tree)
```
There are several filters provided with the library:
```
tree
Allows nodes of type Tree.
int
Allows nodes of type int64.
float
Allows nodes of type float64.
string
Allows nodes of type string.
time
Allows nodes of type time.Time.
bool
Allows nodes of type bool.
```
## Query Results
An executed query returns a Result object. This contains the nodes
in the TOML tree that qualify the query expression. Position information
is also available for each value in the set.
```go
// display the results of a query
results := query.CompileAndExecute("$.foo.bar.baz", tree)
for idx, value := results.Values() {
fmt.Println("%v: %v", results.Positions()[idx], value)
}
```
## Compiled Queries
Queries may be executed directly on a Tree object, or compiled ahead
of time and executed discretely. The former is more convenient, but has the
penalty of having to recompile the query expression each time.
```go
// basic query
results := query.CompileAndExecute("$.foo.bar.baz", tree)
// compiled query
query, err := toml.Compile("$.foo.bar.baz")
results := query.Execute(tree)
// run the compiled query again on a different tree
moreResults := query.Execute(anotherTree)
```
## User Defined Query Filters
Filter expressions may also be user defined by using the SetFilter()
function on the Query object. The function must return true/false, which
signifies if the passed node is kept or discarded, respectively.
```go
// create a query that references a user-defined filter
query, _ := query.Compile("$[?(bazOnly)]")
// define the filter, and assign it to the query
query.SetFilter("bazOnly", func(node interface{}) bool{
if tree, ok := node.(*Tree); ok {
return tree.Has("baz")
}
return false // reject all other node types
})
// run the query
query.Execute(tree)
```
+13 -15
View File
@@ -25,7 +25,7 @@
// items.
//
// As illustrated above, the query path is much more efficient, especially since
// the structure of the TOML file can vary. Rather than making assumptions about
// the structure of the TOML file can vary. Rather than making assumptions about
// a document's structure, a query allows the programmer to make structured
// requests into the document, and get zero or more values as a result.
//
@@ -35,7 +35,7 @@
// sub-expressions:
//
// $
// Root of the TOML tree. This must always come first.
// Root of the TOML tree. This must always come first.
// .name
// Selects child of this node, where 'name' is a TOML key
// name.
@@ -57,7 +57,7 @@
// sub-expressions: index, key name, or filter.
// [start:end:step]
// Slice operator - selects array elements from start to
// end-1, at the given step. All three arguments are
// end-1, at the given step. All three arguments are
// optional.
// [?(filter)]
// Named filter expression - the function 'filter' is
@@ -80,25 +80,23 @@
// Slice expressions also allow negative indexes for the start and stop
// arguments.
//
// // select all array elements.
// // select all array elements except the last one.
// query.CompileAndExecute("$.foo[0:-1]", tree)
//
// Slice expressions may have an optional stride/step parameter:
//
// // select every other element
// query.CompileAndExecute("$.foo[0:-1:2]", tree)
// query.CompileAndExecute("$.foo[0::2]", tree)
//
// Slice start and end parameters are also optional:
//
// // these are all equivalent and select all the values in the array
// query.CompileAndExecute("$.foo[:]", tree)
// query.CompileAndExecute("$.foo[0:]", tree)
// query.CompileAndExecute("$.foo[:-1]", tree)
// query.CompileAndExecute("$.foo[0:-1:]", tree)
// query.CompileAndExecute("$.foo[::]", tree)
// query.CompileAndExecute("$.foo[::1]", tree)
// query.CompileAndExecute("$.foo[0:]", tree)
// query.CompileAndExecute("$.foo[0::]", tree)
// query.CompileAndExecute("$.foo[0::1]", tree)
// query.CompileAndExecute("$.foo[:-1:1]", tree)
// query.CompileAndExecute("$.foo[0:-1:1]", tree)
//
// Query Filters
//
@@ -126,8 +124,8 @@
//
// Query Results
//
// An executed query returns a Result object. This contains the nodes
// in the TOML tree that qualify the query expression. Position information
// An executed query returns a Result object. This contains the nodes
// in the TOML tree that qualify the query expression. Position information
// is also available for each value in the set.
//
// // display the results of a query
@@ -139,7 +137,7 @@
// Compiled Queries
//
// Queries may be executed directly on a Tree object, or compiled ahead
// of time and executed discretely. The former is more convenient, but has the
// of time and executed discretely. The former is more convenient, but has the
// penalty of having to recompile the query expression each time.
//
// // basic query
@@ -155,7 +153,7 @@
// User Defined Query Filters
//
// Filter expressions may also be user defined by using the SetFilter()
// function on the Query object. The function must return true/false, which
// function on the Query object. The function must return true/false, which
// signifies if the passed node is kept or discarded, respectively.
//
// // create a query that references a user-defined filter
@@ -166,7 +164,7 @@
// if tree, ok := node.(*Tree); ok {
// return tree.Has("baz")
// }
// return false // reject all other node types
// return false // reject all other node types
// })
//
// // run the query
+117 -38
View File
@@ -2,6 +2,8 @@ package query
import (
"fmt"
"reflect"
"github.com/pelletier/go-toml"
)
@@ -44,16 +46,16 @@ func newMatchKeyFn(name string) *matchKeyFn {
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
if array, ok := node.([]*toml.Tree); ok {
for _, tree := range array {
item := tree.Get(f.Name)
item := tree.GetPath([]string{f.Name})
if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name)
ctx.lastPosition = tree.GetPositionPath([]string{f.Name})
f.next.call(item, ctx)
}
}
} else if tree, ok := node.(*toml.Tree); ok {
item := tree.Get(f.Name)
item := tree.GetPath([]string{f.Name})
if item != nil {
ctx.lastPosition = tree.GetPosition(f.Name)
ctx.lastPosition = tree.GetPositionPath([]string{f.Name})
f.next.call(item, ctx)
}
}
@@ -70,53 +72,130 @@ func newMatchIndexFn(idx int) *matchIndexFn {
}
func (f *matchIndexFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok {
if f.Idx < len(arr) && f.Idx >= 0 {
if treesArray, ok := node.([]*toml.Tree); ok {
if len(treesArray) > 0 {
ctx.lastPosition = treesArray[0].Position()
}
}
f.next.call(arr[f.Idx], ctx)
v := reflect.ValueOf(node)
if v.Kind() == reflect.Slice {
if v.Len() == 0 {
return
}
// Manage negative values
idx := f.Idx
if idx < 0 {
idx += v.Len()
}
if 0 <= idx && idx < v.Len() {
callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface())
}
}
}
func callNextIndexSlice(next pathFn, node interface{}, ctx *queryContext, value interface{}) {
if treesArray, ok := node.([]*toml.Tree); ok {
ctx.lastPosition = treesArray[0].Position()
}
next.call(value, ctx)
}
// filter by slicing
type matchSliceFn struct {
matchBase
Start, End, Step int
Start, End, Step *int
}
func newMatchSliceFn(start, end, step int) *matchSliceFn {
return &matchSliceFn{Start: start, End: end, Step: step}
func newMatchSliceFn() *matchSliceFn {
return &matchSliceFn{}
}
func (f *matchSliceFn) setStart(start int) *matchSliceFn {
f.Start = &start
return f
}
func (f *matchSliceFn) setEnd(end int) *matchSliceFn {
f.End = &end
return f
}
func (f *matchSliceFn) setStep(step int) *matchSliceFn {
f.Step = &step
return f
}
func (f *matchSliceFn) call(node interface{}, ctx *queryContext) {
if arr, ok := node.([]interface{}); ok {
// adjust indexes for negative values, reverse ordering
realStart, realEnd := f.Start, f.End
if realStart < 0 {
realStart = len(arr) + realStart
v := reflect.ValueOf(node)
if v.Kind() == reflect.Slice {
if v.Len() == 0 {
return
}
if realEnd < 0 {
realEnd = len(arr) + realEnd
var start, end, step int
// Initialize step
if f.Step != nil {
step = *f.Step
} else {
step = 1
}
if realEnd < realStart {
realEnd, realStart = realStart, realEnd // swap
}
// loop and gather
for idx := realStart; idx < realEnd; idx += f.Step {
if treesArray, ok := node.([]*toml.Tree); ok {
if len(treesArray) > 0 {
ctx.lastPosition = treesArray[0].Position()
}
// Initialize start
if f.Start != nil {
start = *f.Start
// Manage negative values
if start < 0 {
start += v.Len()
}
// Manage out of range values
start = max(start, 0)
start = min(start, v.Len()-1)
} else if step > 0 {
start = 0
} else {
start = v.Len() - 1
}
// Initialize end
if f.End != nil {
end = *f.End
// Manage negative values
if end < 0 {
end += v.Len()
}
// Manage out of range values
end = max(end, -1)
end = min(end, v.Len())
} else if step > 0 {
end = v.Len()
} else {
end = -1
}
// Loop on values
if step > 0 {
for idx := start; idx < end; idx += step {
callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface())
}
} else {
for idx := start; idx > end; idx += step {
callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface())
}
f.next.call(arr[idx], ctx)
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// match anything
type matchAnyFn struct {
matchBase
@@ -129,8 +208,8 @@ func newMatchAnyFn() *matchAnyFn {
func (f *matchAnyFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*toml.Tree); ok {
for _, k := range tree.Keys() {
v := tree.Get(k)
ctx.lastPosition = tree.GetPosition(k)
v := tree.GetPath([]string{k})
ctx.lastPosition = tree.GetPositionPath([]string{k})
f.next.call(v, ctx)
}
}
@@ -168,8 +247,8 @@ func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
var visit func(tree *toml.Tree)
visit = func(tree *toml.Tree) {
for _, k := range tree.Keys() {
v := tree.Get(k)
ctx.lastPosition = tree.GetPosition(k)
v := tree.GetPath([]string{k})
ctx.lastPosition = tree.GetPositionPath([]string{k})
f.next.call(v, ctx)
switch node := v.(type) {
case *toml.Tree:
@@ -207,9 +286,9 @@ func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
switch castNode := node.(type) {
case *toml.Tree:
for _, k := range castNode.Keys() {
v := castNode.Get(k)
v := castNode.GetPath([]string{k})
if fn(v) {
ctx.lastPosition = castNode.GetPosition(k)
ctx.lastPosition = castNode.GetPositionPath([]string{k})
f.next.call(v, ctx)
}
}
+21 -10
View File
@@ -2,8 +2,10 @@ package query
import (
"fmt"
"github.com/pelletier/go-toml"
"strconv"
"testing"
"github.com/pelletier/go-toml"
)
// dump path tree to a string
@@ -19,8 +21,17 @@ func pathString(root pathFn) string {
result += fmt.Sprintf("{%d}", fn.Idx)
result += pathString(fn.next)
case *matchSliceFn:
result += fmt.Sprintf("{%d:%d:%d}",
fn.Start, fn.End, fn.Step)
startString, endString, stepString := "nil", "nil", "nil"
if fn.Start != nil {
startString = strconv.Itoa(*fn.Start)
}
if fn.End != nil {
endString = strconv.Itoa(*fn.End)
}
if fn.Step != nil {
stepString = strconv.Itoa(*fn.Step)
}
result += fmt.Sprintf("{%s:%s:%s}", startString, endString, stepString)
result += pathString(fn.next)
case *matchAnyFn:
result += "{}"
@@ -110,7 +121,7 @@ func TestPathSliceStart(t *testing.T) {
assertPath(t,
"$[123:]",
buildPath(
newMatchSliceFn(123, maxInt, 1),
newMatchSliceFn().setStart(123),
))
}
@@ -118,7 +129,7 @@ func TestPathSliceStartEnd(t *testing.T) {
assertPath(t,
"$[123:456]",
buildPath(
newMatchSliceFn(123, 456, 1),
newMatchSliceFn().setStart(123).setEnd(456),
))
}
@@ -126,7 +137,7 @@ func TestPathSliceStartEndColon(t *testing.T) {
assertPath(t,
"$[123:456:]",
buildPath(
newMatchSliceFn(123, 456, 1),
newMatchSliceFn().setStart(123).setEnd(456),
))
}
@@ -134,7 +145,7 @@ func TestPathSliceStartStep(t *testing.T) {
assertPath(t,
"$[123::7]",
buildPath(
newMatchSliceFn(123, maxInt, 7),
newMatchSliceFn().setStart(123).setStep(7),
))
}
@@ -142,7 +153,7 @@ func TestPathSliceEndStep(t *testing.T) {
assertPath(t,
"$[:456:7]",
buildPath(
newMatchSliceFn(0, 456, 7),
newMatchSliceFn().setEnd(456).setStep(7),
))
}
@@ -150,7 +161,7 @@ func TestPathSliceStep(t *testing.T) {
assertPath(t,
"$[::7]",
buildPath(
newMatchSliceFn(0, maxInt, 7),
newMatchSliceFn().setStep(7),
))
}
@@ -158,7 +169,7 @@ func TestPathSliceAll(t *testing.T) {
assertPath(t,
"$[123:456:7]",
buildPath(
newMatchSliceFn(123, 456, 7),
newMatchSliceFn().setStart(123).setEnd(456).setStep(7),
))
}
+11 -8
View File
@@ -203,12 +203,13 @@ loop: // labeled loop for easy breaking
func (p *queryParser) parseSliceExpr() queryParserStateFn {
// init slice to grab all elements
start, end, step := 0, maxInt, 1
var start, end, step *int = nil, nil, nil
// parse optional start
tok := p.getToken()
if tok.typ == tokenInteger {
start = tok.Int()
v := tok.Int()
start = &v
tok = p.getToken()
}
if tok.typ != tokenColon {
@@ -218,11 +219,12 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn {
// parse optional end
tok = p.getToken()
if tok.typ == tokenInteger {
end = tok.Int()
v := tok.Int()
end = &v
tok = p.getToken()
}
if tok.typ == tokenRightBracket {
p.query.appendPath(newMatchSliceFn(start, end, step))
p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step})
return p.parseMatchExpr
}
if tok.typ != tokenColon {
@@ -232,17 +234,18 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn {
// parse optional step
tok = p.getToken()
if tok.typ == tokenInteger {
step = tok.Int()
if step < 0 {
return p.parseError(tok, "step must be a positive value")
v := tok.Int()
if v == 0 {
return p.parseError(tok, "step cannot be zero")
}
step = &v
tok = p.getToken()
}
if tok.typ != tokenRightBracket {
return p.parseError(tok, "expected ']'")
}
p.query.appendPath(newMatchSliceFn(start, end, step))
p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step})
return p.parseMatchExpr
}
+226 -95
View File
@@ -78,6 +78,19 @@ func assertValue(t *testing.T, result, ref interface{}) {
}
}
func assertParseError(t *testing.T, query string, errString string) {
_, err := Compile(query)
if err == nil {
t.Error("error should be non-nil")
return
}
if err.Error() != errString {
t.Errorf("error does not match")
t.Log("test:", err.Error())
t.Log("ref: ", errString)
}
}
func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) {
tree, err := toml.Load(tomlDoc)
if err != nil {
@@ -128,54 +141,213 @@ func TestQueryKeyString(t *testing.T) {
})
}
func TestQueryIndex(t *testing.T) {
func TestQueryKeyUnicodeString(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
"$.foo.a[5]",
"['f𝟘.o']\na = 42",
"$['f𝟘.o']['a']",
[]interface{}{
queryTestNode{
int64(6), toml.Position{2, 1},
int64(42), toml.Position{2, 1},
},
})
}
func TestQueryIndexError1(t *testing.T) {
assertParseError(t, "$.foo.a[5", "(1, 10): expected ',' or ']', not ''")
}
func TestQueryIndexError2(t *testing.T) {
assertParseError(t, "$.foo.a[]", "(1, 9): expected union sub expression, not ']', 0")
}
func TestQueryIndex(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[5]",
[]interface{}{
queryTestNode{int64(5), toml.Position{2, 1}},
})
}
func TestQueryIndexNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[-2]",
[]interface{}{
queryTestNode{int64(8), toml.Position{2, 1}},
})
}
func TestQueryIndexWrong(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[99]",
[]interface{}{})
}
func TestQueryIndexEmpty(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = []",
"$.foo.a[5]",
[]interface{}{})
}
func TestQueryIndexTree(t *testing.T) {
assertQueryPositions(t,
"[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\nb = 3",
"$.foo[1].b",
[]interface{}{
queryTestNode{int64(3), toml.Position{4, 1}},
})
}
func TestQuerySliceError1(t *testing.T) {
assertParseError(t, "$.foo.a[3:?]", "(1, 11): expected ']' or ':'")
}
func TestQuerySliceError2(t *testing.T) {
assertParseError(t, "$.foo.a[:::]", "(1, 11): expected ']'")
}
func TestQuerySliceError3(t *testing.T) {
assertParseError(t, "$.foo.a[::0]", "(1, 11): step cannot be zero")
}
func TestQuerySliceRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
"$.foo.a[0:5]",
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[:5]",
[]interface{}{
queryTestNode{
int64(1), toml.Position{2, 1},
},
queryTestNode{
int64(2), toml.Position{2, 1},
},
queryTestNode{
int64(3), toml.Position{2, 1},
},
queryTestNode{
int64(4), toml.Position{2, 1},
},
queryTestNode{
int64(5), toml.Position{2, 1},
},
queryTestNode{int64(0), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{int64(3), toml.Position{2, 1}},
queryTestNode{int64(4), toml.Position{2, 1}},
})
}
func TestQuerySliceStep(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [1,2,3,4,5,6,7,8,9,0]",
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[0:5:2]",
[]interface{}{
queryTestNode{int64(0), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{int64(4), toml.Position{2, 1}},
})
}
func TestQuerySliceStartNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[-3:]",
[]interface{}{
queryTestNode{int64(7), toml.Position{2, 1}},
queryTestNode{int64(8), toml.Position{2, 1}},
queryTestNode{int64(9), toml.Position{2, 1}},
})
}
func TestQuerySliceEndNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[:-6]",
[]interface{}{
queryTestNode{int64(0), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{int64(3), toml.Position{2, 1}},
})
}
func TestQuerySliceStepNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[::-2]",
[]interface{}{
queryTestNode{int64(9), toml.Position{2, 1}},
queryTestNode{int64(7), toml.Position{2, 1}},
queryTestNode{int64(5), toml.Position{2, 1}},
queryTestNode{int64(3), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
})
}
func TestQuerySliceStartOverRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[-99:3]",
[]interface{}{
queryTestNode{int64(0), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{2, 1}},
})
}
func TestQuerySliceStartOverRangeNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[99:7:-1]",
[]interface{}{
queryTestNode{int64(9), toml.Position{2, 1}},
queryTestNode{int64(8), toml.Position{2, 1}},
})
}
func TestQuerySliceEndOverRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[7:99]",
[]interface{}{
queryTestNode{int64(7), toml.Position{2, 1}},
queryTestNode{int64(8), toml.Position{2, 1}},
queryTestNode{int64(9), toml.Position{2, 1}},
})
}
func TestQuerySliceEndOverRangeNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[2:-99:-1]",
[]interface{}{
queryTestNode{int64(2), toml.Position{2, 1}},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(0), toml.Position{2, 1}},
})
}
func TestQuerySliceWrongRange(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[5:3]",
[]interface{}{})
}
func TestQuerySliceWrongRangeNegative(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[3:5:-1]",
[]interface{}{})
}
func TestQuerySliceEmpty(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = []",
"$.foo.a[5:]",
[]interface{}{})
}
func TestQuerySliceTree(t *testing.T) {
assertQueryPositions(t,
"[[foo]]\na='nok'\n[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\na='ok'\nb = 3",
"$.foo[1:].a",
[]interface{}{
queryTestNode{
int64(1), toml.Position{2, 1},
},
queryTestNode{
int64(3), toml.Position{2, 1},
},
queryTestNode{
int64(5), toml.Position{2, 1},
},
[]interface{}{
int64(0), int64(1), int64(2), int64(3), int64(4),
int64(5), int64(6), int64(7), int64(8), int64(9)},
toml.Position{4, 1}},
queryTestNode{"ok", toml.Position{6, 1}},
})
}
@@ -265,12 +437,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
int64(1), toml.Position{2, 1},
},
queryTestNode{
int64(2), toml.Position{3, 1},
},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{3, 1}},
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
@@ -285,12 +453,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(4),
}, toml.Position{4, 1},
},
queryTestNode{
int64(3), toml.Position{5, 1},
},
queryTestNode{
int64(4), toml.Position{6, 1},
},
queryTestNode{int64(3), toml.Position{5, 1}},
queryTestNode{int64(4), toml.Position{6, 1}},
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
@@ -305,12 +469,8 @@ func TestQueryRecursionAll(t *testing.T) {
"b": int64(6),
}, toml.Position{7, 1},
},
queryTestNode{
int64(5), toml.Position{8, 1},
},
queryTestNode{
int64(6), toml.Position{9, 1},
},
queryTestNode{int64(5), toml.Position{8, 1}},
queryTestNode{int64(6), toml.Position{9, 1}},
})
}
@@ -358,56 +518,30 @@ func TestQueryFilterFn(t *testing.T) {
assertQueryPositions(t, string(buff),
"$..[?(int)]",
[]interface{}{
queryTestNode{
int64(8001), toml.Position{13, 1},
},
queryTestNode{
int64(8001), toml.Position{13, 1},
},
queryTestNode{
int64(8002), toml.Position{13, 1},
},
queryTestNode{
int64(5000), toml.Position{14, 1},
},
queryTestNode{int64(8001), toml.Position{13, 1}},
queryTestNode{int64(8001), toml.Position{13, 1}},
queryTestNode{int64(8002), toml.Position{13, 1}},
queryTestNode{int64(5000), toml.Position{14, 1}},
})
assertQueryPositions(t, string(buff),
"$..[?(string)]",
[]interface{}{
queryTestNode{
"TOML Example", toml.Position{3, 1},
},
queryTestNode{
"Tom Preston-Werner", toml.Position{6, 1},
},
queryTestNode{
"GitHub", toml.Position{7, 1},
},
queryTestNode{
"GitHub Cofounder & CEO\nLikes tater tots and beer.",
toml.Position{8, 1},
},
queryTestNode{
"192.168.1.1", toml.Position{12, 1},
},
queryTestNode{
"10.0.0.1", toml.Position{21, 3},
},
queryTestNode{
"eqdc10", toml.Position{22, 3},
},
queryTestNode{
"10.0.0.2", toml.Position{25, 3},
},
queryTestNode{
"eqdc10", toml.Position{26, 3},
},
queryTestNode{"TOML Example", toml.Position{3, 1}},
queryTestNode{"Tom Preston-Werner", toml.Position{6, 1}},
queryTestNode{"GitHub", toml.Position{7, 1}},
queryTestNode{"GitHub Cofounder & CEO\nLikes tater tots and beer.", toml.Position{8, 1}},
queryTestNode{"192.168.1.1", toml.Position{12, 1}},
queryTestNode{"10.0.0.1", toml.Position{21, 3}},
queryTestNode{"eqdc10", toml.Position{22, 3}},
queryTestNode{"10.0.0.2", toml.Position{25, 3}},
queryTestNode{"eqdc10", toml.Position{26, 3}},
})
assertQueryPositions(t, string(buff),
"$..[?(float)]",
[]interface{}{ // no float values in document
[]interface{}{
queryTestNode{4e-08, toml.Position{30, 1}},
})
tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
@@ -460,6 +594,7 @@ func TestQueryFilterFn(t *testing.T) {
[]interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)},
},
"score": 4e-08,
}, toml.Position{28, 1},
},
})
@@ -467,16 +602,12 @@ func TestQueryFilterFn(t *testing.T) {
assertQueryPositions(t, string(buff),
"$..[?(time)]",
[]interface{}{
queryTestNode{
tv, toml.Position{9, 1},
},
queryTestNode{tv, toml.Position{9, 1}},
})
assertQueryPositions(t, string(buff),
"$..[?(bool)]",
[]interface{}{
queryTestNode{
true, toml.Position{15, 1},
},
queryTestNode{true, toml.Position{15, 1}},
})
}
+25 -31
View File
@@ -7,25 +7,26 @@ import (
"github.com/pelletier/go-toml"
)
func assertArrayContainsInAnyOrder(t *testing.T, array []interface{}, objects ...interface{}) {
func assertArrayContainsInOrder(t *testing.T, array []interface{}, objects ...interface{}) {
if len(array) != len(objects) {
t.Fatalf("array contains %d objects but %d are expected", len(array), len(objects))
}
for _, o := range objects {
found := false
for _, a := range array {
if a == o {
found = true
break
}
}
if !found {
t.Fatal(o, "not found in array", array)
for i := 0; i < len(array); i++ {
if array[i] != objects[i] {
t.Fatalf("wanted '%s', have '%s'", objects[i], array[i])
}
}
}
func checkQuery(t *testing.T, tree *toml.Tree, query string, objects ...interface{}) {
results, err := CompileAndExecute(query, tree)
if err != nil {
t.Fatal("unexpected error:", err)
}
assertArrayContainsInOrder(t, results.Values(), objects...)
}
func TestQueryExample(t *testing.T) {
config, _ := toml.Load(`
[[book]]
@@ -37,16 +38,18 @@ func TestQueryExample(t *testing.T) {
[[book]]
title = "Neuromancer"
author = "William Gibson"
`)
authors, err := CompileAndExecute("$.book.author", config)
if err != nil {
t.Fatal("unexpected error:", err)
}
names := authors.Values()
if len(names) != 3 {
t.Fatalf("query should return 3 names but returned %d", len(names))
}
assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson")
`)
checkQuery(t, config, "$.book.author", "Stephen King", "Ernest Hemmingway", "William Gibson")
checkQuery(t, config, "$.book[0].author", "Stephen King")
checkQuery(t, config, "$.book[-1].author", "William Gibson")
checkQuery(t, config, "$.book[1:].author", "Ernest Hemmingway", "William Gibson")
checkQuery(t, config, "$.book[-1:].author", "William Gibson")
checkQuery(t, config, "$.book[::2].author", "Stephen King", "William Gibson")
checkQuery(t, config, "$.book[::-1].author", "William Gibson", "Ernest Hemmingway", "Stephen King")
checkQuery(t, config, "$.book[:].author", "Stephen King", "Ernest Hemmingway", "William Gibson")
checkQuery(t, config, "$.book[::].author", "Stephen King", "Ernest Hemmingway", "William Gibson")
}
func TestQueryReadmeExample(t *testing.T) {
@@ -56,16 +59,7 @@ user = "pelletier"
password = "mypassword"
`)
query, err := Compile("$..[user,password]")
if err != nil {
t.Fatal("unexpected error:", err)
}
results := query.Execute(config)
values := results.Values()
if len(values) != 2 {
t.Fatalf("query should return 2 values but returned %d", len(values))
}
assertArrayContainsInAnyOrder(t, values, "pelletier", "mypassword")
checkQuery(t, config, "$..[user,password]", "pelletier", "mypassword")
}
func TestQueryPathNotPresent(t *testing.T) {
+4 -4
View File
@@ -2,9 +2,9 @@ package query
import (
"fmt"
"github.com/pelletier/go-toml"
"strconv"
"unicode"
"github.com/pelletier/go-toml"
)
// Define tokens
@@ -92,11 +92,11 @@ func isSpace(r rune) bool {
}
func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_'
return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_'
}
func isDigit(r rune) bool {
return unicode.IsNumber(r)
return '0' <= r && r <= '9'
}
func isHexDigit(r rune) bool {
+7 -8
View File
@@ -1,9 +1,6 @@
package toml
import (
"fmt"
"unicode"
)
import "fmt"
// Define tokens
type tokenType int
@@ -33,8 +30,9 @@ const (
tokenRightParen
tokenDoubleLeftBracket
tokenDoubleRightBracket
tokenDate
tokenLocalDate
tokenLocalTime
tokenTimeOffset
tokenKeyGroup
tokenKeyGroupArray
tokenComma
@@ -69,7 +67,8 @@ var tokenTypeNames = []string{
"]]",
"[[",
"LocalDate",
"LocalDate",
"LocalTime",
"TimeOffset",
"KeyGroup",
"KeyGroupArray",
",",
@@ -112,7 +111,7 @@ func isSpace(r rune) bool {
}
func isAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || r == '_'
return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_'
}
func isKeyChar(r rune) bool {
@@ -127,7 +126,7 @@ func isKeyStartChar(r rune) bool {
}
func isDigit(r rune) bool {
return unicode.IsNumber(r)
return '0' <= r && r <= '9'
}
func isHexDigit(r rune) bool {
+2 -1
View File
@@ -25,8 +25,9 @@ func TestTokenStringer(t *testing.T) {
{tokenRightParen, ")"},
{tokenDoubleLeftBracket, "]]"},
{tokenDoubleRightBracket, "[["},
{tokenDate, "LocalDate"},
{tokenLocalDate, "LocalDate"},
{tokenLocalTime, "LocalTime"},
{tokenTimeOffset, "TimeOffset"},
{tokenKeyGroup, "KeyGroup"},
{tokenKeyGroupArray, "KeyGroupArray"},
{tokenComma, ","},
+141 -1
View File
@@ -15,6 +15,7 @@ type tomlValue struct {
comment string
commented bool
multiline bool
literal bool
position Position
}
@@ -23,6 +24,7 @@ type Tree struct {
values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree
comment string
commented bool
inline bool
position Position
}
@@ -121,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.
func (t *Tree) GetPosition(key string) Position {
if key == "" {
@@ -129,6 +214,50 @@ func (t *Tree) GetPosition(key string) Position {
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'.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetPositionPath(keys []string) Position {
@@ -186,6 +315,7 @@ type SetOptions struct {
Comment string
Commented bool
Multiline bool
Literal bool
}
// SetWithOptions is the same as Set, but allows you to provide formatting
@@ -211,7 +341,8 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
// go to most recent element
if len(node) == 0 {
// 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]
}
@@ -222,17 +353,25 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
switch v := value.(type) {
case *Tree:
v.comment = opts.Comment
v.commented = opts.Commented
toInsert = value
case []*Tree:
for i := range v {
v[i].commented = opts.Commented
}
toInsert = value
case *tomlValue:
v.comment = opts.Comment
v.commented = opts.Commented
v.multiline = opts.Multiline
v.literal = opts.Literal
toInsert = v
default:
toInsert = &tomlValue{value: value,
comment: opts.Comment,
commented: opts.Commented,
multiline: opts.Multiline,
literal: opts.Literal,
position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}}
}
@@ -307,6 +446,7 @@ func (t *Tree) createSubTree(keys []string, pos Position) error {
if !exists {
tree := newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
tree.position = pos
tree.inline = subtree.inline
subtree.values[intermediateKey] = tree
nextTree = tree
}
+81
View File
@@ -3,6 +3,7 @@
package toml
import (
"reflect"
"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) {
tree, _ := Load(`
[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) {
simpleMap := map[string]interface{}{"hello": 42}
tree, err := TreeFromMap(simpleMap)
+1 -3
View File
@@ -8,8 +8,6 @@ import (
"reflect"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
)
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) {
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")
}
}
-15
View File
@@ -5,21 +5,6 @@ import (
"testing"
)
func TestInvalidArrayMixedTypesArraysAndInts(t *testing.T) {
input := `arrays-and-ints = [1, ["Arrays are not integers."]]`
testgenInvalid(t, input)
}
func TestInvalidArrayMixedTypesIntsAndFloats(t *testing.T) {
input := `ints-and-floats = [1, 1.1]`
testgenInvalid(t, input)
}
func TestInvalidArrayMixedTypesStringsAndInts(t *testing.T) {
input := `strings-and-ints = ["hi", 42]`
testgenInvalid(t, input)
}
func TestInvalidDatetimeMalformedNoLeads(t *testing.T) {
input := `no-leads = 1987-7-05T17:45:00Z`
testgenInvalid(t, input)
+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
case fmt.Stringer:
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:
return nil, fmt.Errorf("cannot convert type %T to Tree", object)
}
+118 -1
View File
@@ -1,6 +1,7 @@
package toml
import (
"reflect"
"strconv"
"testing"
"time"
@@ -105,7 +106,7 @@ func TestTreeCreateToTreeInvalidTableGroupType(t *testing.T) {
}
func TestRoundTripArrayOfTables(t *testing.T) {
orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\",\"b\"]\n"
orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\", \"b\"]\n"
tree, err := Load(orig)
if err != nil {
t.Fatalf("unexpected error: %s", err)
@@ -124,3 +125,119 @@ func TestRoundTripArrayOfTables(t *testing.T) {
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)
}
}
+105 -28
View File
@@ -28,23 +28,35 @@ type sortNode struct {
// Encodes a string to a TOML-compliant multi-line string value
// This function is a clone of the existing encodeTomlString function, except that whitespace characters
// are preserved. Quotation marks and backslashes are also not escaped.
func encodeMultilineTomlString(value string) string {
func encodeMultilineTomlString(value string, commented string) string {
var b bytes.Buffer
adjacentQuoteCount := 0
for _, rr := range value {
b.WriteString(commented)
for i, rr := range value {
if rr != '"' {
adjacentQuoteCount = 0
} else {
adjacentQuoteCount++
}
switch rr {
case '\b':
b.WriteString(`\b`)
case '\t':
b.WriteString("\t")
case '\n':
b.WriteString("\n")
b.WriteString("\n" + commented)
case '\f':
b.WriteString(`\f`)
case '\r':
b.WriteString("\r")
case '"':
b.WriteString(`"`)
if adjacentQuoteCount >= 3 || i == len(value)-1 {
adjacentQuoteCount = 0
b.WriteString(`\"`)
} else {
b.WriteString(`"`)
}
case '\\':
b.WriteString(`\`)
default:
@@ -91,7 +103,30 @@ func encodeTomlString(value string) string {
return b.String()
}
func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElementPerLine bool) (string, error) {
func tomlTreeStringRepresentation(t *Tree, ord MarshalOrder) (string, error) {
var orderedVals []sortNode
switch ord {
case OrderPreserve:
orderedVals = sortByLines(t)
default:
orderedVals = sortAlphabetical(t)
}
var values []string
for _, node := range orderedVals {
k := node.key
v := t.values[k]
repr, err := tomlValueStringRepresentation(v, "", "", ord, false)
if err != nil {
return "", err
}
values = append(values, quoteKeyIfNeeded(k)+" = "+repr)
}
return "{ " + strings.Join(values, ", ") + " }", nil
}
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.
// That change was made to allow this function to see formatting options.
tv, ok := v.(*tomlValue)
@@ -123,12 +158,20 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil
case string:
if tv.multiline {
return "\"\"\"\n" + encodeMultilineTomlString(value) + "\"\"\"", 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
case []byte:
b, _ := v.([]byte)
return tomlValueStringRepresentation(string(b), indent, arraysOneElementPerLine)
return string(b), nil
case bool:
if value {
return "true", nil
@@ -142,6 +185,8 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
return value.String(), nil
case LocalTime:
return value.String(), nil
case *Tree:
return tomlTreeStringRepresentation(value, ord)
case nil:
return "", nil
}
@@ -152,7 +197,7 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
var values []string
for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface()
itemRepr, err := tomlValueStringRepresentation(item, indent, arraysOneElementPerLine)
itemRepr, err := tomlValueStringRepresentation(item, commented, indent, ord, arraysOneElementPerLine)
if err != nil {
return "", err
}
@@ -166,22 +211,24 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
for _, value := range values {
stringBuffer.WriteString(valueIndent)
stringBuffer.WriteString(value)
stringBuffer.WriteString(commented + value)
stringBuffer.WriteString(`,`)
stringBuffer.WriteString("\n")
}
stringBuffer.WriteString(indent + "]")
stringBuffer.WriteString(indent + commented + "]")
return stringBuffer.String(), nil
}
return "[" + strings.Join(values, ",") + "]", nil
return "[" + strings.Join(values, ", ") + "]", nil
}
return "", fmt.Errorf("unsupported value type %T: %v", v, v)
}
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 {
if tv.position.Line < line || line == 0 {
line = tv.position.Line
@@ -270,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) {
return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical)
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) (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
switch ord {
@@ -289,14 +336,10 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
k := node.key
v := t.values[k]
combinedKey := k
combinedKey := quoteKeyIfNeeded(k)
if keyspace != "" {
combinedKey = keyspace + "." + combinedKey
}
var commented string
if t.commented {
commented = "# "
}
switch node := v.(type) {
// node has to be of those two types given how keys are sorted above
@@ -317,24 +360,33 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
return bytesCount, errc
}
}
var commented string
if parentCommented || t.commented || tv.commented {
commented = "# "
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = node.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord)
bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || tv.commented)
if err != nil {
return bytesCount, err
}
case []*Tree:
for _, subTree := range node {
var commented string
if parentCommented || t.commented || subTree.commented {
commented = "# "
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = subTree.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord)
bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || subTree.commented)
if err != nil {
return bytesCount, err
}
@@ -347,7 +399,11 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine)
var commented string
if parentCommented || t.commented || v.commented {
commented = "# "
}
repr, err := tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine)
if err != nil {
return bytesCount, err
}
@@ -358,17 +414,20 @@ func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount i
if strings.HasPrefix(comment, "#") {
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)
if errc != nil {
return bytesCount, errc
}
}
var commented string
if v.commented {
commented = "# "
}
quotedKey := quoteKeyIfNeeded(k)
writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
bytesCount += int64(writtenBytesCount)
@@ -468,8 +527,26 @@ func (t *Tree) ToMap() map[string]interface{} {
case *Tree:
result[k] = node.ToMap()
case *tomlValue:
result[k] = node.value
result[k] = tomlValueToGo(node.value)
}
}
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
}
+86
View File
@@ -236,6 +236,7 @@ func TestTreeWriteToMapExampleFile(t *testing.T) {
[]interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)},
},
"score": 4e-08,
},
}
testMaps(t, tree.ToMap(), expected)
@@ -294,6 +295,42 @@ func TestTreeWriteToMapWithArrayOfInlineTables(t *testing.T) {
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) {
tree, err := Load(`a = 3.0`)
if err != nil {
@@ -327,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) {
tomlString :=
`[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)
}