Compare commits

...

38 Commits

Author SHA1 Message Date
Thomas Pelletier 618f0181ac AST Tweaks (#551)
* Use pointers instead of copying around ast.Node

Node is a 56B struct that is constantly in the hot path. Passing nodes
around by copy had a cost that started to add up. This change replaces
them by pointers. Using unsafe pointer arithmetic and converting
sibling/child indexes to relative offsets, it removes the need to carry
around a pointer to the root of the tree. This saves 8B per Node. This
space will be used to store an extra []byte slice to provide contextual
error handling on all nodes, including the ones whose data is different
than the raw input (for example: strings with escaped characters), while
staying under the size of a cache line.

* Remove conditional

* Add Raw to track range in data for parsed values

* Simplify reference tracking
2021-06-03 21:48:51 -04:00
Thomas Pelletier f3bb20ea79 Benchmark marshal (#550) 2021-06-02 09:29:19 -04:00
Thomas Pelletier b0d6c62255 Don't use bytes.Buffer when not necessary (#549)
When parsing strings, they can be referenced directly from the document
when they don't contain escaped characters. This avoids paying to cost
of allocating (and sometimes growing) the bytes buffer unecessarily.
2021-06-01 09:51:59 -04:00
Thomas Pelletier b202375414 Add benchmarks results to readme (#548) 2021-06-01 09:10:17 -04:00
Thomas Pelletier 250e073408 Stack-based unmarshaler (#546)
* Benchmark script

* Rewrite unmarshaler using the stack

Instead of tracking the build chain using `target`s, use the stack
instead.

Working and most benchmarks look good, but regression on structs unmarshalling.

~60% slower on ReferenceFile/struct.

* Shortcut to check if last node of iterator

* Remove unecessary pointer allocation

* Skip over unused keys without marking them as seen

* Add some tests

* Fix mktemp on macos
2021-05-31 12:14:13 -04:00
Thomas Pelletier 11f022ab09 Fix parser skipping over the whole file (#547)
* Add test for ReferenceFile/struct
* Stop skipping after table
* Add .gitattributes to force LF encoding
* Fix the reference file
2021-05-30 18:53:07 -04:00
Thomas Pelletier 840df4a229 seen tracker: use array for storage (#545)
name                              old time/op    new time/op    delta
UnmarshalDataset/config-32          81.2ms ± 3%    77.8ms ± 3%   -4.25%  (p=0.000 n=20+19)
UnmarshalDataset/canada-32           104ms ± 5%     105ms ± 4%     ~     (p=0.102 n=20+20)
UnmarshalDataset/citm_catalog-32    57.5ms ± 5%    59.0ms ± 5%   +2.54%  (p=0.033 n=20+20)
UnmarshalDataset/twitter-32         25.7ms ± 7%    28.1ms ± 5%   +9.33%  (p=0.000 n=20+20)
UnmarshalDataset/code-32             305ms ± 6%     292ms ± 5%   -4.29%  (p=0.000 n=20+19)
UnmarshalDataset/example-32          519µs ± 6%     522µs ± 5%     ~     (p=0.659 n=20+20)
UnmarshalSimple/struct-32           1.44µs ± 1%    1.17µs ± 6%  -18.78%  (p=0.000 n=14+20)
UnmarshalSimple/map-32              2.30µs ± 4%    1.99µs ± 4%  -13.65%  (p=0.000 n=20+19)
ReferenceFile/struct-32             44.1µs ± 4%    38.1µs ± 5%  -13.61%  (p=0.000 n=18+20)
ReferenceFile/map-32                 197µs ± 7%     189µs ± 5%   -3.91%  (p=0.000 n=20+20)
HugoFrontMatter-32                  45.9µs ± 6%    39.3µs ± 6%  -14.46%  (p=0.000 n=19+20)

name                              old speed      new speed      delta
UnmarshalDataset/config-32        12.9MB/s ± 3%  13.5MB/s ± 3%   +4.42%  (p=0.000 n=20+19)
UnmarshalDataset/canada-32        21.1MB/s ± 5%  20.9MB/s ± 4%     ~     (p=0.101 n=20+20)
UnmarshalDataset/citm_catalog-32  9.72MB/s ± 6%  9.47MB/s ± 5%   -2.53%  (p=0.031 n=20+20)
UnmarshalDataset/twitter-32       17.2MB/s ± 7%  15.8MB/s ± 5%   -8.57%  (p=0.000 n=20+20)
UnmarshalDataset/code-32          8.81MB/s ± 7%  9.20MB/s ± 5%   +4.47%  (p=0.000 n=20+19)
UnmarshalDataset/example-32       15.6MB/s ± 6%  15.5MB/s ± 5%     ~     (p=0.644 n=20+20)
UnmarshalSimple/struct-32         7.61MB/s ± 1%  9.39MB/s ± 7%  +23.33%  (p=0.000 n=15+20)
UnmarshalSimple/map-32            4.78MB/s ± 4%  5.54MB/s ± 5%  +15.85%  (p=0.000 n=20+19)
ReferenceFile/struct-32            119MB/s ± 4%   138MB/s ± 5%  +15.79%  (p=0.000 n=18+20)
ReferenceFile/map-32              26.6MB/s ± 7%  27.7MB/s ± 5%   +4.06%  (p=0.000 n=20+20)
HugoFrontMatter-32                11.9MB/s ± 6%  13.9MB/s ± 6%  +16.91%  (p=0.000 n=19+20)

name                              old alloc/op   new alloc/op   delta
UnmarshalDataset/config-32          16.9MB ± 0%    13.9MB ± 0%  -17.48%  (p=0.000 n=18+18)
UnmarshalDataset/canada-32          74.3MB ± 0%    74.3MB ± 0%   -0.00%  (p=0.000 n=20+20)
UnmarshalDataset/citm_catalog-32    37.3MB ± 0%    37.3MB ± 0%   +0.11%  (p=0.000 n=20+20)
UnmarshalDataset/twitter-32         15.6MB ± 0%    15.6MB ± 0%     ~     (p=0.211 n=19+20)
UnmarshalDataset/code-32            59.5MB ± 0%    52.4MB ± 0%  -11.96%  (p=0.000 n=19+20)
UnmarshalDataset/example-32          238kB ± 0%     239kB ± 0%   +0.02%  (p=0.000 n=18+20)
UnmarshalSimple/struct-32             981B ± 0%      709B ± 0%  -27.73%  (p=0.000 n=20+20)
UnmarshalSimple/map-32              1.45kB ± 0%    1.17kB ± 0%  -18.82%  (p=0.000 n=20+20)
ReferenceFile/struct-32             11.8kB ± 0%     9.7kB ± 0%  -17.64%  (p=0.000 n=20+20)
ReferenceFile/map-32                51.5kB ± 0%    52.2kB ± 0%   +1.30%  (p=0.000 n=20+17)
HugoFrontMatter-32                  12.1kB ± 0%    11.1kB ± 0%   -7.97%  (p=0.000 n=20+19)

name                              old allocs/op  new allocs/op  delta
UnmarshalDataset/config-32            645k ± 0%      557k ± 0%  -13.76%  (p=0.000 n=20+16)
UnmarshalDataset/canada-32            896k ± 0%      896k ± 0%   -0.00%  (p=0.000 n=20+20)
UnmarshalDataset/citm_catalog-32      380k ± 0%      377k ± 0%   -0.75%  (p=0.000 n=19+20)
UnmarshalDataset/twitter-32           158k ± 0%      158k ± 0%   -0.01%  (p=0.000 n=18+18)
UnmarshalDataset/code-32             2.92M ± 0%     2.57M ± 0%  -11.87%  (p=0.000 n=20+20)
UnmarshalDataset/example-32          3.66k ± 0%     3.64k ± 0%   -0.63%  (p=0.000 n=20+20)
UnmarshalSimple/struct-32             13.0 ± 0%      10.0 ± 0%  -23.08%  (p=0.000 n=20+20)
UnmarshalSimple/map-32                22.0 ± 0%      19.0 ± 0%  -13.64%  (p=0.000 n=20+20)
ReferenceFile/struct-32                253 ± 0%       155 ± 0%  -38.74%  (p=0.000 n=20+20)
ReferenceFile/map-32                 1.67k ± 0%     1.44k ± 0%  -14.23%  (p=0.000 n=20+20)
HugoFrontMatter-32                     357 ± 0%       313 ± 0%  -12.32%  (p=0.000 n=20+20)
2021-05-26 18:47:00 -04:00
Thomas Pelletier c2d1fd86e5 Fix timezone detection when time has fractional component (#544) 2021-05-21 09:37:43 -04:00
Thomas Pelletier 238a6fef7d Add links to proper feedback channels 2021-05-15 08:55:07 -04:00
Thomas Pelletier 67852cf007 Clarify default struct tag being unsupported (#543)
Follow up to https://github.com/pelletier/go-toml/issues/542
2021-05-15 08:49:15 -04:00
Thomas Pelletier d276c42adc Run coverage test on branches only 2021-05-10 20:22:12 -04:00
Thomas Pelletier 95c701b253 Increase test coverage (#538)
Also fix array in map bug.
2021-05-10 20:17:05 -04:00
Thomas Pelletier 3db329a512 ci: basic github action for coverage (#537) 2021-05-09 17:37:03 -04:00
Thomas Pelletier 45ea20024b Readme (#535) 2021-05-08 17:03:51 -04:00
Thomas Pelletier ea225df3ed v2: errors (#534)
```
name                              old time/op    new time/op    delta
UnmarshalDataset/config-32          86.7ms ± 2%    87.5ms ± 2%     ~     (p=0.113 n=9+10)
UnmarshalDataset/canada-32           129ms ± 4%     106ms ± 3%  -17.94%  (p=0.000 n=10+10)
UnmarshalDataset/citm_catalog-32    59.4ms ± 5%    58.7ms ± 5%     ~     (p=0.393 n=10+10)
UnmarshalDataset/twitter-32         27.0ms ± 7%    26.9ms ± 6%     ~     (p=0.720 n=10+9)
UnmarshalDataset/code-32             326ms ± 4%     322ms ± 7%     ~     (p=0.661 n=9+10)
UnmarshalDataset/example-32          510µs ±11%     526µs ± 7%     ~     (p=0.182 n=10+9)
UnmarshalSimple-32                  1.41µs ± 6%    1.41µs ± 4%     ~     (p=0.736 n=10+9)
ReferenceFile-32                    45.6µs ± 3%    43.9µs ±10%     ~     (p=0.089 n=10+10)

name                              old speed      new speed      delta
UnmarshalDataset/config-32        12.1MB/s ± 2%  12.0MB/s ± 2%     ~     (p=0.108 n=9+10)
UnmarshalDataset/canada-32        17.1MB/s ± 4%  20.9MB/s ± 3%  +21.86%  (p=0.000 n=10+10)
UnmarshalDataset/citm_catalog-32  9.41MB/s ± 5%  9.51MB/s ± 5%     ~     (p=0.362 n=10+10)
UnmarshalDataset/twitter-32       16.4MB/s ± 8%  16.5MB/s ± 6%     ~     (p=0.704 n=10+9)
UnmarshalDataset/code-32          8.24MB/s ± 4%  8.34MB/s ± 7%     ~     (p=0.675 n=9+10)
UnmarshalDataset/example-32       15.9MB/s ±11%  15.4MB/s ± 7%     ~     (p=0.182 n=10+9)
ReferenceFile-32                   115MB/s ± 4%   120MB/s ±10%     ~     (p=0.085 n=10+10)

name                              old alloc/op   new alloc/op   delta
UnmarshalDataset/config-32          16.9MB ± 0%    16.9MB ± 0%   -0.02%  (p=0.000 n=10+10)
UnmarshalDataset/canada-32          76.8MB ± 0%    74.3MB ± 0%   -3.31%  (p=0.000 n=10+10)
UnmarshalDataset/citm_catalog-32    37.3MB ± 0%    37.1MB ± 0%   -0.60%  (p=0.000 n=9+10)
UnmarshalDataset/twitter-32         15.6MB ± 0%    15.6MB ± 0%   -0.09%  (p=0.000 n=10+10)
UnmarshalDataset/code-32            60.2MB ± 0%    59.3MB ± 0%   -1.51%  (p=0.000 n=10+9)
UnmarshalDataset/example-32          238kB ± 0%     238kB ± 0%   -0.18%  (p=0.000 n=10+10)
ReferenceFile-32                    11.8kB ± 0%    11.8kB ± 0%     ~     (all equal)

name                              old allocs/op  new allocs/op  delta
UnmarshalDataset/config-32            653k ± 0%      645k ± 0%   -1.20%  (p=0.000 n=10+6)
UnmarshalDataset/canada-32           1.01M ± 0%     0.90M ± 0%  -11.04%  (p=0.000 n=9+10)
UnmarshalDataset/citm_catalog-32      384k ± 0%      370k ± 0%   -3.75%  (p=0.000 n=10+10)
UnmarshalDataset/twitter-32           160k ± 0%      157k ± 0%   -1.32%  (p=0.000 n=10+10)
UnmarshalDataset/code-32             2.97M ± 0%     2.91M ± 0%   -2.15%  (p=0.000 n=10+7)
UnmarshalDataset/example-32          3.69k ± 0%     3.63k ± 0%   -1.52%  (p=0.000 n=10+10)
ReferenceFile-32                       253 ± 0%       253 ± 0%     ~     (all equal)
```
2021-05-08 16:04:25 -04:00
Thomas Pelletier 4545a3e94b ci: remove benchmarks
Both github actions and my own VPS have too much noise to be useful.
2021-05-07 23:34:17 -04:00
Vincent Serpoul 3f2bb0b363 golangci-lint (#530) 2021-05-06 22:29:21 -04:00
Vincent Serpoul 201d5dd422 golangci-lint: misc (#529) 2021-04-27 20:29:00 -04:00
Thomas Pelletier 1e80267558 parser: require \n after parsing integer in kv (#527)
Fixes #526
2021-04-24 09:57:21 -04:00
Thomas Pelletier 931f02a519 encoder: support indentation (#525) 2021-04-23 17:08:27 -04:00
Thomas Pelletier a533331aee v2: benchdiff (#524) 2021-04-23 15:21:41 -04:00
Vincent Serpoul 466faaab9f golangci-lint: marshaler, strict (#523) 2021-04-23 10:41:21 -04:00
Thomas Pelletier e443b4fdb8 encoder: support TextMarshaler (#522)
Fixes #521
2021-04-22 10:13:41 -04:00
Vincent Serpoul 2b1c52dddd golangci-lint: decoder/unmarshal (#518) 2021-04-22 09:29:23 -04:00
Thomas Pelletier 21445f5170 Add test for issue #424 2021-04-21 22:27:30 -04:00
Thomas Pelletier 9ba52996d8 Encoder multiline array (#520) 2021-04-21 22:13:45 -04:00
Thomas Pelletier 6fe332a869 Encoder inline tables (#519) 2021-04-21 19:11:15 -04:00
Thomas Pelletier 32c1a8d372 encoder: move nspow into the parseLocalTime 2021-04-20 23:19:40 -04:00
Thomas Pelletier ee102a3528 decoder: fix time fractional parsing 2021-04-20 23:16:08 -04:00
Thomas Pelletier 9b67e40640 decoder: strict mode (#512) 2021-04-20 21:26:22 -04:00
Vincent Serpoul dca2103910 golangci-lint: marshaler (#516) 2021-04-20 20:24:44 -04:00
Cameron Moore a713a96e69 Add more newline tests for scanner (#515) 2021-04-16 19:07:29 -04:00
Cameron Moore a7b50eb8f1 Tidy (#511)
* Disconnect package godoc comment from imported file

* Add missing newline in toml.abnf

* Tag testing helper funcs
2021-04-15 16:49:19 -04:00
Cameron Moore 24b62ebe61 Simplify scanFollows usage (#510)
Use static functions to avoid declaring global vars and creating more
package init costs.  This change has no negative effects on benchmarks
in my testing.
2021-04-15 16:48:19 -04:00
Thomas Pelletier 9bc4641a49 ci-lint: disable ifshort 2021-04-15 13:37:24 -04:00
Thomas Pelletier b86b890b8d decoder: handle private anonymous structs
Ref #508
2021-04-15 12:49:24 -04:00
Vincent Serpoul 080baa8574 golangci-lint: localtime (#509) 2021-04-15 12:44:31 -04:00
Thomas Pelletier 0537b928df decoder: add test for #507 2021-04-15 11:36:36 -04:00
47 changed files with 6413 additions and 2679 deletions
+3
View File
@@ -0,0 +1,3 @@
* text=auto
benchmark/benchmark.toml text eol=lf
+16 -2
View File
@@ -1,5 +1,19 @@
**Issue:** add link to pelletier/go-toml issue here <!--
Thank you for your pull request!
Please read the Code changes section of the CONTRIBUTING.md file,
and make sure you have followed the instructions.
https://github.com/pelletier/go-toml/blob/v2/CONTRIBUTING.md#code-changes
-->
Explanation of what this pull request does. Explanation of what this pull request does.
More detailed description of the decisions being made and the reasons why (if the patch is non-trivial). More detailed description of the decisions being made and the reasons why (if
the patch is non-trivial).
---
Paste `benchstat` results here
+20
View File
@@ -0,0 +1,20 @@
name: coverage
on:
pull_request:
branches:
- v2
jobs:
report:
runs-on: "ubuntu-latest"
name: report
steps:
- uses: actions/checkout@master
with:
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@master
with:
go-version: 1.16
- name: Run tests with coverage
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
-3
View File
@@ -23,6 +23,3 @@ jobs:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Run unit tests - name: Run unit tests
run: go test -race ./... run: go test -race ./...
- name: Run benchmark tests
run: go test -race ./...
working-directory: benchmark
+12 -9
View File
@@ -4,6 +4,9 @@ golangci-lint-version = "1.39.0"
[linters-settings.wsl] [linters-settings.wsl]
allow-assign-and-anything = true allow-assign-and-anything = true
[linters-settings.exhaustive]
default-signifies-exhaustive = true
[linters] [linters]
disable-all = true disable-all = true
enable = [ enable = [
@@ -18,13 +21,13 @@ enable = [
"errcheck", "errcheck",
"errorlint", "errorlint",
"exhaustive", "exhaustive",
"exhaustivestruct", # "exhaustivestruct",
"exportloopref", "exportloopref",
"forbidigo", "forbidigo",
"forcetypeassert", # "forcetypeassert",
"funlen", "funlen",
"gci", "gci",
"gochecknoglobals", # "gochecknoglobals",
"gochecknoinits", "gochecknoinits",
"gocognit", "gocognit",
"goconst", "goconst",
@@ -32,7 +35,7 @@ enable = [
"gocyclo", "gocyclo",
"godot", "godot",
"godox", "godox",
"goerr113", # "goerr113",
"gofmt", "gofmt",
"gofumpt", "gofumpt",
"goheader", "goheader",
@@ -45,7 +48,7 @@ enable = [
"gosec", "gosec",
"gosimple", "gosimple",
"govet", "govet",
"ifshort", # "ifshort",
"importas", "importas",
"ineffassign", "ineffassign",
"lll", "lll",
@@ -54,10 +57,10 @@ enable = [
"nakedret", "nakedret",
"nestif", "nestif",
"nilerr", "nilerr",
"nlreturn", # "nlreturn",
"noctx", "noctx",
"nolintlint", "nolintlint",
"paralleltest", #"paralleltest",
"prealloc", "prealloc",
"predeclared", "predeclared",
"revive", "revive",
@@ -76,6 +79,6 @@ enable = [
"varcheck", "varcheck",
"wastedassign", "wastedassign",
"whitespace", "whitespace",
"wrapcheck", # "wrapcheck",
"wsl" # "wsl"
] ]
+113 -63
View File
@@ -1,74 +1,74 @@
## Contributing # Contributing
Thank you for your interest in go-toml! We appreciate you considering Thank you for your interest in go-toml! We appreciate you considering
contributing to go-toml! contributing to go-toml!
The main goal is the project is to provide an easy-to-use TOML The main goal is the project is to provide an easy-to-use and efficient TOML
implementation for Go that gets the job done and gets out of your way implementation for Go that gets the job done and gets out of your way dealing
dealing with TOML is probably not the central piece of your project. with TOML is probably not the central piece of your project.
As the single maintainer of go-toml, time is scarce. All help, big or As the single maintainer of go-toml, time is scarce. All help, big or small, is
small, is more than welcomed! more than welcomed!
### Ask questions ## Ask questions
Any question you may have, somebody else might have it too. Always feel Any question you may have, somebody else might have it too. Always feel free to
free to ask them on the [issues tracker][issues-tracker]. We will try to ask them on the [discussion board][discussions]. We will try to answer them as
answer them as clearly and quickly as possible, time permitting. clearly and quickly as possible, time permitting.
Asking questions also helps us identify areas where the documentation needs Asking questions also helps us identify areas where the documentation needs
improvement, or new features that weren't envisioned before. Sometimes, a improvement, or new features that weren't envisioned before. Sometimes, a
seemingly innocent question leads to the fix of a bug. Don't hesitate and seemingly innocent question leads to the fix of a bug. Don't hesitate and ask
ask away! away!
### Improve the documentation [discussions]: https://github.com/pelletier/go-toml/discussions
The best way to share your knowledge and experience with go-toml is to ## Improve the documentation
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 The best way to share your knowledge and experience with go-toml is to improve
source code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a the documentation. Fix a typo, clarify an interface, add an example, anything
change to the documentation, create a pull request with your proposed goes!
changes. For simple changes like that, the easiest way to go is probably
the "Fork this project and edit the file" button on Github, displayed at
the top right of the file. Unless it's a trivial change (for example a
typo), provide a little bit of context in your pull request description or
commit message.
### Report a bug The documentation is present in the [README][readme] and thorough the 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 the top right of the
file. Unless it's a trivial change (for example a typo), provide a little bit of
context in your pull request description or commit message.
Found a bug! Sorry to hear that :(. Help us and other track them down and ## Report a bug
fix by reporting it. [File a new bug report][bug-report] on the [issues
tracker][issues-tracker]. The template should provide enough guidance on
what to include. When in doubt: add more details! By reducing ambiguity and
providing more information, it decreases back and forth and saves everyone
time.
### Code changes Found a bug! Sorry to hear that :(. Help us and other track them down and fix by
reporting it. [File a new bug report][bug-report] on the [issues
tracker][issues-tracker]. The template should provide enough guidance on what to
include. When in doubt: add more details! By reducing ambiguity and providing
more information, it decreases back and forth and saves everyone time.
## Code changes
Want to contribute a patch? Very happy to hear that! Want to contribute a patch? Very happy to hear that!
First, some high-level rules: First, some high-level rules:
* A short proposal with some POC code is better than a lengthy piece of - A short proposal with some POC code is better than a lengthy piece of text
text with no code. Code speaks louder than words. with no code. Code speaks louder than words. That being said, bigger changes
* No backward-incompatible patch will be accepted unless discussed. should probably start with a [discussion][discussions].
Sometimes it's hard, and Go's lack of versioning by default does not - No backward-incompatible patch will be accepted unless discussed. Sometimes
help, but we try not to break people's programs unless we absolutely have it's hard, but we try not to break people's programs unless we absolutely have
to. to.
* If you are writing a new feature or extending an existing one, make sure - If you are writing a new feature or extending an existing one, make sure to
to write some documentation. write some documentation.
* Bug fixes need to be accompanied with regression tests. - Bug fixes need to be accompanied with regression tests.
* New code needs to be tested. - New code needs to be tested.
* Your commit messages need to explain why the change is needed, even if - Your commit messages need to explain why the change is needed, even if already
already included in the PR description. included in the PR description.
It does sound like a lot, but those best practices are here to save time It does sound like a lot, but those best practices are here to save time overall
overall and continuously improve the quality of the project, which is and continuously improve the quality of the project, which is something everyone
something everyone benefits from. benefits from.
#### Get started ### Get started
The fairly standard code contribution process looks like that: The fairly standard code contribution process looks like that:
@@ -76,42 +76,92 @@ The fairly standard code contribution process looks like that:
2. Make your changes, commit on any branch you like. 2. Make your changes, commit on any branch you like.
3. [Open up a pull request][pull-request] 3. [Open up a pull request][pull-request]
4. Review, potential ask for changes. 4. Review, potential ask for changes.
5. Merge. You're in! 5. Merge.
Feel free to ask for help! You can create draft pull requests to gather Feel free to ask for help! You can create draft pull requests to gather
some early feedback! some early feedback!
#### Run the tests ### Run the tests
You can run tests for go-toml using Go's test tool: `go test ./...`. You can run tests for go-toml using Go's test tool: `go test -race ./...`.
When creating a pull requests, all tests will be ran on Linux on a few Go
versions (Travis CI), and on Windows using the latest Go version
(AppVeyor).
#### Style During the pull request process, all tests will be ran on Linux, Windows, and
MacOS on the last two versions of Go.
Try to look around and follow the same format and structure as the rest of However, given GitHub's new policy to _not_ run Actions on pull requests until a
the code. We enforce using `go fmt` on the whole code base. maintainer clicks on button, it is highly recommended that you run them locally
as you make changes.
### Check coverage
We use `go tool cover` to compute test coverage. Most code editors have a way to
run and display code coverage, but at the end of the day, we do this:
```
go test -covermode=atomic -coverprofile=coverage.out
go tool cover -func=coverage.out
```
and verify that the overall percentage of tested code does not go down. This is
a requirement. As a rule of thumb, all lines of code touched by your changes
should be covered. On Unix you can use `./ci.sh coverage -d v2` to check if your
code lowers the coverage.
### Verify performance
Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's
builtin benchmark systems. Because of their noisy nature, containers provided by
Github Actions cannot be reliably used for benchmarking. As a result, you are
responsible for checking that your changes do not incur a performance penalty.
You can run their following to execute benchmarks:
```
go test ./... -bench=. -count=10
```
Benchmark results should be compared against each other with
[benchstat][benchstat]. Typical flow looks like this:
1. On the `v2` branch, run `go test ./... -bench=. -count 10` and save output to
a file (for example `old.txt`).
2. Make some code changes.
3. Run `go test ....` again, and save the output to an other file (for example
`new.txt`).
4. Run `benchstat old.txt new.txt` to check that time/op does not go up in any
test.
On Unix you can use `./ci.sh benchmark -d v2` to verify how your code impacts
performance.
It is highly encouraged to add the benchstat results to your pull request
description. Pull requests that lower performance will receive more scrutiny.
[benchstat]: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
### Style
Try to look around and follow the same format and structure as the rest of the
code. We enforce using `go fmt` on the whole code base.
--- ---
### Maintainers-only ## Maintainers-only
#### Merge pull request ### Merge pull request
Checklist: Checklist:
* Passing CI. - Passing CI.
* Does not introduce backward-incompatible changes (unless discussed). - Does not introduce backward-incompatible changes (unless discussed).
* Has relevant doc changes. - Has relevant doc changes.
* Has relevant unit tests. - Benchstat does not show performance regression.
1. Merge using "squash and merge". 1. Merge using "squash and merge".
2. Make sure to edit the commit message to keep all the useful information 2. Make sure to edit the commit message to keep all the useful information
nice and clean. nice and clean.
3. Make sure the commit title is clear and contains the PR number (#123). 3. Make sure the commit title is clear and contains the PR number (#123).
#### New release ### New release
1. Go to [releases][releases]. Click on "X commits to master since this 1. Go to [releases][releases]. Click on "X commits to master since this
release". release".
+392 -40
View File
@@ -1,57 +1,409 @@
# go-toml V2 # go-toml v2
Development branch. Use at your own risk. Go library for the [TOML](https://toml.io/en/) format.
[👉 Discussion on github](https://github.com/pelletier/go-toml/discussions/471). This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
* `toml.Unmarshal()` should work as well as v1. ## Development status
## Must do This is the upcoming major version of go-toml. It is currently in active
development. As of release v2.0.0-beta.1, the library has reached feature parity
with v1, and fixes a lot known bugs and performance issues along the way.
### Unmarshal If you do not need the advanced document editing features of v1, you are
encouraged to try out this version.
- [x] Unmarshal into maps. [👉 Roadmap for v2](https://github.com/pelletier/go-toml/discussions/506)
- [x] Support Array Tables.
- [x] Unmarshal into pointers.
- [x] Support Date / times.
- [x] Support struct tags annotations.
- [x] Support Arrays.
- [x] Support Unmarshaler interface.
- [x] Original go-toml unmarshal tests pass.
- [x] Benchmark!
- [x] Abstract AST.
- [x] Original go-toml testgen tests pass.
- [x] Track file position (line, column) for errors.
- [ ] Strict mode.
- [ ] Document Unmarshal / Decode
### Marshal [🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
- [x] Minimal implementation [💬 Anything else](https://github.com/pelletier/go-toml/discussions)
- [x] Multiline strings
- [ ] Multiline arrays
- [ ] `inline` tag for tables
- [ ] Optional indentation
- [ ] Option to pick default quotes
### Document ## Documentation
- [ ] Gather requirements and design API. Full API, examples, and implementation notes are available in the Go documentation.
## Ideas [![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml/v2.svg)](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
- [ ] Allow types to implement a `ASTUnmarshaler` interface to unmarshal ## Import
straight from the AST?
- [x] Rewrite AST to use a single array as storage instead of one allocation per
node.
- [ ] Provide "minimal allocations" option that uses `unsafe` to reuse the input
byte array as storage for strings.
- [x] Cache reflection operations per type.
- [ ] Optimize tracker pass.
## Differences with v1 ```go
import "github.com/pelletier/go-toml/v2"
```
* [unmarshal](https://github.com/pelletier/go-toml/discussions/488) ## Features
### Stdlib behavior
As much as possible, this library is designed to behave similarly as the
standard library's `encoding/json`.
### Performance
While go-toml favors usability, it is written with performance in mind. Most
operations should not be shockingly slow. See [benchmarks](#benchmarks).
### Strict mode
`Decoder` can be set to "strict mode", which makes it error when some parts of
the TOML document was not prevent in the target structure. This is a great way
to check for typos. [See example in the documentation][strict].
[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.SetStrict
### Contextualized errors
When decoding errors occur, go-toml returns [`DecodeError`][decode-err]), which
contains a human readable contextualized version of the error. For example:
```
2| key1 = "value1"
3| key2 = "missing2"
| ~~~~ missing field
4| key3 = "missing3"
5| key4 = "value4"
```
[decode-err]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#DecodeError
### Local date and time support
TOML supports native [local date/times][ldt]. It allows to represent a given
date, time, or date-time without relation to a timezone or offset. To support
this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
[`LocalDateTime`][tldt]. Those types can be transformed to and from `time.Time`,
making them convenient yet unambiguous structures for their respective TOML
representation.
[ldt]: https://toml.io/en/v1.0.0#local-date-time
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
## Getting started
Given the following struct, let's see how to read it and write it as TOML:
```go
type MyConfig struct {
Version int
Name string
Tags []string
}
```
### Unmarshaling
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
content. For example:
```go
doc := `
version = 2
name = "go-toml"
tags = ["go", "toml"]
`
var cfg MyConfig
err := toml.Unmarshal([]byte(doc), &cfg)
if err != nil {
panic(err)
}
fmt.Println("version:", cfg.Version)
fmt.Println("name:", cfg.Name)
fmt.Println("tags:", cfg.Tags)
// Output:
// version: 2
// name: go-toml
// tags: [go toml]
```
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
### Marshaling
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
as a TOML document:
```go
cfg := MyConfig{
Version: 2,
Name: "go-toml",
Tags: []string{"go", "toml"},
}
b, err := toml.Marshal(cfg)
if err != nil {
panic(err)
}
fmt.Println(string(b))
// Output:
// Version = 2
// Name = 'go-toml'
// Tags = ['go', 'toml']
```
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
## Benchmarks
Execution time speedup compared to other Go TOML libraries:
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/HugoFrontMatter</td><td>2.0x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/map</td><td>1.8x</td><td>2.0x</td></tr>
<tr><td>Marshal/ReferenceFile/struct</td><td>2.7x</td><td>2.7x</td></tr>
<tr><td>Unmarshal/HugoFrontMatter</td><td>3.0x</td><td>2.6x</td></tr>
<tr><td>Unmarshal/ReferenceFile/map</td><td>3.0x</td><td>3.1x</td></tr>
<tr><td>Unmarshal/ReferenceFile/struct</td><td>5.9x</td><td>6.6x</td></tr>
</tbody>
</table>
<details><summary>See more</summary>
<p>The table above has the results of the most common use-cases. The table below
contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p>
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>
<tr><td>Marshal/SimpleDocument/map</td><td>1.7x</td><td>2.1x</td></tr>
<tr><td>Marshal/SimpleDocument/struct</td><td>2.6x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/SimpleDocument/map</td><td>4.1x</td><td>2.9x</td></tr>
<tr><td>Unmarshal/SimpleDocument/struct</td><td>6.3x</td><td>4.1x</td></tr>
<tr><td>UnmarshalDataset/example</td><td>3.5x</td><td>2.4x</td></tr>
<tr><td>UnmarshalDataset/code</td><td>2.2x</td><td>2.8x</td></tr>
<tr><td>UnmarshalDataset/twitter</td><td>2.8x</td><td>2.1x</td></tr>
<tr><td>UnmarshalDataset/citm_catalog</td><td>2.3x</td><td>1.5x</td></tr>
<tr><td>UnmarshalDataset/config</td><td>4.2x</td><td>3.2x</td></tr>
<tr><td>[Geo mean]</td><td>3.0x</td><td>2.7x</td></tr>
</tbody>
</table>
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
</details>
## Migrating from v1
This section describes the differences between v1 and v2, with some pointers on
how to get the original behavior when possible.
### Decoding / Unmarshal
#### Automatic field name guessing
When unmarshaling to a struct, if a key in the TOML document does not exactly
match the name of a struct field or any of the `toml`-tagged field, v1 tries
multiple variations of the key ([code][v1-keys]).
V2 instead does a case-insensitive matching, like `encoding/json`.
This could impact you if you are relying on casing to differentiate two fields,
and one of them is a not using the `toml` struct tag. The recommended solution
is to be specific about tag names for those fields using the `toml` struct tag.
[v1-keys]: https://github.com/pelletier/go-toml/blob/a2e52561804c6cd9392ebf0048ca64fe4af67a43/marshal.go#L775-L781
#### Ignore preexisting value in interface
When decoding into a non-nil `interface{}`, go-toml v1 uses the type of the
element in the interface to decode the object. For example:
```go
type inner struct {
B interface{}
}
type doc struct {
A interface{}
}
d := doc{
A: inner{
B: "Before",
},
}
data := `
[A]
B = "After"
`
toml.Unmarshal([]byte(data), &d)
fmt.Printf("toml v1: %#v\n", d)
// toml v1: main.doc{A:main.inner{B:"After"}}
```
In this case, field `A` is of type `interface{}`, containing a `inner` struct.
V1 sees that type and uses it when decoding the object.
When decoding an object into an `interface{}`, V2 instead disregards whatever
value the `interface{}` may contain and replaces it with a
`map[string]interface{}`. With the same data structure as above, here is what
the result looks like:
```go
toml.Unmarshal([]byte(data), &d)
fmt.Printf("toml v2: %#v\n", d)
// toml v2: main.doc{A:map[string]interface {}{"B":"After"}}
```
This is to match `encoding/json`'s behavior. There is no way to make the v2
decoder behave like v1.
#### Values out of array bounds ignored
When decoding into an array, v1 returns an error when the number of elements
contained in the doc is superior to the capacity of the array. For example:
```go
type doc struct {
A [2]string
}
d := doc{}
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
fmt.Println(err)
// (1, 1): unmarshal: TOML array length (3) exceeds destination array length (2)
```
In the same situation, v2 ignores the last value:
```go
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
fmt.Println("err:", err, "d:", d)
// err: <nil> d: {[one two]}
```
This is to match `encoding/json`'s behavior. There is no way to make the v2
decoder behave like v1.
#### Support for `toml.Unmarshaler` has been dropped
This method was not widely used, poorly defined, and added a lot of complexity.
A similar effect can be achieved by implementing the `encoding.TextUnmarshaler`
interface and use strings.
#### Support for `default` struct tag has been dropped
This feature adds complexity and a poorly defined API for an effect that can be
accomplished outside of the library.
It does not seem like other format parsers in Go support that feature (the
project referenced in the original ticket #202 has not been updated since 2017).
Given that go-toml v2 should not touch values not in the document, the same
effect can be achieved by pre-filling the struct with defaults (libraries like
[go-defaults][go-defaults] can help). Also, string representation is not well
defined for all types: it creates issues like #278.
The recommended replacement is pre-filling the struct before unmarshaling.
[go-defaults]: https://github.com/mcuadros/go-defaults
### Encoding / Marshal
#### Default struct fields order
V1 emits struct fields order alphabetically by default. V2 struct fields are
emitted in order they are defined. For example:
```go
type S struct {
B string
A string
}
data := S{
B: "B",
A: "A",
}
b, _ := tomlv1.Marshal(data)
fmt.Println("v1:\n" + string(b))
b, _ = tomlv2.Marshal(data)
fmt.Println("v2:\n" + string(b))
// Output:
// v1:
// A = "A"
// B = "B"
// v2:
// B = 'B'
// A = 'A'
```
There is no way to make v2 encoder behave like v1. A workaround could be to
manually sort the fields alphabetically in the struct definition.
#### No indentation by default
V1 automatically indents content of tables by default. V2 does not. However the
same behavior can be obtained using [`Encoder.SetIndentTables`][sit]. For example:
```go
data := map[string]interface{}{
"table": map[string]string{
"key": "value",
},
}
b, _ := tomlv1.Marshal(data)
fmt.Println("v1:\n" + string(b))
b, _ = tomlv2.Marshal(data)
fmt.Println("v2:\n" + string(b))
buf := bytes.Buffer{}
enc := tomlv2.NewEncoder(&buf)
enc.SetIndentTables(true)
enc.Encode(data)
fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
// Output:
// v1:
//
// [table]
// key = "value"
//
// v2:
// [table]
// key = 'value'
//
//
// v2 Encoder:
// [table]
// key = 'value'
```
[sit]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Encoder.SetIndentTables
#### Keys and strings are single quoted
V1 always uses double quotes (`"`) around strings and keys that cannot be
represented bare (unquoted). V2 uses single quotes instead by default (`'`),
unless a character cannot be represented, then falls back to double quotes.
There is no way to make v2 encoder behave like v1.
#### `TextMarshaler` emits as a string, not TOML
Types that implement [`encoding.TextMarshaler`][tm] can emit arbitrary TOML in
v1. The encoder would append the result to the output directly. In v2 the result
is wrapped in a string. As a result, this interface cannot be implemented by the
root object.
There is no way to make v2 encoder behave like v1.
[tm]: https://golang.org/pkg/encoding/#TextMarshaler
## License ## License
+25 -39
View File
@@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -30,65 +31,50 @@ var bench_inputs = []struct {
func TestUnmarshalDatasetCode(t *testing.T) { func TestUnmarshalDatasetCode(t *testing.T) {
for _, tc := range bench_inputs { for _, tc := range bench_inputs {
buf := fixture(t, tc.name)
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
for _, r := range runners { buf := fixture(t, tc.name)
if r.name == "bs" && tc.name == "canada" {
t.Skip("skipping: burntsushi can't handle mixed arrays")
}
t.Run(r.name, func(t *testing.T) { var v interface{}
var v interface{} require.NoError(t, toml.Unmarshal(buf, &v))
check(t, r.unmarshal(buf, &v))
b, err := json.Marshal(v) b, err := json.Marshal(v)
check(t, err) require.NoError(t, err)
require.Equal(t, len(b), tc.jsonLen) require.Equal(t, len(b), tc.jsonLen)
})
}
}) })
} }
} }
func BenchmarkUnmarshalDataset(b *testing.B) { func BenchmarkUnmarshalDataset(b *testing.B) {
for _, tc := range bench_inputs { for _, tc := range bench_inputs {
buf := fixture(b, tc.name)
b.Run(tc.name, func(b *testing.B) { b.Run(tc.name, func(b *testing.B) {
bench(b, func(r runner, b *testing.B) { buf := fixture(b, tc.name)
if r.name == "bs" && tc.name == "canada" { b.SetBytes(int64(len(buf)))
b.Skip("skipping: burntsushi can't handle mixed arrays") b.ReportAllocs()
} b.ResetTimer()
for i := 0; i < b.N; i++ {
b.SetBytes(int64(len(buf))) var v interface{}
b.ReportAllocs() require.NoError(b, toml.Unmarshal(buf, &v))
b.ResetTimer() }
for i := 0; i < b.N; i++ {
var v interface{}
check(b, r.unmarshal(buf, &v))
}
})
}) })
} }
} }
// fixture returns the uncompressed contents of path. // fixture returns the uncompressed contents of path.
func fixture(tb testing.TB, path string) []byte { func fixture(tb testing.TB, path string) []byte {
f, err := os.Open(filepath.Join("testdata", path+".toml.gz")) tb.Helper()
check(tb, err)
file := path + ".toml.gz"
f, err := os.Open(filepath.Join("testdata", file))
if os.IsNotExist(err) {
tb.Skip("benchmark fixture not found:", file)
}
require.NoError(tb, err)
defer f.Close() defer f.Close()
gz, err := gzip.NewReader(f) gz, err := gzip.NewReader(f)
check(tb, err) require.NoError(tb, err)
buf, err := ioutil.ReadAll(gz) buf, err := ioutil.ReadAll(gz)
check(tb, err) require.NoError(tb, err)
return buf return buf
} }
func check(tb testing.TB, err error) {
if err != nil {
tb.Helper()
tb.Fatal(err)
}
}
+1 -1
View File
@@ -186,7 +186,7 @@ key3 = 1979-05-27T00:32:00.999999-07:00
key1 = [ 1, 2, 3 ] key1 = [ 1, 2, 3 ]
key2 = [ "red", "yellow", "green" ] key2 = [ "red", "yellow", "green" ]
key3 = [ [ 1, 2 ], [3, 4, 5] ] key3 = [ [ 1, 2 ], [3, 4, 5] ]
#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok
# Arrays can also be multiline. So in addition to ignoring whitespace, arrays # Arrays can also be multiline. So in addition to ignoring whitespace, arrays
# also ignore newlines between the brackets. Terminating commas are ok before # also ignore newlines between the brackets. Terminating commas are ok before
+524 -49
View File
@@ -1,43 +1,103 @@
package benchmark_test package benchmark_test
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"testing" "testing"
"time" "time"
tomlbs "github.com/BurntSushi/toml"
tomlv1 "github.com/pelletier/go-toml-v1"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type runner struct { func TestUnmarshalSimple(t *testing.T) {
name string doc := []byte(`A = "hello"`)
unmarshal func([]byte, interface{}) error d := struct {
} A string
}{}
var runners = []runner{ err := toml.Unmarshal(doc, &d)
{"v2", toml.Unmarshal}, if err != nil {
{"v1", tomlv1.Unmarshal}, panic(err)
{"bs", tomlbs.Unmarshal},
}
func bench(b *testing.B, f func(r runner, b *testing.B)) {
for _, r := range runners {
b.Run(r.name, func(b *testing.B) {
f(r, b)
})
} }
} }
func BenchmarkUnmarshalSimple(b *testing.B) { func BenchmarkUnmarshal(b *testing.B) {
bench(b, func(r runner, b *testing.B) { b.Run("SimpleDocument", func(b *testing.B) {
d := struct {
A string
}{}
doc := []byte(`A = "hello"`) doc := []byte(`A = "hello"`)
b.Run("struct", func(b *testing.B) {
b.SetBytes(int64(len(doc)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := struct {
A string
}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
}
})
b.Run("map", func(b *testing.B) {
b.SetBytes(int64(len(doc)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := map[string]interface{}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
}
})
})
b.Run("ReferenceFile", func(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.Run("struct", func(b *testing.B) {
b.SetBytes(int64(len(bytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := benchmarkDoc{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
}
})
b.Run("map", func(b *testing.B) {
b.SetBytes(int64(len(bytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := map[string]interface{}{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
}
})
})
b.Run("HugoFrontMatter", func(b *testing.B) {
b.SetBytes(int64(len(hugoFrontMatterbytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
err := r.unmarshal(doc, &d) d := map[string]interface{}{}
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -45,6 +105,137 @@ func BenchmarkUnmarshalSimple(b *testing.B) {
}) })
} }
func marshal(v interface{}) ([]byte, error) {
var b bytes.Buffer
enc := toml.NewEncoder(&b)
err := enc.Encode(v)
return b.Bytes(), err
}
func BenchmarkMarshal(b *testing.B) {
b.Run("SimpleDocument", func(b *testing.B) {
doc := []byte(`A = "hello"`)
b.Run("struct", func(b *testing.B) {
d := struct {
A string
}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
b.Run("map", func(b *testing.B) {
d := map[string]interface{}{}
err := toml.Unmarshal(doc, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
})
b.Run("ReferenceFile", func(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.Run("struct", func(b *testing.B) {
d := benchmarkDoc{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
b.Run("map", func(b *testing.B) {
d := map[string]interface{}{}
err := toml.Unmarshal(bytes, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
})
b.Run("HugoFrontMatter", func(b *testing.B) {
d := map[string]interface{}{}
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
if err != nil {
panic(err)
}
b.ReportAllocs()
b.ResetTimer()
var out []byte
for i := 0; i < b.N; i++ {
out, err = marshal(d)
if err != nil {
panic(err)
}
}
b.SetBytes(int64(len(out)))
})
}
type benchmarkDoc struct { type benchmarkDoc struct {
Table struct { Table struct {
Key string Key string
@@ -58,7 +249,7 @@ type benchmarkDoc struct {
} }
Point struct { Point struct {
X int64 X int64
U int64 Y int64
} }
} }
} }
@@ -131,6 +322,7 @@ type benchmarkDoc struct {
Key2 []string Key2 []string
Key3 [][]int64 Key3 [][]int64
// TODO: Key4 not supported by go-toml's Unmarshal // TODO: Key4 not supported by go-toml's Unmarshal
Key4 []interface{}
Key5 []int64 Key5 []int64
Key6 []int64 Key6 []int64
} }
@@ -142,38 +334,321 @@ type benchmarkDoc struct {
Fruit []struct { Fruit []struct {
Name string Name string
Physical struct { Physical struct {
Color string Color string
Shape string Shape string
Variety []struct { }
Name string Variety []struct {
} Name string
} }
} }
} }
func BenchmarkReferenceFile(b *testing.B) { func TestUnmarshalReferenceFile(t *testing.T) {
bench(b, func(r runner, b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
if err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(bytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := benchmarkDoc{}
err := r.unmarshal(bytes, &d)
if err != nil {
panic(err)
}
}
})
}
func TestReferenceFile(t *testing.T) {
bytes, err := ioutil.ReadFile("benchmark.toml") bytes, err := ioutil.ReadFile("benchmark.toml")
require.NoError(t, err) require.NoError(t, err)
d := benchmarkDoc{} d := benchmarkDoc{}
err = toml.Unmarshal(bytes, &d) err = toml.Unmarshal(bytes, &d)
require.NoError(t, err) require.NoError(t, err)
expected := benchmarkDoc{
Table: struct {
Key string
Subtable struct{ Key string }
Inline struct {
Name struct {
First string
Last string
}
Point struct {
X int64
Y int64
}
}
}{
Key: "value",
Subtable: struct{ Key string }{
Key: "another value",
},
// note: x.y.z.w is purposefully missing
Inline: struct {
Name struct {
First string
Last string
}
Point struct {
X int64
Y int64
}
}{
Name: struct {
First string
Last string
}{
First: "Tom",
Last: "Preston-Werner",
},
Point: struct {
X int64
Y int64
}{
X: 1,
Y: 2,
},
},
},
String: struct {
Basic struct{ Basic string }
Multiline struct {
Key1 string
Key2 string
Key3 string
Continued struct {
Key1 string
Key2 string
Key3 string
}
}
Literal struct {
Winpath string
Winpath2 string
Quoted string
Regex string
Multiline struct {
Regex2 string
Lines string
}
}
}{
Basic: struct{ Basic string }{
Basic: "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF.",
},
Multiline: struct {
Key1 string
Key2 string
Key3 string
Continued struct {
Key1 string
Key2 string
Key3 string
}
}{
Key1: "One\nTwo",
Key2: "One\nTwo",
Key3: "One\nTwo",
Continued: struct {
Key1 string
Key2 string
Key3 string
}{
Key1: `The quick brown fox jumps over the lazy dog.`,
Key2: `The quick brown fox jumps over the lazy dog.`,
Key3: `The quick brown fox jumps over the lazy dog.`,
},
},
Literal: struct {
Winpath string
Winpath2 string
Quoted string
Regex string
Multiline struct {
Regex2 string
Lines string
}
}{
Winpath: `C:\Users\nodejs\templates`,
Winpath2: `\\ServerX\admin$\system32\`,
Quoted: `Tom "Dubs" Preston-Werner`,
Regex: `<\i\c*\s*>`,
Multiline: struct {
Regex2 string
Lines string
}{
Regex2: `I [dw]on't need \d{2} apples`,
Lines: `The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
`,
},
},
},
Integer: struct {
Key1 int64
Key2 int64
Key3 int64
Key4 int64
Underscores struct {
Key1 int64
Key2 int64
Key3 int64
}
}{
Key1: 99,
Key2: 42,
Key3: 0,
Key4: -17,
Underscores: struct {
Key1 int64
Key2 int64
Key3 int64
}{
Key1: 1000,
Key2: 5349221,
Key3: 12345,
},
},
Float: struct {
Fractional struct {
Key1 float64
Key2 float64
Key3 float64
}
Exponent struct {
Key1 float64
Key2 float64
Key3 float64
}
Both struct{ Key float64 }
Underscores struct {
Key1 float64
Key2 float64
}
}{
Fractional: struct {
Key1 float64
Key2 float64
Key3 float64
}{
Key1: 1.0,
Key2: 3.1415,
Key3: -0.01,
},
Exponent: struct {
Key1 float64
Key2 float64
Key3 float64
}{
Key1: 5e+22,
Key2: 1e6,
Key3: -2e-2,
},
Both: struct{ Key float64 }{
Key: 6.626e-34,
},
Underscores: struct {
Key1 float64
Key2 float64
}{
Key1: 9224617.445991228313,
Key2: 1e100,
},
},
Boolean: struct {
True bool
False bool
}{
True: true,
False: false,
},
Datetime: struct {
Key1 time.Time
Key2 time.Time
Key3 time.Time
}{
Key1: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
Key2: time.Date(1979, 5, 27, 0, 32, 0, 0, time.FixedZone("", -7*3600)),
Key3: time.Date(1979, 5, 27, 0, 32, 0, 999999000, time.FixedZone("", -7*3600)),
},
Array: struct {
Key1 []int64
Key2 []string
Key3 [][]int64
Key4 []interface{}
Key5 []int64
Key6 []int64
}{
Key1: []int64{1, 2, 3},
Key2: []string{"red", "yellow", "green"},
Key3: [][]int64{{1, 2}, {3, 4, 5}},
Key4: []interface{}{
[]interface{}{int64(1), int64(2)},
[]interface{}{"a", "b", "c"},
},
Key5: []int64{1, 2, 3},
Key6: []int64{1, 2},
},
Products: []struct {
Name string
Sku int64
Color string
}{
{
Name: "Hammer",
Sku: 738594937,
},
{},
{
Name: "Nail",
Sku: 284758393,
Color: "gray",
},
},
Fruit: []struct {
Name string
Physical struct {
Color string
Shape string
}
Variety []struct{ Name string }
}{
{
Name: "apple",
Physical: struct {
Color string
Shape string
}{
Color: "red",
Shape: "round",
},
Variety: []struct{ Name string }{
{Name: "red delicious"},
{Name: "granny smith"},
},
},
{
Name: "banana",
Variety: []struct{ Name string }{
{Name: "plantain"},
},
},
},
}
require.Equal(t, expected, d)
} }
var hugoFrontMatterbytes = []byte(`
categories = ["Development", "VIM"]
date = "2012-04-06"
description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
slug = "spf13-vim-3-0-release-and-new-website"
tags = [".vimrc", "plugins", "spf13-vim", "vim"]
title = "spf13-vim 3.0 release and new website"
include_toc = true
show_comments = false
[[cascade]]
background = "yosemite.jpg"
[cascade._target]
kind = "page"
lang = "en"
path = "/blog/**"
[[cascade]]
background = "goldenbridge.jpg"
[cascade._target]
kind = "section"
`)
-14
View File
@@ -1,14 +0,0 @@
module github.com/pelletier/go-toml/v2/benchmark
go 1.16
replace github.com/pelletier/go-toml/v2 => ../
replace github.com/pelletier/go-toml-v1 => github.com/pelletier/go-toml v1.8.1
require (
github.com/BurntSushi/toml v0.3.1
github.com/pelletier/go-toml-v1 v0.0.0-00010101000000-000000000000
github.com/pelletier/go-toml/v2 v2.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.7.0
)
-16
View File
@@ -1,16 +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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Executable
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env bash
stderr() {
echo "$@" 1>&2
}
usage() {
b=$(basename "$0")
echo $b: ERROR: "$@" 1>&2
cat 1>&2 <<EOF
DESCRIPTION
$(basename "$0") is the script to run continuous integration commands for
go-toml on unix.
Requires Go and Git to be available in the PATH. Expects to be ran from the
root of go-toml's Git repository.
USAGE
$b COMMAND [OPTIONS...]
COMMANDS
benchmark [OPTIONS...] [BRANCH]
Run benchmarks.
ARGUMENTS
BRANCH Optional. Defines which Git branch to use when running
benchmarks.
OPTIONS
-d Compare benchmarks of HEAD with BRANCH using benchstats. In
this form the BRANCH argument is required.
-a Compare benchmarks of HEAD against go-toml v1 and
BurntSushi/toml.
-html When used with -a, emits the output as HTML, ready to be
embedded in the README.
coverage [OPTIONS...] [BRANCH]
Generates code coverage.
ARGUMENTS
BRANCH Optional. Defines which Git branch to use when reporting
coverage. Defaults to HEAD.
OPTIONS
-d Compare coverage of HEAD with the one of BRANCH. In this form,
the BRANCH argument is required. Exit code is non-zero when
coverage percentage decreased.
EOF
exit 1
}
cover() {
branch="${1}"
dir="$(mktemp -d)"
stderr "Executing coverage for ${branch} at ${dir}"
if [ "${branch}" = "HEAD" ]; then
cp -r . "${dir}/"
else
git worktree add "$dir" "$branch"
fi
pushd "$dir"
go test -covermode=atomic -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
popd
if [ "${branch}" != "HEAD" ]; then
git worktree remove --force "$dir"
fi
}
coverage() {
case "$1" in
-d)
shift
target="${1?Need to provide a target branch argument}"
output_dir="$(mktemp -d)"
target_out="${output_dir}/target.txt"
head_out="${output_dir}/head.txt"
cover "${target}" > "${target_out}"
cover "HEAD" > "${head_out}"
cat "${target_out}"
cat "${head_out}"
echo ""
target_pct="$(cat ${target_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')"
head_pct="$(cat ${head_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')"
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
echo "Delta: ${delta_pct}"
if [[ $delta_pct = \-* ]]; then
echo "Regression!";
return 1
fi
return 0
;;
esac
cover "${1-HEAD}"
}
bench() {
branch="${1}"
out="${2}"
replace="${3}"
dir="$(mktemp -d)"
stderr "Executing benchmark for ${branch} at ${dir}"
if [ "${branch}" = "HEAD" ]; then
cp -r . "${dir}/"
else
git worktree add "$dir" "$branch"
fi
pushd "$dir"
if [ "${replace}" != "" ]; then
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \;
go get "${replace}"
# hack: remove canada.toml.gz because it is not supported by
# burntsushi, and replace is only used for benchmark -a
rm -f benchmark/testdata/canada.toml.gz
fi
go test -bench=. -count=10 ./... | tee "${out}"
popd
if [ "${branch}" != "HEAD" ]; then
git worktree remove --force "$dir"
fi
}
fmktemp() {
if mktemp --version|grep GNU >/dev/null; then
mktemp --suffix=-$1;
else
mktemp -t $1;
fi
}
benchstathtml() {
python3 - $1 <<'EOF'
import sys
lines = []
stop = False
with open(sys.argv[1]) as f:
for line in f.readlines():
line = line.strip()
if line == "":
stop = True
if not stop:
lines.append(line.split(','))
results = []
for line in reversed(lines[1:]):
v2 = float(line[1])
results.append([
line[0].replace("-32", ""),
"%.1fx" % (float(line[3])/v2), # v1
"%.1fx" % (float(line[5])/v2), # bs
])
# move geomean to the end
results.append(results[0])
del results[0]
def printtable(data):
print("""
<table>
<thead>
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
</thead>
<tbody>""")
for r in data:
print(" <tr><td>{}</td><td>{}</td><td>{}</td></tr>".format(*r))
print(""" </tbody>
</table>""")
def match(x):
return "ReferenceFile" in x[0] or "HugoFrontMatter" in x[0]
above = [x for x in results if match(x)]
below = [x for x in results if not match(x)]
printtable(above)
print("<details><summary>See more</summary>")
print("""<p>The table above has the results of the most common use-cases. The table below
contains the results of all benchmarks, including unrealistic ones. It is
provided for completeness.</p>""")
printtable(below)
print('<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>')
print("</details>")
EOF
}
benchmark() {
case "$1" in
-d)
shift
target="${1?Need to provide a target branch argument}"
old=`fmktemp ${target}`
bench "${target}" "${old}"
new=`fmktemp HEAD`
bench HEAD "${new}"
benchstat "${old}" "${new}"
return 0
;;
-a)
shift
v2stats=`fmktemp go-toml-v2`
bench HEAD "${v2stats}" "github.com/pelletier/go-toml/v2"
v1stats=`fmktemp go-toml-v1`
bench HEAD "${v1stats}" "github.com/pelletier/go-toml"
bsstats=`fmktemp bs-toml`
bench HEAD "${bsstats}" "github.com/BurntSushi/toml"
cp "${v2stats}" go-toml-v2.txt
cp "${v1stats}" go-toml-v1.txt
cp "${bsstats}" bs-toml.txt
if [ "$1" = "-html" ]; then
tmpcsv=`fmktemp csv`
benchstat -csv -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
benchstathtml $tmpcsv
else
benchstat -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt
fi
rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt
return $?
esac
bench "${1-HEAD}" `mktemp`
}
case "$1" in
coverage) shift; coverage $@;;
benchmark) shift; benchmark $@;;
*) usage "bad argument $1";;
esac
+101 -160
View File
@@ -1,11 +1,9 @@
package toml package toml
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -19,7 +17,7 @@ func parseInteger(b []byte) (int64, error) {
case 'o': case 'o':
return parseIntOct(b) return parseIntOct(b)
default: default:
return 0, newDecodeError(b[1:2], "invalid base: '%c'", b[1]) panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", b[1]))
} }
} }
@@ -37,52 +35,34 @@ func parseLocalDate(b []byte) (LocalDate, error) {
return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD") return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD")
} }
var err error date.Year = parseDecimalDigits(b[0:4])
date.Year, err = parseDecimalDigits(b[0:4]) v := parseDecimalDigits(b[5:7])
if err != nil {
return date, err
}
v, err := parseDecimalDigits(b[5:7])
if err != nil {
return date, err
}
date.Month = time.Month(v) date.Month = time.Month(v)
date.Day, err = parseDecimalDigits(b[8:10]) date.Day = parseDecimalDigits(b[8:10])
if err != nil {
return date, err
}
return date, nil return date, nil
} }
var errNotDigit = errors.New("not a digit") func parseDecimalDigits(b []byte) int {
func parseDecimalDigits(b []byte) (int, error) {
v := 0 v := 0
for _, c := range b { for _, c := range b {
if !isDigit(c) {
return 0, fmt.Errorf("%s: %w", b, errNotDigit)
}
v *= 10 v *= 10
v += int(c - '0') v += int(c - '0')
} }
return v, nil return v
} }
var errParseDateTimeMissingInfo = errors.New("date-time missing timezone information")
func parseDateTime(b []byte) (time.Time, error) { func parseDateTime(b []byte) (time.Time, error) {
// offset-date-time = full-date time-delim full-time // offset-date-time = full-date time-delim full-time
// full-time = partial-time time-offset // full-time = partial-time time-offset
// time-offset = "Z" / time-numoffset // time-offset = "Z" / time-numoffset
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute // time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
dt, b, err := parseLocalDateTime(b) dt, b, err := parseLocalDateTime(b)
if err != nil { if err != nil {
return time.Time{}, err return time.Time{}, err
@@ -91,7 +71,8 @@ func parseDateTime(b []byte) (time.Time, error) {
var zone *time.Location var zone *time.Location
if len(b) == 0 { if len(b) == 0 {
return time.Time{}, errParseDateTimeMissingInfo // parser should have checked that when assigning the date time node
panic("date time should have a timezone")
} }
if b[0] == 'Z' { if b[0] == 'Z' {
@@ -103,18 +84,15 @@ func parseDateTime(b []byte) (time.Time, error) {
return time.Time{}, newDecodeError(b, "invalid date-time timezone") return time.Time{}, newDecodeError(b, "invalid date-time timezone")
} }
direction := 1 direction := 1
switch b[0] { if b[0] == '-' {
case '+':
case '-':
direction = -1 direction = -1
default:
return time.Time{}, newDecodeError(b[0:1], "invalid timezone offset character")
} }
hours := digitsToInt(b[1:3]) hours := digitsToInt(b[1:3])
minutes := digitsToInt(b[4:6]) minutes := digitsToInt(b[4:6])
seconds := direction * (hours*3600 + minutes*60) seconds := direction * (hours*3600 + minutes*60)
zone = time.FixedZone("", seconds) zone = time.FixedZone("", seconds)
b = b[dateTimeByteLen:]
} }
if len(b) > 0 { if len(b) > 0 {
@@ -134,19 +112,12 @@ func parseDateTime(b []byte) (time.Time, error) {
return t, nil return t, nil
} }
var (
errParseLocalDateTimeWrongLength = errors.New(
"local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNN]",
)
errParseLocalDateTimeWrongSeparator = errors.New("datetime separator is expected to be T or a space")
)
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) { func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
var dt LocalDateTime var dt LocalDateTime
const localDateTimeByteLen = 11 const localDateTimeByteMinLen = 11
if len(b) < localDateTimeByteLen { if len(b) < localDateTimeByteMinLen {
return dt, nil, errParseLocalDateTimeWrongLength return dt, nil, newDecodeError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
} }
date, err := parseLocalDate(b[:10]) date, err := parseLocalDate(b[:10])
@@ -157,7 +128,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
sep := b[10] sep := b[10]
if sep != 'T' && sep != ' ' { if sep != 'T' && sep != ' ' {
return dt, nil, errParseLocalDateTimeWrongSeparator return dt, nil, newDecodeError(b[10:11], "datetime separator is expected to be T or a space")
} }
t, rest, err := parseLocalTime(b[11:]) t, rest, err := parseLocalTime(b[11:])
@@ -169,213 +140,183 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
return dt, rest, nil return dt, rest, nil
} }
var errParseLocalTimeWrongLength = errors.New("times are expected to have the format HH:MM:SS[.NNNNNN]")
// parseLocalTime is a bit different because it also returns the remaining // parseLocalTime is a bit different because it also returns the remaining
// []byte that is didn't need. This is to allow parseDateTime to parse those // []byte that is didn't need. This is to allow parseDateTime to parse those
// remaining bytes as a timezone. // remaining bytes as a timezone.
func parseLocalTime(b []byte) (LocalTime, []byte, error) { func parseLocalTime(b []byte) (LocalTime, []byte, error) {
var t LocalTime var (
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
t LocalTime
)
const localTimeByteLen = 8 const localTimeByteLen = 8
if len(b) < localTimeByteLen { if len(b) < localTimeByteLen {
return t, nil, errParseLocalTimeWrongLength return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
}
var err error
t.Hour, err = parseDecimalDigits(b[0:2])
if err != nil {
return t, nil, err
} }
t.Hour = parseDecimalDigits(b[0:2])
if b[2] != ':' { if b[2] != ':' {
return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes") return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes")
} }
t.Minute, err = parseDecimalDigits(b[3:5]) t.Minute = parseDecimalDigits(b[3:5])
if err != nil {
return t, nil, err
}
if b[5] != ':' { if b[5] != ':' {
return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds") return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds")
} }
t.Second, err = parseDecimalDigits(b[6:8]) t.Second = parseDecimalDigits(b[6:8])
if err != nil {
return t, nil, err
}
if len(b) >= 15 && b[8] == '.' { const minLengthWithFrac = 9
t.Nanosecond, err = parseDecimalDigits(b[9:15]) if len(b) >= minLengthWithFrac && b[minLengthWithFrac-1] == '.' {
if err != nil { frac := 0
return t, nil, err digits := 0
for i, c := range b[minLengthWithFrac:] {
if !isDigit(c) {
if i == 0 {
return t, nil, newDecodeError(b[i:i+1], "need at least one digit after fraction point")
}
break
}
const maxFracPrecision = 9
if i >= maxFracPrecision {
return t, nil, newDecodeError(b[i:i+1], "maximum precision for date time is nanosecond")
}
frac *= 10
frac += int(c - '0')
digits++
} }
return t, b[15:], nil t.Nanosecond = frac * nspow[digits]
return t, b[9+digits:], nil
} }
return t, b[8:], nil return t, b[8:], nil
} }
var (
errParseFloatStartDot = errors.New("float cannot start with a dot")
errParseFloatEndDot = errors.New("float cannot end with a dot")
)
//nolint:cyclop //nolint:cyclop
func parseFloat(b []byte) (float64, error) { func parseFloat(b []byte) (float64, error) {
//nolint:godox
// TODO: inefficient
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' { if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
return math.NaN(), nil return math.NaN(), nil
} }
tok := string(b) cleaned, err := checkAndRemoveUnderscores(b)
err := numberContainsInvalidUnderscore(tok)
if err != nil { if err != nil {
return 0, err return 0, err
} }
cleanedVal := cleanupNumberToken(tok) if cleaned[0] == '.' {
if cleanedVal[0] == '.' { return 0, newDecodeError(b, "float cannot start with a dot")
return 0, errParseFloatStartDot
} }
if cleanedVal[len(cleanedVal)-1] == '.' { if cleaned[len(cleaned)-1] == '.' {
return 0, errParseFloatEndDot return 0, newDecodeError(b, "float cannot end with a dot")
} }
f, err := strconv.ParseFloat(cleanedVal, 64) f, err := strconv.ParseFloat(string(cleaned), 64)
if err != nil { if err != nil {
return 0, fmt.Errorf("coudn't ParseFloat %w", err) return 0, newDecodeError(b, "unable to parse float: %w", err)
} }
return f, nil return f, nil
} }
func parseIntHex(b []byte) (int64, error) { func parseIntHex(b []byte) (int64, error) {
tok := string(b) cleaned, err := checkAndRemoveUnderscores(b[2:])
cleanedVal := cleanupNumberToken(tok)
err := hexNumberContainsInvalidUnderscore(cleanedVal)
if err != nil { if err != nil {
return 0, err return 0, err
} }
i, err := strconv.ParseInt(cleanedVal[2:], 16, 64) i, err := strconv.ParseInt(string(cleaned), 16, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf("coudn't ParseIntHex %w", err) return 0, newDecodeError(b, "couldn't parse hexadecimal number: %w", err)
} }
return i, nil return i, nil
} }
func parseIntOct(b []byte) (int64, error) { func parseIntOct(b []byte) (int64, error) {
tok := string(b) cleaned, err := checkAndRemoveUnderscores(b[2:])
cleanedVal := cleanupNumberToken(tok)
err := numberContainsInvalidUnderscore(cleanedVal)
if err != nil { if err != nil {
return 0, err return 0, err
} }
i, err := strconv.ParseInt(cleanedVal[2:], 8, 64) i, err := strconv.ParseInt(string(cleaned), 8, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf("coudn't ParseIntOct %w", err) return 0, newDecodeError(b, "couldn't parse octal number: %w", err)
} }
return i, nil return i, nil
} }
func parseIntBin(b []byte) (int64, error) { func parseIntBin(b []byte) (int64, error) {
tok := string(b) cleaned, err := checkAndRemoveUnderscores(b[2:])
cleanedVal := cleanupNumberToken(tok)
err := numberContainsInvalidUnderscore(cleanedVal)
if err != nil { if err != nil {
return 0, err return 0, err
} }
i, err := strconv.ParseInt(cleanedVal[2:], 2, 64) i, err := strconv.ParseInt(string(cleaned), 2, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf("coudn't ParseIntBin %w", err) return 0, newDecodeError(b, "couldn't parse binary number: %w", err)
} }
return i, nil return i, nil
} }
func parseIntDec(b []byte) (int64, error) { func parseIntDec(b []byte) (int64, error) {
tok := string(b) cleaned, err := checkAndRemoveUnderscores(b)
cleanedVal := cleanupNumberToken(tok)
err := numberContainsInvalidUnderscore(cleanedVal)
if err != nil { if err != nil {
return 0, err return 0, err
} }
i, err := strconv.ParseInt(cleanedVal, 10, 64) i, err := strconv.ParseInt(string(cleaned), 10, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf("coudn't parseIntDec %w", err) return 0, newDecodeError(b, "couldn't parse decimal number: %w", err)
} }
return i, nil return i, nil
} }
func numberContainsInvalidUnderscore(value string) error { func checkAndRemoveUnderscores(b []byte) ([]byte, error) {
// For large numbers, you may use underscores between digits to enhance if b[0] == '_' {
// readability. Each underscore must be surrounded by at least one digit on return nil, newDecodeError(b[0:1], "number cannot start with underscore")
// 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 = isDigitRune(r)
} }
return nil if b[len(b)-1] == '_' {
} return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
func hexNumberContainsInvalidUnderscore(value string) error {
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 // fast path
i := 0
for ; i < len(b); i++ {
if b[i] == '_' {
break
}
}
if i == len(b) {
return b, nil
}
before := false
cleaned := make([]byte, i, len(b))
copy(cleaned, b)
for i++; i < len(b); i++ {
c := b[i]
if c == '_' {
if !before {
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
}
before = false
} else {
before = true
cleaned = append(cleaned, c)
}
}
return cleaned, nil
} }
func cleanupNumberToken(value string) string {
cleanedVal := strings.ReplaceAll(value, "_", "")
return cleanedVal
}
func isHexDigit(r rune) bool {
return isDigitRune(r) ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
func isDigitRune(r rune) bool {
return r >= '0' && r <= '9'
}
var (
errInvalidUnderscore = errors.New("invalid use of _ in number")
errInvalidUnderscoreHex = errors.New("invalid use of _ in hex number")
)
+2
View File
@@ -0,0 +1,2 @@
// Package toml is a library to read and write TOML documents.
package toml
+48 -11
View File
@@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/pelletier/go-toml/v2/internal/unsafe" "github.com/pelletier/go-toml/v2/internal/danger"
) )
// DecodeError represents an error encountered during the parsing or decoding // DecodeError represents an error encountered during the parsing or decoding
@@ -18,15 +18,49 @@ type DecodeError struct {
message string message string
line int line int
column int column int
key Key
human string human string
} }
// StrictMissingError occurs in a TOML document that does not have a
// corresponding field in the target value. It contains all the missing fields
// in Errors.
//
// Emitted by Decoder when SetStrict(true) was called.
type StrictMissingError struct {
// One error per field that could not be found.
Errors []DecodeError
}
// Error returns the canonical string for this error.
func (s *StrictMissingError) Error() string {
return "strict mode: fields in the document are missing in the target struct"
}
// String returns a human readable description of all errors.
func (s *StrictMissingError) String() string {
var buf strings.Builder
for i, e := range s.Errors {
if i > 0 {
buf.WriteString("\n---\n")
}
buf.WriteString(e.String())
}
return buf.String()
}
type Key []string
// internal version of DecodeError that is used as the base to create a // internal version of DecodeError that is used as the base to create a
// DecodeError with full context. // DecodeError with full context.
type decodeError struct { type decodeError struct {
highlight []byte highlight []byte
message string message string
key Key // optional
} }
func (de *decodeError) Error() string { func (de *decodeError) Error() string {
@@ -36,13 +70,13 @@ func (de *decodeError) Error() string {
func newDecodeError(highlight []byte, format string, args ...interface{}) error { func newDecodeError(highlight []byte, format string, args ...interface{}) error {
return &decodeError{ return &decodeError{
highlight: highlight, highlight: highlight,
message: fmt.Sprintf(format, args...), message: fmt.Errorf(format, args...).Error(),
} }
} }
// Error returns the error message contained in the DecodeError. // Error returns the error message contained in the DecodeError.
func (e *DecodeError) Error() string { func (e *DecodeError) Error() string {
return e.message return "toml: " + e.message
} }
// String returns the human-readable contextualized error. This string is multi-line. // String returns the human-readable contextualized error. This string is multi-line.
@@ -56,7 +90,13 @@ func (e *DecodeError) Position() (row int, column int) {
return e.line, e.column return e.line, e.column
} }
// decodeErrorFromHighlight creates a DecodeError referencing to a highlighted // Key that was being processed when the error occurred. The key is present only
// if this DecodeError is part of a StrictMissingError.
func (e *DecodeError) Key() Key {
return e.key
}
// decodeErrorFromHighlight creates a DecodeError referencing a highlighted
// range of bytes from document. // range of bytes from document.
// //
// highlight needs to be a sub-slice of document, or this function panics. // highlight needs to be a sub-slice of document, or this function panics.
@@ -64,14 +104,10 @@ func (e *DecodeError) Position() (row int, column int) {
// The function copies all bytes used in DecodeError, so that document and // The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated. // highlight can be freely deallocated.
//nolint:funlen //nolint:funlen
func wrapDecodeError(document []byte, de *decodeError) error { func wrapDecodeError(document []byte, de *decodeError) *DecodeError {
if de == nil { offset := danger.SubsliceOffset(document, de.highlight)
return nil
}
offset := unsafe.SubsliceOffset(document, de.highlight) errMessage := de.Error()
errMessage := de.message
errLine, errColumn := positionAtEnd(document[:offset]) errLine, errColumn := positionAtEnd(document[:offset])
before, after := linesOfContext(document, de.highlight, offset, 3) before, after := linesOfContext(document, de.highlight, offset, 3)
@@ -137,6 +173,7 @@ func wrapDecodeError(document []byte, de *decodeError) error {
message: errMessage, message: errMessage,
line: errLine, line: errLine,
column: errColumn, column: errColumn,
key: de.key,
human: buf.String(), human: buf.String(),
} }
} }
+40 -2
View File
@@ -3,6 +3,7 @@ package toml
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"strings" "strings"
"testing" "testing"
@@ -11,7 +12,6 @@ import (
//nolint:funlen //nolint:funlen
func TestDecodeError(t *testing.T) { func TestDecodeError(t *testing.T) {
t.Parallel()
examples := []struct { examples := []struct {
desc string desc string
@@ -153,7 +153,7 @@ line 5`,
for _, e := range examples { for _, e := range examples {
e := e e := e
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
t.Parallel()
b := bytes.Buffer{} b := bytes.Buffer{}
b.Write([]byte(e.doc[0])) b.Write([]byte(e.doc[0]))
start := b.Len() start := b.Len()
@@ -179,3 +179,41 @@ line 5`,
}) })
} }
} }
func TestDecodeError_Accessors(t *testing.T) {
e := DecodeError{
message: "foo",
line: 1,
column: 2,
key: []string{"one", "two"},
human: "bar",
}
assert.Equal(t, "toml: foo", e.Error())
r, c := e.Position()
assert.Equal(t, 1, r)
assert.Equal(t, 2, c)
assert.Equal(t, Key{"one", "two"}, e.Key())
assert.Equal(t, "bar", e.String())
}
func ExampleDecodeError() {
doc := `name = 123__456`
s := map[string]interface{}{}
err := Unmarshal([]byte(doc), &s)
fmt.Println(err)
//nolint:errorlint
de := err.(*DecodeError)
fmt.Println(de.String())
row, col := de.Position()
fmt.Println("error occurred at row", row, "column", col)
// Output:
// toml: number must have at least one digit between underscores
// 1| name = 123__456
// | ~~ number must have at least one digit between underscores
// error occurred at row 1 column 11
}
+100
View File
@@ -0,0 +1,100 @@
package toml_test
import (
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func TestFastSimple(t *testing.T) {
m := map[string]int64{}
err := toml.Unmarshal([]byte(`a = 42`), &m)
require.NoError(t, err)
require.Equal(t, map[string]int64{"a": 42}, m)
}
func TestFastSimpleString(t *testing.T) {
m := map[string]string{}
err := toml.Unmarshal([]byte(`a = "hello"`), &m)
require.NoError(t, err)
require.Equal(t, map[string]string{"a": "hello"}, m)
}
func TestFastSimpleInterface(t *testing.T) {
m := map[string]interface{}{}
err := toml.Unmarshal([]byte(`
a = "hello"
b = 42`), &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"a": "hello",
"b": int64(42),
}, m)
}
func TestFastMultipartKeyInterface(t *testing.T) {
m := map[string]interface{}{}
err := toml.Unmarshal([]byte(`
a.interim = "test"
a.b.c = "hello"
b = 42`), &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"a": map[string]interface{}{
"interim": "test",
"b": map[string]interface{}{
"c": "hello",
},
},
"b": int64(42),
}, m)
}
func TestFastExistingMap(t *testing.T) {
m := map[string]interface{}{
"ints": map[string]int{},
}
err := toml.Unmarshal([]byte(`
ints.one = 1
ints.two = 2
strings.yo = "hello"`), &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"ints": map[string]interface{}{
"one": int64(1),
"two": int64(2),
},
"strings": map[string]interface{}{
"yo": "hello",
},
}, m)
}
func TestFastArrayTable(t *testing.T) {
b := []byte(`
[root]
[[root.nested]]
name = 'Bob'
[[root.nested]]
name = 'Alice'
`)
m := map[string]interface{}{}
err := toml.Unmarshal(b, &m)
require.NoError(t, err)
require.Equal(t, map[string]interface{}{
"root": map[string]interface{}{
"nested": []interface{}{
map[string]interface{}{
"name": "Bob",
},
map[string]interface{}{
"name": "Alice",
},
},
},
}, m)
}
+2 -1
View File
@@ -2,4 +2,5 @@ module github.com/pelletier/go-toml/v2
go 1.15 go 1.15
require github.com/stretchr/testify v1.7.0 // latest (v1.7.0) doesn't have the fix for time.Time
require github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
+2 -2
View File
@@ -3,8 +3,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+42 -28
View File
@@ -2,6 +2,9 @@ package ast
import ( import (
"fmt" "fmt"
"unsafe"
"github.com/pelletier/go-toml/v2/internal/danger"
) )
// Iterator starts uninitialized, you need to call Next() first. // Iterator starts uninitialized, you need to call Next() first.
@@ -14,7 +17,7 @@ import (
// } // }
type Iterator struct { type Iterator struct {
started bool started bool
node Node node *Node
} }
// Next moves the iterator forward and returns true if points to a node, false // Next moves the iterator forward and returns true if points to a node, false
@@ -28,8 +31,14 @@ func (c *Iterator) Next() bool {
return c.node.Valid() return c.node.Valid()
} }
// IsLast returns true if the current node of the iterator is the last one.
// Subsequent call to Next() will return false.
func (c *Iterator) IsLast() bool {
return c.node.next == 0
}
// Node returns a copy of the node pointed at by the iterator. // Node returns a copy of the node pointed at by the iterator.
func (c *Iterator) Node() Node { func (c *Iterator) Node() *Node {
return c.node return c.node
} }
@@ -44,14 +53,13 @@ type Root struct {
func (r *Root) Iterator() Iterator { func (r *Root) Iterator() Iterator {
it := Iterator{} it := Iterator{}
if len(r.nodes) > 0 { if len(r.nodes) > 0 {
it.node = r.nodes[0] it.node = &r.nodes[0]
} }
return it return it
} }
func (r *Root) at(idx int) Node { func (r *Root) at(idx Reference) *Node {
// TODO: unsafe to point to the node directly return &r.nodes[idx]
return r.nodes[idx]
} }
// Arrays have one child per element in the array. // Arrays have one child per element in the array.
@@ -63,42 +71,48 @@ func (r *Root) at(idx int) Node {
// children []Node // children []Node
type Node struct { type Node struct {
Kind Kind Kind Kind
Data []byte // Raw bytes from the input Raw Range // Raw bytes from the input.
Data []byte // Node value (could be either allocated or referencing the input).
// next idx (in the root array). 0 if last of the collection. // References to other nodes, as offsets in the backing array from this
next int // node. References can go backward, so those can be negative.
// child idx (in the root array). 0 if no child. next int // 0 if last element
child int child int // 0 if no child
// pointer to the root array }
root *Root
type Range struct {
Offset uint32
Length uint32
} }
// Next returns a copy of the next node, or an invalid Node if there is no // Next returns a copy of the next node, or an invalid Node if there is no
// next node. // next node.
func (n Node) Next() Node { func (n *Node) Next() *Node {
if n.next <= 0 { if n.next == 0 {
return noNode return nil
} }
return n.root.at(n.next) ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.next))
} }
// Child returns a copy of the first child node of this node. Other children // Child returns a copy of the first child node of this node. Other children
// can be accessed calling Next on the first child. // can be accessed calling Next on the first child.
// Returns an invalid Node if there is none. // Returns an invalid Node if there is none.
func (n Node) Child() Node { func (n *Node) Child() *Node {
if n.child <= 0 { if n.child == 0 {
return noNode return nil
} }
return n.root.at(n.child) ptr := unsafe.Pointer(n)
size := unsafe.Sizeof(Node{})
return (*Node)(danger.Stride(ptr, size, n.child))
} }
// Valid returns true if the node's kind is set (not to Invalid). // Valid returns true if the node's kind is set (not to Invalid).
func (n Node) Valid() bool { func (n *Node) Valid() bool {
return n.Kind != Invalid return n != nil
} }
var noNode = Node{}
// Key returns the child nodes making the Key on a supported node. Panics // Key returns the child nodes making the Key on a supported node. Panics
// otherwise. // otherwise.
// They are guaranteed to be all be of the Kind Key. A simple key would return // They are guaranteed to be all be of the Kind Key. A simple key would return
@@ -121,13 +135,13 @@ func (n *Node) Key() Iterator {
// Value returns a pointer to the value node of a KeyValue. // Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil. // Guaranteed to be non-nil.
// Panics if not called on a KeyValue node, or if the Children are malformed. // Panics if not called on a KeyValue node, or if the Children are malformed.
func (n Node) Value() Node { func (n *Node) Value() *Node {
assertKind(KeyValue, n) assertKind(KeyValue, *n)
return n.Child() return n.Child()
} }
// Children returns an iterator over a node's children. // Children returns an iterator over a node's children.
func (n Node) Children() Iterator { func (n *Node) Children() Iterator {
return Iterator{node: n.Child()} return Iterator{node: n.Child()}
} }
+11 -20
View File
@@ -1,12 +1,11 @@
package ast package ast
type Reference struct { type Reference int
idx int
set bool const InvalidReference Reference = -1
}
func (r Reference) Valid() bool { func (r Reference) Valid() bool {
return r.set return r != InvalidReference
} }
type Builder struct { type Builder struct {
@@ -18,8 +17,8 @@ func (b *Builder) Tree() *Root {
return &b.tree return &b.tree
} }
func (b *Builder) NodeAt(ref Reference) Node { func (b *Builder) NodeAt(ref Reference) *Node {
return b.tree.at(ref.idx) return b.tree.at(ref)
} }
func (b *Builder) Reset() { func (b *Builder) Reset() {
@@ -28,33 +27,25 @@ func (b *Builder) Reset() {
} }
func (b *Builder) Push(n Node) Reference { func (b *Builder) Push(n Node) Reference {
n.root = &b.tree
b.lastIdx = len(b.tree.nodes) b.lastIdx = len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n) b.tree.nodes = append(b.tree.nodes, n)
return Reference{ return Reference(b.lastIdx)
idx: b.lastIdx,
set: true,
}
} }
func (b *Builder) PushAndChain(n Node) Reference { func (b *Builder) PushAndChain(n Node) Reference {
n.root = &b.tree
newIdx := len(b.tree.nodes) newIdx := len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n) b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 { if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx b.tree.nodes[b.lastIdx].next = newIdx - b.lastIdx
} }
b.lastIdx = newIdx b.lastIdx = newIdx
return Reference{ return Reference(b.lastIdx)
idx: b.lastIdx,
set: true,
}
} }
func (b *Builder) AttachChild(parent Reference, child Reference) { func (b *Builder) AttachChild(parent Reference, child Reference) {
b.tree.nodes[parent.idx].child = child.idx b.tree.nodes[parent].child = int(child) - int(parent)
} }
func (b *Builder) Chain(from Reference, to Reference) { func (b *Builder) Chain(from Reference, to Reference) {
b.tree.nodes[from.idx].next = to.idx b.tree.nodes[from].next = int(to) - int(from)
} }
+65
View File
@@ -0,0 +1,65 @@
package danger
import (
"fmt"
"reflect"
"unsafe"
)
const maxInt = uintptr(int(^uint(0) >> 1))
func SubsliceOffset(data []byte, subslice []byte) int {
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
if hlp.Data < datap.Data {
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
}
offset := hlp.Data - datap.Data
if offset > maxInt {
panic(fmt.Errorf("slice offset larger than int (%d)", offset))
}
intoffset := int(offset)
if intoffset > datap.Len {
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
}
if intoffset+hlp.Len > datap.Len {
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
}
return intoffset
}
func BytesRange(start []byte, end []byte) []byte {
if start == nil || end == nil {
panic("cannot call BytesRange with nil")
}
startp := (*reflect.SliceHeader)(unsafe.Pointer(&start))
endp := (*reflect.SliceHeader)(unsafe.Pointer(&end))
if startp.Data > endp.Data {
panic(fmt.Errorf("start pointer address (%d) is after end pointer address (%d)", startp.Data, endp.Data))
}
l := startp.Len
endLen := int(endp.Data-startp.Data) + endp.Len
if endLen > l {
l = endLen
}
if l > startp.Cap {
panic(fmt.Errorf("range length is larger than capacity"))
}
return start[:l]
}
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
// TODO: replace with unsafe.Add when Go 1.17 is released
// https://github.com/golang/go/issues/40481
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
}
+178
View File
@@ -0,0 +1,178 @@
package danger_test
import (
"testing"
"unsafe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pelletier/go-toml/v2/internal/danger"
)
func TestSubsliceOffsetValid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
offset int
}{
{
desc: "simple",
test: func() ([]byte, []byte) {
data := []byte("hello")
return data, data[1:]
},
offset: 1,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
offset := danger.SubsliceOffset(d, s)
assert.Equal(t, e.offset, offset)
})
}
}
func TestSubsliceOffsetInvalid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
}{
{
desc: "unrelated arrays",
test: func() ([]byte, []byte) {
return []byte("one"), []byte("two")
},
},
{
desc: "slice starts before data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[5:], full[1:]
},
},
{
desc: "slice starts after data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[:3], full[5:]
},
},
{
desc: "slice ends after data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[:5], full[3:8]
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
require.Panics(t, func() {
danger.SubsliceOffset(d, s)
})
})
}
}
func TestStride(t *testing.T) {
a := []byte{1, 2, 3, 4}
x := &a[1]
n := (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), 1))
require.Equal(t, &a[2], n)
n = (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), -1))
require.Equal(t, &a[0], n)
}
func TestBytesRange(t *testing.T) {
type fn = func() ([]byte, []byte)
examples := []struct {
desc string
test fn
expected []byte
}{
{
desc: "simple",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[1:3], full[6:8]
},
expected: []byte("ello wo"),
},
{
desc: "full",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[0:1], full[len(full)-1:]
},
expected: []byte("hello world"),
},
{
desc: "end before start",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[len(full)-1:], full[0:1]
},
},
{
desc: "nils",
test: func() ([]byte, []byte) {
return nil, nil
},
},
{
desc: "nils start",
test: func() ([]byte, []byte) {
return nil, []byte("foo")
},
},
{
desc: "nils end",
test: func() ([]byte, []byte) {
return []byte("foo"), nil
},
},
{
desc: "start is end",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[1:3], full[1:3]
},
expected: []byte("el"),
},
{
desc: "end contained in start",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[1:7], full[2:4]
},
expected: []byte("ello w"),
},
{
desc: "different backing arrays",
test: func() ([]byte, []byte) {
one := []byte("hello world")
two := []byte("hello world")
return one, two
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
start, end := e.test()
if e.expected == nil {
require.Panics(t, func() {
danger.BytesRange(start, end)
})
} else {
res := danger.BytesRange(start, end)
require.Equal(t, e.expected, res)
}
})
}
}
@@ -4,6 +4,7 @@ package imported_tests
// defaults of v2. // defaults of v2.
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@@ -164,3 +165,34 @@ stringlist = []
require.Equal(t, string(expected), string(result)) require.Equal(t, string(expected), string(result))
} }
type textMarshaler struct {
FirstName string
LastName string
}
func (m textMarshaler) MarshalText() ([]byte, error) {
fullName := fmt.Sprintf("%s %s", m.FirstName, m.LastName)
return []byte(fullName), nil
}
func TestTextMarshaler(t *testing.T) {
type wrap struct {
TM textMarshaler
}
m := textMarshaler{FirstName: "Sally", LastName: "Fields"}
t.Run("at root", func(t *testing.T) {
_, err := toml.Marshal(m)
// in v2 we do not allow TextMarshaler at root
require.Error(t, err)
})
t.Run("leaf", func(t *testing.T) {
res, err := toml.Marshal(wrap{m})
require.NoError(t, err)
require.Equal(t, "TM = 'Sally Fields'\n", string(res))
})
}
+308 -304
View File
@@ -7,6 +7,7 @@ package imported_tests
// marked as skipped until we figure out if that's something we want in v2. // marked as skipped until we figure out if that's something we want in v2.
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
@@ -222,11 +223,13 @@ type testSubDoc struct {
unexported int `toml:"shouldntBeHere"` unexported int `toml:"shouldntBeHere"`
} }
var biteMe = "Bite me" var (
var float1 float32 = 12.3 biteMe = "Bite me"
var float2 float32 = 45.6 float1 float32 = 12.3
var float3 float32 = 78.9 float2 float32 = 45.6
var subdoc = testSubDoc{"Second", 0} float3 float32 = 78.9
subdoc = testSubDoc{"Second", 0}
)
var docData = testDoc{ var docData = testDoc{
Title: "TOML Marshal Testing", Title: "TOML Marshal Testing",
@@ -381,7 +384,7 @@ var intErrTomls = []string{
} }
func TestErrUnmarshal(t *testing.T) { func TestErrUnmarshal(t *testing.T) {
var errTomls = []string{ errTomls := []string{
"bool = truly\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"", "bool = truly\ndate = 1979-05-27T07:32:00Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:3200Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"", "bool = true\ndate = 1979-05-27T07:3200Z\nfloat = 123.4\nint = 5000\nstring = \"Bite me\"",
"bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123a4\nint = 5000\nstring = \"Bite me\"", "bool = true\ndate = 1979-05-27T07:32:00Z\nfloat = 123a4\nint = 5000\nstring = \"Bite me\"",
@@ -467,7 +470,7 @@ func TestEmptyUnmarshalOmit(t *testing.T) {
Map map[string]string `toml:"map,omitempty"` Map map[string]string `toml:"map,omitempty"`
} }
var emptyTestData2 = emptyMarshalTestStruct2{ emptyTestData2 := emptyMarshalTestStruct2{
Title: "Placeholder", Title: "Placeholder",
Bool: false, Bool: false,
Int: 0, Int: 0,
@@ -495,21 +498,23 @@ type pointerMarshalTestStruct struct {
DblPtr *[]*[]*string DblPtr *[]*[]*string
} }
var pointerStr = "Hello" var (
var pointerList = []string{"Hello back"} pointerStr = "Hello"
var pointerListPtr = []*string{&pointerStr} pointerList = []string{"Hello back"}
var pointerMap = map[string]string{"response": "Goodbye"} pointerListPtr = []*string{&pointerStr}
var pointerMapPtr = map[string]*string{"alternate": &pointerStr} pointerMap = map[string]string{"response": "Goodbye"}
var pointerTestData = pointerMarshalTestStruct{ pointerMapPtr = map[string]*string{"alternate": &pointerStr}
Str: &pointerStr, pointerTestData = pointerMarshalTestStruct{
List: &pointerList, Str: &pointerStr,
ListPtr: &pointerListPtr, List: &pointerList,
Map: &pointerMap, ListPtr: &pointerListPtr,
MapPtr: &pointerMapPtr, Map: &pointerMap,
EmptyStr: nil, MapPtr: &pointerMapPtr,
EmptyList: nil, EmptyStr: nil,
EmptyMap: nil, EmptyList: nil,
} EmptyMap: nil,
}
)
var pointerTestToml = []byte(`List = ["Hello back"] var pointerTestToml = []byte(`List = ["Hello back"]
ListPtr = ["Hello"] ListPtr = ["Hello"]
@@ -537,15 +542,17 @@ func TestUnmarshalTypeMismatch(t *testing.T) {
type nestedMarshalTestStruct struct { type nestedMarshalTestStruct struct {
String [][]string String [][]string
//Struct [][]basicMarshalTestSubStruct // Struct [][]basicMarshalTestSubStruct
StringPtr *[]*[]*string StringPtr *[]*[]*string
// StructPtr *[]*[]*basicMarshalTestSubStruct // StructPtr *[]*[]*basicMarshalTestSubStruct
} }
var str1 = "Three" var (
var str2 = "Four" str1 = "Three"
var strPtr = []*string{&str1, &str2} str2 = "Four"
var strPtr2 = []*[]*string{&strPtr} strPtr = []*string{&str1, &str2}
strPtr2 = []*[]*string{&strPtr}
)
var nestedTestData = nestedMarshalTestStruct{ var nestedTestData = nestedMarshalTestStruct{
String: [][]string{{"Five", "Six"}, {"One", "Two"}}, String: [][]string{{"Five", "Six"}, {"One", "Two"}},
@@ -596,6 +603,7 @@ var nestedCustomMarshalerData = customMarshalerParent{
var nestedCustomMarshalerToml = []byte(`friends = ["Sally Fields"] var nestedCustomMarshalerToml = []byte(`friends = ["Sally Fields"]
me = "Maiku Suteda" me = "Maiku Suteda"
`) `)
var nestedCustomMarshalerTomlForUnmarshal = []byte(`[friends] var nestedCustomMarshalerTomlForUnmarshal = []byte(`[friends]
FirstName = "Sally" FirstName = "Sally"
LastName = "Fields"`) LastName = "Fields"`)
@@ -611,22 +619,12 @@ func (x *IntOrString) MarshalTOML() ([]byte, error) {
return []byte(s), nil return []byte(s), nil
} }
type textMarshaler struct {
FirstName string
LastName string
}
func (m textMarshaler) MarshalText() ([]byte, error) {
fullName := fmt.Sprintf("%s %s", m.FirstName, m.LastName)
return []byte(fullName), nil
}
func TestUnmarshalTextMarshaler(t *testing.T) { func TestUnmarshalTextMarshaler(t *testing.T) {
var nested = struct { nested := struct {
Friends textMarshaler `toml:"friends"` Friends textMarshaler `toml:"friends"`
}{} }{}
var expected = struct { expected := struct {
Friends textMarshaler `toml:"friends"` Friends textMarshaler `toml:"friends"`
}{ }{
Friends: textMarshaler{FirstName: "Sally", LastName: "Fields"}, Friends: textMarshaler{FirstName: "Sally", LastName: "Fields"},
@@ -1369,7 +1367,6 @@ func TestUnmarshalPreservesUnexportedFields(t *testing.T) {
t.Run("unexported field should not be set from toml", func(t *testing.T) { t.Run("unexported field should not be set from toml", func(t *testing.T) {
var actual unexportedFieldPreservationTest var actual unexportedFieldPreservationTest
err := toml.Unmarshal([]byte(doc), &actual) err := toml.Unmarshal([]byte(doc), &actual)
if err != nil { if err != nil {
t.Fatal("did not expect an error") t.Fatal("did not expect an error")
} }
@@ -1403,7 +1400,6 @@ func TestUnmarshalPreservesUnexportedFields(t *testing.T) {
Nested3: &unexportedFieldPreservationTestNested{"baz", "bax"}, Nested3: &unexportedFieldPreservationTestNested{"baz", "bax"},
} }
err := toml.Unmarshal([]byte(doc), &actual) err := toml.Unmarshal([]byte(doc), &actual)
if err != nil { if err != nil {
t.Fatal("did not expect an error") t.Fatal("did not expect an error")
} }
@@ -1429,211 +1425,206 @@ func TestUnmarshalPreservesUnexportedFields(t *testing.T) {
}) })
} }
// func TestUnmarshalLocalDate(t *testing.T) {
//func TestUnmarshalLocalDate(t *testing.T) { t.Run("ToLocalDate", func(t *testing.T) {
// t.Run("ToLocalDate", func(t *testing.T) { type dateStruct struct {
// type dateStruct struct { Date toml.LocalDate
// Date toml.LocalDate }
// }
// doc := `date = 1979-05-27`
// doc := `date = 1979-05-27`
// var obj dateStruct
// var obj dateStruct
// err := toml.Unmarshal([]byte(doc), &obj)
// err := toml.Unmarshal([]byte(doc), &obj) if err != nil {
// t.Fatal(err)
// if err != nil { }
// t.Fatal(err)
// } if obj.Date.Year != 1979 {
// t.Errorf("expected year 1979, got %d", obj.Date.Year)
// if obj.Date.Year != 1979 { }
// t.Errorf("expected year 1979, got %d", obj.Date.Year) if obj.Date.Month != 5 {
// } t.Errorf("expected month 5, got %d", obj.Date.Month)
// if obj.Date.Month != 5 { }
// t.Errorf("expected month 5, got %d", obj.Date.Month) if obj.Date.Day != 27 {
// } t.Errorf("expected day 27, got %d", obj.Date.Day)
// if obj.Date.Day != 27 { }
// t.Errorf("expected day 27, got %d", obj.Date.Day) })
// }
// }) t.Run("ToLocalDate", func(t *testing.T) {
// type dateStruct struct {
// t.Run("ToLocalDate", func(t *testing.T) { Date time.Time
// type dateStruct struct { }
// Date time.Time
// } doc := `date = 1979-05-27`
//
// doc := `date = 1979-05-27` var obj dateStruct
//
// var obj dateStruct err := toml.Unmarshal([]byte(doc), &obj)
// if err != nil {
// err := toml.Unmarshal([]byte(doc), &obj) t.Fatal(err)
// }
// if err != nil {
// t.Fatal(err) if obj.Date.Year() != 1979 {
// } t.Errorf("expected year 1979, got %d", obj.Date.Year())
// }
// if obj.Date.Year() != 1979 { if obj.Date.Month() != 5 {
// t.Errorf("expected year 1979, got %d", obj.Date.Year()) t.Errorf("expected month 5, got %d", obj.Date.Month())
// } }
// if obj.Date.Month() != 5 { if obj.Date.Day() != 27 {
// t.Errorf("expected month 5, got %d", obj.Date.Month()) t.Errorf("expected day 27, got %d", obj.Date.Day())
// } }
// if obj.Date.Day() != 27 { })
// t.Errorf("expected day 27, got %d", obj.Date.Day()) }
// }
// }) func TestUnmarshalLocalDateTime(t *testing.T) {
//} examples := []struct {
// name string
//func TestUnmarshalLocalDateTime(t *testing.T) { in string
// examples := []struct { out toml.LocalDateTime
// name string }{
// in string {
// out toml.LocalDateTime name: "normal",
// }{ in: "1979-05-27T07:32:00",
// { out: toml.LocalDateTime{
// name: "normal", Date: toml.LocalDate{
// in: "1979-05-27T07:32:00", Year: 1979,
// out: toml.LocalDateTime{ Month: 5,
// Date: toml.LocalDate{ Day: 27,
// Year: 1979, },
// Month: 5, Time: toml.LocalTime{
// Day: 27, Hour: 7,
// }, Minute: 32,
// Time: toml.LocalTime{ Second: 0,
// Hour: 7, Nanosecond: 0,
// Minute: 32, },
// Second: 0, },
// Nanosecond: 0, },
// }, {
// }}, name: "with nanoseconds",
// { in: "1979-05-27T00:32:00.999999",
// name: "with nanoseconds", out: toml.LocalDateTime{
// in: "1979-05-27T00:32:00.999999", Date: toml.LocalDate{
// out: toml.LocalDateTime{ Year: 1979,
// Date: toml.LocalDate{ Month: 5,
// Year: 1979, Day: 27,
// Month: 5, },
// Day: 27, Time: toml.LocalTime{
// }, Hour: 0,
// Time: toml.LocalTime{ Minute: 32,
// Hour: 0, Second: 0,
// Minute: 32, Nanosecond: 999999000,
// Second: 0, },
// Nanosecond: 999999000, },
// }, },
// }, }
// },
// } for i, example := range examples {
// doc := fmt.Sprintf(`date = %s`, example.in)
// for i, example := range examples {
// doc := fmt.Sprintf(`date = %s`, example.in) t.Run(fmt.Sprintf("ToLocalDateTime_%d_%s", i, example.name), func(t *testing.T) {
// type dateStruct struct {
// t.Run(fmt.Sprintf("ToLocalDateTime_%d_%s", i, example.name), func(t *testing.T) { Date toml.LocalDateTime
// type dateStruct struct { }
// Date toml.LocalDateTime
// } var obj dateStruct
//
// var obj dateStruct err := toml.Unmarshal([]byte(doc), &obj)
// if err != nil {
// err := toml.Unmarshal([]byte(doc), &obj) t.Fatal(err)
// }
// if err != nil {
// t.Fatal(err) if obj.Date != example.out {
// } t.Errorf("expected '%s', got '%s'", example.out, obj.Date)
// }
// if obj.Date != example.out { })
// t.Errorf("expected '%s', got '%s'", example.out, obj.Date)
// } t.Run(fmt.Sprintf("ToTime_%d_%s", i, example.name), func(t *testing.T) {
// }) type dateStruct struct {
// Date time.Time
// t.Run(fmt.Sprintf("ToTime_%d_%s", i, example.name), func(t *testing.T) { }
// type dateStruct struct {
// Date time.Time var obj dateStruct
// }
// err := toml.Unmarshal([]byte(doc), &obj)
// var obj dateStruct if err != nil {
// t.Fatal(err)
// err := toml.Unmarshal([]byte(doc), &obj) }
//
// if err != nil { if obj.Date.Year() != example.out.Date.Year {
// t.Fatal(err) t.Errorf("expected year %d, got %d", example.out.Date.Year, obj.Date.Year())
// } }
// if obj.Date.Month() != example.out.Date.Month {
// if obj.Date.Year() != example.out.Date.Year { t.Errorf("expected month %d, got %d", example.out.Date.Month, obj.Date.Month())
// t.Errorf("expected year %d, got %d", example.out.Date.Year, obj.Date.Year()) }
// } if obj.Date.Day() != example.out.Date.Day {
// if obj.Date.Month() != example.out.Date.Month { t.Errorf("expected day %d, got %d", example.out.Date.Day, obj.Date.Day())
// t.Errorf("expected month %d, got %d", example.out.Date.Month, obj.Date.Month()) }
// } if obj.Date.Hour() != example.out.Time.Hour {
// if obj.Date.Day() != example.out.Date.Day { t.Errorf("expected hour %d, got %d", example.out.Time.Hour, obj.Date.Hour())
// t.Errorf("expected day %d, got %d", example.out.Date.Day, obj.Date.Day()) }
// } if obj.Date.Minute() != example.out.Time.Minute {
// if obj.Date.Hour() != example.out.Time.Hour { t.Errorf("expected minute %d, got %d", example.out.Time.Minute, obj.Date.Minute())
// t.Errorf("expected hour %d, got %d", example.out.Time.Hour, obj.Date.Hour()) }
// } if obj.Date.Second() != example.out.Time.Second {
// if obj.Date.Minute() != example.out.Time.Minute { t.Errorf("expected second %d, got %d", example.out.Time.Second, obj.Date.Second())
// t.Errorf("expected minute %d, got %d", example.out.Time.Minute, obj.Date.Minute()) }
// } if obj.Date.Nanosecond() != example.out.Time.Nanosecond {
// if obj.Date.Second() != example.out.Time.Second { t.Errorf("expected nanoseconds %d, got %d", example.out.Time.Nanosecond, obj.Date.Nanosecond())
// t.Errorf("expected second %d, got %d", example.out.Time.Second, obj.Date.Second()) }
// } })
// if obj.Date.Nanosecond() != example.out.Time.Nanosecond { }
// t.Errorf("expected nanoseconds %d, got %d", example.out.Time.Nanosecond, obj.Date.Nanosecond()) }
// }
// }) func TestUnmarshalLocalTime(t *testing.T) {
// } examples := []struct {
//} name string
// in string
//func TestUnmarshalLocalTime(t *testing.T) { out toml.LocalTime
// examples := []struct { }{
// name string {
// in string name: "normal",
// out toml.LocalTime in: "07:32:00",
// }{ out: toml.LocalTime{
// { Hour: 7,
// name: "normal", Minute: 32,
// in: "07:32:00", Second: 0,
// out: toml.LocalTime{ Nanosecond: 0,
// Hour: 7, },
// Minute: 32, },
// Second: 0, {
// Nanosecond: 0, name: "with nanoseconds",
// }, in: "00:32:00.999999",
// }, out: toml.LocalTime{
// { Hour: 0,
// name: "with nanoseconds", Minute: 32,
// in: "00:32:00.999999", Second: 0,
// out: toml.LocalTime{ Nanosecond: 999999000,
// Hour: 0, },
// Minute: 32, },
// Second: 0, }
// Nanosecond: 999999000,
// }, for i, example := range examples {
// }, doc := fmt.Sprintf(`Time = %s`, example.in)
// }
// t.Run(fmt.Sprintf("ToLocalTime_%d_%s", i, example.name), func(t *testing.T) {
// for i, example := range examples { type dateStruct struct {
// doc := fmt.Sprintf(`Time = %s`, example.in) Time toml.LocalTime
// }
// t.Run(fmt.Sprintf("ToLocalTime_%d_%s", i, example.name), func(t *testing.T) {
// type dateStruct struct { var obj dateStruct
// Time toml.LocalTime
// } err := toml.Unmarshal([]byte(doc), &obj)
// if err != nil {
// var obj dateStruct t.Fatal(err)
// }
// err := toml.Unmarshal([]byte(doc), &obj)
// if obj.Time != example.out {
// if err != nil { t.Errorf("expected '%s', got '%s'", example.out, obj.Time)
// t.Fatal(err) }
// } })
// }
// if obj.Time != example.out { }
// t.Errorf("expected '%s', got '%s'", example.out, obj.Time)
// }
// })
// }
//}
// test case for issue #339 // test case for issue #339
func TestUnmarshalSameInnerField(t *testing.T) { func TestUnmarshalSameInnerField(t *testing.T) {
@@ -1955,66 +1946,80 @@ String2="2"`
assert.Error(t, err) assert.Error(t, err)
} }
func decoder(doc string) *toml.Decoder {
return toml.NewDecoder(bytes.NewReader([]byte(doc)))
}
func strictDecoder(doc string) *toml.Decoder {
d := decoder(doc)
d.SetStrict(true)
return d
}
func TestDecoderStrict(t *testing.T) { func TestDecoderStrict(t *testing.T) {
t.Skip() input := `
// input := ` [decoded]
//[decoded] key = ""
// key = ""
// [undecoded]
//[undecoded] key = ""
// key = ""
// [undecoded.inner]
// [undecoded.inner] key = ""
// key = ""
// [[undecoded.array]]
// [[undecoded.array]] key = ""
// key = ""
// [[undecoded.array]]
// [[undecoded.array]] key = ""
// key = ""
// `
//` var doc struct {
// var doc struct { Decoded struct {
// Decoded struct { Key string
// Key string }
// } }
// }
// err := strictDecoder(input).Decode(&doc)
// expected := `undecoded keys: ["undecoded.array.0.key" "undecoded.array.1.key" "undecoded.inner.key" "undecoded.key"]` require.Error(t, err)
// require.IsType(t, &toml.StrictMissingError{}, err)
// err := NewDecoder(bytes.NewReader([]byte(input))).Strict(true).Decode(&doc) se := err.(*toml.StrictMissingError)
// if err == nil {
// t.Error("expected error, got none") keys := []toml.Key{}
// } else if err.Error() != expected {
// t.Errorf("expect err: %s, got: %s", expected, err.Error()) for _, e := range se.Errors {
// } keys = append(keys, e.Key())
// }
// if err := NewDecoder(bytes.NewReader([]byte(input))).Decode(&doc); err != nil {
// t.Errorf("unexpected err: %s", err) expectedKeys := []toml.Key{
// } {"undecoded"},
// {"undecoded", "inner"},
// var m map[string]interface{} {"undecoded", "array"},
// if err := NewDecoder(bytes.NewReader([]byte(input))).Decode(&m); err != nil { {"undecoded", "array"},
// t.Errorf("unexpected err: %s", err) }
// }
require.Equal(t, expectedKeys, keys)
err = decoder(input).Decode(&doc)
require.NoError(t, err)
var m map[string]interface{}
err = decoder(input).Decode(&m)
} }
func TestDecoderStrictValid(t *testing.T) { func TestDecoderStrictValid(t *testing.T) {
t.Skip() input := `
// input := ` [decoded]
//[decoded] key = ""
// key = "" `
//` var doc struct {
// var doc struct { Decoded struct {
// Decoded struct { Key string
// Key string }
// } }
// }
// err := strictDecoder(input).Decode(&doc)
// err := NewDecoder(bytes.NewReader([]byte(input))).Strict(true).Decode(&doc) require.NoError(t, err)
// if err != nil {
// t.Fatal("unexpected error:", err)
// }
} }
type docUnmarshalTOML struct { type docUnmarshalTOML struct {
@@ -2279,8 +2284,7 @@ func (d *durationString) UnmarshalTOML(v interface{}) error {
return nil return nil
} }
type config437Error struct { type config437Error struct{}
}
func (e *config437Error) UnmarshalTOML(v interface{}) error { func (e *config437Error) UnmarshalTOML(v interface{}) error {
return errors.New("expected") return errors.New("expected")
+50
View File
@@ -0,0 +1,50 @@
package tracker
import (
"github.com/pelletier/go-toml/v2/internal/ast"
)
// KeyTracker is a tracker that keeps track of the current Key as the AST is
// walked.
type KeyTracker struct {
k []string
}
// UpdateTable sets the state of the tracker with the AST table node.
func (t *KeyTracker) UpdateTable(node *ast.Node) {
t.reset()
t.Push(node)
}
// UpdateArrayTable sets the state of the tracker with the AST array table node.
func (t *KeyTracker) UpdateArrayTable(node *ast.Node) {
t.reset()
t.Push(node)
}
// Push the given key on the stack.
func (t *KeyTracker) Push(node *ast.Node) {
it := node.Key()
for it.Next() {
t.k = append(t.k, string(it.Node().Data))
}
}
// Pop key from stack.
func (t *KeyTracker) Pop(node *ast.Node) {
it := node.Key()
for it.Next() {
t.k = t.k[:len(t.k)-1]
}
}
// Key returns the current key
func (t *KeyTracker) Key() []string {
k := make([]string, len(t.k))
copy(k, t.k)
return k
}
func (t *KeyTracker) reset() {
t.k = t.k[:0]
}
+257
View File
@@ -0,0 +1,257 @@
package tracker
import (
"bytes"
"fmt"
"github.com/pelletier/go-toml/v2/internal/ast"
)
type keyKind uint8
const (
invalidKind keyKind = iota
valueKind
tableKind
arrayTableKind
)
func (k keyKind) String() string {
switch k {
case invalidKind:
return "invalid"
case valueKind:
return "value"
case tableKind:
return "table"
case arrayTableKind:
return "array table"
}
panic("missing keyKind string mapping")
}
// SeenTracker tracks which keys have been seen with which TOML type to flag
// duplicates and mismatches according to the spec.
//
// Each node in the visited tree is represented by an entry. Each entry has an
// identifier, which is provided by a counter. Entries are stored in the array
// entries. As new nodes are discovered (referenced for the first time in the
// TOML document), entries are created and appended to the array. An entry
// points to its parent using its id.
//
// To find whether a given key (sequence of []byte) has already been visited,
// the entries are linearly searched, looking for one with the right name and
// parent id.
//
// Given that all keys appear in the document after their parent, it is
// guaranteed that all descendants of a node are stored after the node, this
// speeds up the search process.
//
// When encountering [[array tables]], the descendants of that node are removed
// to allow that branch of the tree to be "rediscovered". To maintain the
// invariant above, the deletion process needs to keep the order of entries.
// This results in more copies in that case.
type SeenTracker struct {
entries []entry
currentIdx int
nextID int
}
type entry struct {
id int
parent int
name []byte
kind keyKind
explicit bool
}
// Remove all descendent of node at position idx.
func (s *SeenTracker) clear(idx int) {
p := s.entries[idx].id
rest := clear(p, s.entries[idx+1:])
s.entries = s.entries[:idx+1+len(rest)]
}
func clear(parentID int, entries []entry) []entry {
for i := 0; i < len(entries); {
if entries[i].parent == parentID {
id := entries[i].id
copy(entries[i:], entries[i+1:])
entries = entries[:len(entries)-1]
rest := clear(id, entries[i:])
entries = entries[:i+len(rest)]
} else {
i++
}
}
return entries
}
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool) int {
parentID := s.id(parentIdx)
idx := len(s.entries)
s.entries = append(s.entries, entry{
id: s.nextID,
parent: parentID,
name: name,
kind: kind,
explicit: explicit,
})
s.nextID++
return idx
}
// CheckExpression takes a top-level node and checks that it does not contain keys
// that have been seen in previous calls, and validates that types are consistent.
func (s *SeenTracker) CheckExpression(node *ast.Node) error {
if s.entries == nil {
// s.entries = make([]entry, 0, 8)
// Skip ID = 0 to remove the confusion between nodes whose parent has
// id 0 and root nodes (parent id is 0 because it's the zero value).
s.nextID = 1
// Start unscoped, so idx is negative.
s.currentIdx = -1
}
switch node.Kind {
case ast.KeyValue:
return s.checkKeyValue(node)
case ast.Table:
return s.checkTable(node)
case ast.ArrayTable:
return s.checkArrayTable(node)
default:
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
}
}
func (s *SeenTracker) checkTable(node *ast.Node) error {
it := node.Key()
parentIdx := -1
// This code is duplicated in checkArrayTable. This is because factoring
// it in a function requires to copy the iterator, or allocate it to the
// heap, which is not cheap.
for it.Next() {
if it.IsLast() {
break
}
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false)
}
parentIdx = idx
}
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx >= 0 {
kind := s.entries[idx].kind
if kind != tableKind {
return fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
}
if s.entries[idx].explicit {
return fmt.Errorf("toml: table %s already exists", string(k))
}
s.entries[idx].explicit = true
} else {
idx = s.create(parentIdx, k, tableKind, true)
}
s.currentIdx = idx
return nil
}
func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
it := node.Key()
parentIdx := -1
for it.Next() {
if it.IsLast() {
break
}
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx < 0 {
idx = s.create(parentIdx, k, tableKind, false)
}
parentIdx = idx
}
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx >= 0 {
kind := s.entries[idx].kind
if kind != arrayTableKind {
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
}
s.clear(idx)
} else {
idx = s.create(parentIdx, k, arrayTableKind, true)
}
s.currentIdx = idx
return nil
}
func (s *SeenTracker) checkKeyValue(node *ast.Node) error {
it := node.Key()
parentIdx := s.currentIdx
for it.Next() {
k := it.Node().Data
idx := s.find(parentIdx, k)
if idx >= 0 {
if s.entries[idx].kind != tableKind {
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), s.entries[idx].kind)
}
} else {
idx = s.create(parentIdx, k, tableKind, false)
}
parentIdx = idx
}
kind := valueKind
if node.Value().Kind == ast.InlineTable {
kind = tableKind
}
s.entries[parentIdx].kind = kind
return nil
}
func (s *SeenTracker) id(idx int) int {
if idx >= 0 {
return s.entries[idx].id
}
return 0
}
func (s *SeenTracker) find(parentIdx int, k []byte) int {
parentID := s.id(parentIdx)
for i := parentIdx + 1; i < len(s.entries); i++ {
if s.entries[i].parent == parentID && bytes.Equal(s.entries[i].name, k) {
return i
}
}
return -1
}
-199
View File
@@ -1,200 +1 @@
package tracker package tracker
import (
"fmt"
"github.com/pelletier/go-toml/v2/internal/ast"
)
type keyKind uint8
const (
invalidKind keyKind = iota
valueKind
tableKind
arrayTableKind
)
func (k keyKind) String() string {
switch k {
case invalidKind:
return "invalid"
case valueKind:
return "value"
case tableKind:
return "table"
case arrayTableKind:
return "array table"
}
panic("missing keyKind string mapping")
}
// Tracks which keys have been seen with which TOML type to flag duplicates
// and mismatches according to the spec.
type Seen struct {
root *info
current *info
}
type info struct {
parent *info
kind keyKind
children map[string]*info
explicit bool
}
func (i *info) Clear() {
i.children = nil
}
func (i *info) Has(k string) (*info, bool) {
c, ok := i.children[k]
return c, ok
}
func (i *info) SetKind(kind keyKind) {
i.kind = kind
}
func (i *info) CreateTable(k string, explicit bool) *info {
return i.createChild(k, tableKind, explicit)
}
func (i *info) CreateArrayTable(k string, explicit bool) *info {
return i.createChild(k, arrayTableKind, explicit)
}
func (i *info) createChild(k string, kind keyKind, explicit bool) *info {
if i.children == nil {
i.children = make(map[string]*info, 1)
}
x := &info{
parent: i,
kind: kind,
explicit: explicit,
}
i.children[k] = x
return x
}
// CheckExpression takes a top-level node and checks that it does not contain keys
// that have been seen in previous calls, and validates that types are consistent.
func (s *Seen) CheckExpression(node ast.Node) error {
if s.root == nil {
s.root = &info{
kind: tableKind,
}
s.current = s.root
}
switch node.Kind {
case ast.KeyValue:
return s.checkKeyValue(s.current, node)
case ast.Table:
return s.checkTable(node)
case ast.ArrayTable:
return s.checkArrayTable(node)
default:
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
}
}
func (s *Seen) checkTable(node ast.Node) error {
s.current = s.root
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
if !it.Node().Next().Valid() {
break
}
k := string(it.Node().Data)
child, found := s.current.Has(k)
if !found {
child = s.current.CreateTable(k, false)
}
s.current = child
}
// handle the last part of the key
k := string(it.Node().Data)
i, found := s.current.Has(k)
if found {
if i.kind != tableKind {
return fmt.Errorf("key %s should be a table", k)
}
if i.explicit {
return fmt.Errorf("table %s already exists", k)
}
i.explicit = true
s.current = i
} else {
s.current = s.current.CreateTable(k, true)
}
return nil
}
func (s *Seen) checkArrayTable(node ast.Node) error {
s.current = s.root
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
if !it.Node().Next().Valid() {
break
}
k := string(it.Node().Data)
child, found := s.current.Has(k)
if !found {
child = s.current.CreateTable(k, false)
}
s.current = child
}
// handle the last part of the key
k := string(it.Node().Data)
info, found := s.current.Has(k)
if found {
if info.kind != arrayTableKind {
return fmt.Errorf("key %s already exists but is not an array table", k)
}
info.Clear()
} else {
info = s.current.CreateArrayTable(k, true)
}
s.current = info
return nil
}
func (s *Seen) checkKeyValue(context *info, node ast.Node) error {
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
k := string(it.Node().Data)
child, found := context.Has(k)
if found {
if child.kind != tableKind {
return fmt.Errorf("expected %s to be a table, not a %s", k, child.kind)
}
} else {
child = context.CreateTable(k, false)
}
context = child
}
if node.Value().Kind == ast.InlineTable {
context.SetKind(tableKind)
} else {
context.SetKind(valueKind)
}
return nil
}
-35
View File
@@ -1,35 +0,0 @@
package unsafe
import (
"fmt"
"reflect"
"unsafe"
)
const maxInt = uintptr(int(^uint(0) >> 1))
func SubsliceOffset(data []byte, subslice []byte) int {
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
if hlp.Data < datap.Data {
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
}
offset := hlp.Data - datap.Data
if offset > maxInt {
panic(fmt.Errorf("slice offset larger than int (%d)", offset))
}
intoffset := int(offset)
if intoffset > datap.Len {
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
}
if intoffset+hlp.Len > datap.Len {
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
}
return intoffset
}
-79
View File
@@ -1,79 +0,0 @@
package unsafe_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pelletier/go-toml/v2/internal/unsafe"
)
func TestUnsafeSubsliceOffsetValid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
offset int
}{
{
desc: "simple",
test: func() ([]byte, []byte) {
data := []byte("hello")
return data, data[1:]
},
offset: 1,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
offset := unsafe.SubsliceOffset(d, s)
assert.Equal(t, e.offset, offset)
})
}
}
func TestUnsafeSubsliceOffsetInvalid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
}{
{
desc: "unrelated arrays",
test: func() ([]byte, []byte) {
return []byte("one"), []byte("two")
},
},
{
desc: "slice starts before data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[5:], full[1:]
},
},
{
desc: "slice starts after data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[:3], full[5:]
},
},
{
desc: "slice ends after data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[:5], full[3:8]
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
require.Panics(t, func() {
unsafe.SubsliceOffset(d, s)
})
})
}
}
+38 -19
View File
@@ -23,6 +23,7 @@
// //
// Because they lack location information, these types do not represent unique // Because they lack location information, these types do not represent unique
// moments or intervals of time. Use time.Time for that purpose. // moments or intervals of time. Use time.Time for that purpose.
package toml package toml
import ( import (
@@ -44,6 +45,7 @@ type LocalDate struct {
func LocalDateOf(t time.Time) LocalDate { func LocalDateOf(t time.Time) LocalDate {
var d LocalDate var d LocalDate
d.Year, d.Month, d.Day = t.Date() d.Year, d.Month, d.Day = t.Date()
return d return d
} }
@@ -53,6 +55,7 @@ func ParseLocalDate(s string) (LocalDate, error) {
if err != nil { if err != nil {
return LocalDate{}, err return LocalDate{}, err
} }
return LocalDateOf(t), nil return LocalDateOf(t), nil
} }
@@ -92,23 +95,28 @@ func (d LocalDate) DaysSince(s LocalDate) (days int) {
// We convert to Unix time so we do not have to worry about leap seconds: // We convert to Unix time so we do not have to worry about leap seconds:
// Unix time increases by exactly 86400 seconds per day. // Unix time increases by exactly 86400 seconds per day.
deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix() deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix()
return int(deltaUnix / 86400)
const secondsInADay = 86400
return int(deltaUnix / secondsInADay)
} }
// Before reports whether d1 occurs before d2. // Before reports whether d1 occurs before future date.
func (d1 LocalDate) Before(d2 LocalDate) bool { func (d LocalDate) Before(future LocalDate) bool {
if d1.Year != d2.Year { if d.Year != future.Year {
return d1.Year < d2.Year return d.Year < future.Year
} }
if d1.Month != d2.Month {
return d1.Month < d2.Month if d.Month != future.Month {
return d.Month < future.Month
} }
return d1.Day < d2.Day
return d.Day < future.Day
} }
// After reports whether d1 occurs after d2. // After reports whether d1 occurs after past date.
func (d1 LocalDate) After(d2 LocalDate) bool { func (d LocalDate) After(past LocalDate) bool {
return d2.Before(d1) return past.Before(d)
} }
// MarshalText implements the encoding.TextMarshaler interface. // MarshalText implements the encoding.TextMarshaler interface.
@@ -122,6 +130,7 @@ func (d LocalDate) MarshalText() ([]byte, error) {
func (d *LocalDate) UnmarshalText(data []byte) error { func (d *LocalDate) UnmarshalText(data []byte) error {
var err error var err error
*d, err = ParseLocalDate(string(data)) *d, err = ParseLocalDate(string(data))
return err return err
} }
@@ -145,6 +154,7 @@ func LocalTimeOf(t time.Time) LocalTime {
var tm LocalTime var tm LocalTime
tm.Hour, tm.Minute, tm.Second = t.Clock() tm.Hour, tm.Minute, tm.Second = t.Clock()
tm.Nanosecond = t.Nanosecond() tm.Nanosecond = t.Nanosecond()
return tm return tm
} }
@@ -158,6 +168,7 @@ func ParseLocalTime(s string) (LocalTime, error) {
if err != nil { if err != nil {
return LocalTime{}, err return LocalTime{}, err
} }
return LocalTimeOf(t), nil return LocalTimeOf(t), nil
} }
@@ -169,6 +180,7 @@ func (t LocalTime) String() string {
if t.Nanosecond == 0 { if t.Nanosecond == 0 {
return s return s
} }
return s + fmt.Sprintf(".%09d", t.Nanosecond) return s + fmt.Sprintf(".%09d", t.Nanosecond)
} }
@@ -176,6 +188,7 @@ func (t LocalTime) String() string {
func (t LocalTime) IsValid() bool { func (t LocalTime) IsValid() bool {
// Construct a non-zero time. // Construct a non-zero time.
tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC) tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC)
return LocalTimeOf(tm) == t return LocalTimeOf(tm) == t
} }
@@ -190,6 +203,7 @@ func (t LocalTime) MarshalText() ([]byte, error) {
func (t *LocalTime) UnmarshalText(data []byte) error { func (t *LocalTime) UnmarshalText(data []byte) error {
var err error var err error
*t, err = ParseLocalTime(string(data)) *t, err = ParseLocalTime(string(data))
return err return err
} }
@@ -226,6 +240,7 @@ func ParseLocalDateTime(s string) (LocalDateTime, error) {
return LocalDateTime{}, err return LocalDateTime{}, err
} }
} }
return LocalDateTimeOf(t), nil return LocalDateTimeOf(t), nil
} }
@@ -253,17 +268,20 @@ func (dt LocalDateTime) IsValid() bool {
// //
// In panics if loc is nil. // In panics if loc is nil.
func (dt LocalDateTime) In(loc *time.Location) time.Time { func (dt LocalDateTime) In(loc *time.Location) time.Time {
return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) return time.Date(
dt.Date.Year, dt.Date.Month, dt.Date.Day,
dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc,
)
} }
// Before reports whether dt1 occurs before dt2. // Before reports whether dt occurs before future.
func (dt1 LocalDateTime) Before(dt2 LocalDateTime) bool { func (dt LocalDateTime) Before(future LocalDateTime) bool {
return dt1.In(time.UTC).Before(dt2.In(time.UTC)) return dt.In(time.UTC).Before(future.In(time.UTC))
} }
// After reports whether dt1 occurs after dt2. // After reports whether dt occurs after past.
func (dt1 LocalDateTime) After(dt2 LocalDateTime) bool { func (dt LocalDateTime) After(past LocalDateTime) bool {
return dt2.Before(dt1) return past.Before(dt)
} }
// MarshalText implements the encoding.TextMarshaler interface. // MarshalText implements the encoding.TextMarshaler interface.
@@ -273,9 +291,10 @@ func (dt LocalDateTime) MarshalText() ([]byte, error) {
} }
// UnmarshalText implements the encoding.TextUnmarshaler interface. // UnmarshalText implements the encoding.TextUnmarshaler interface.
// The datetime is expected to be a string in a format accepted by ParseLocalDateTime // The datetime is expected to be a string in a format accepted by ParseLocalDateTime.
func (dt *LocalDateTime) UnmarshalText(data []byte) error { func (dt *LocalDateTime) UnmarshalText(data []byte) error {
var err error var err error
*dt, err = ParseLocalDateTime(string(data)) *dt, err = ParseLocalDateTime(string(data))
return err return err
} }
+60 -17
View File
@@ -26,6 +26,7 @@ func cmpEqual(x, y interface{}) bool {
} }
func TestDates(t *testing.T) { func TestDates(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
date LocalDate date LocalDate
loc *time.Location loc *time.Location
@@ -54,6 +55,7 @@ func TestDates(t *testing.T) {
if got := test.date.String(); got != test.wantStr { if got := test.date.String(); got != test.wantStr {
t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr) t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr)
} }
if got := test.date.In(test.loc); !got.Equal(test.wantTime) { if got := test.date.In(test.loc); !got.Equal(test.wantTime) {
t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime) t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime)
} }
@@ -61,6 +63,7 @@ func TestDates(t *testing.T) {
} }
func TestDateIsValid(t *testing.T) { func TestDateIsValid(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
date LocalDate date LocalDate
want bool want bool
@@ -86,6 +89,9 @@ func TestDateIsValid(t *testing.T) {
} }
func TestParseDate(t *testing.T) { func TestParseDate(t *testing.T) {
var emptyDate LocalDate
for _, test := range []struct { for _, test := range []struct {
str string str string
want LocalDate // if empty, expect an error want LocalDate // if empty, expect an error
@@ -93,21 +99,23 @@ func TestParseDate(t *testing.T) {
{"2016-01-02", LocalDate{2016, 1, 2}}, {"2016-01-02", LocalDate{2016, 1, 2}},
{"2016-12-31", LocalDate{2016, 12, 31}}, {"2016-12-31", LocalDate{2016, 12, 31}},
{"0003-02-04", LocalDate{3, 2, 4}}, {"0003-02-04", LocalDate{3, 2, 4}},
{"999-01-26", LocalDate{}}, {"999-01-26", emptyDate},
{"", LocalDate{}}, {"", emptyDate},
{"2016-01-02x", LocalDate{}}, {"2016-01-02x", emptyDate},
} { } {
got, err := ParseLocalDate(test.str) got, err := ParseLocalDate(test.str)
if got != test.want { if got != test.want {
t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want) t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want)
} }
if err != nil && test.want != (LocalDate{}) {
if err != nil && test.want != (emptyDate) {
t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str) t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str)
} }
} }
} }
func TestDateArithmetic(t *testing.T) { func TestDateArithmetic(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
desc string desc string
start LocalDate start LocalDate
@@ -160,6 +168,7 @@ func TestDateArithmetic(t *testing.T) {
if got := test.start.AddDays(test.days); got != test.end { if got := test.start.AddDays(test.days); got != test.end {
t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end) t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end)
} }
if got := test.end.DaysSince(test.start); got != test.days { if got := test.end.DaysSince(test.start); got != test.days {
t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days) t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days)
} }
@@ -167,6 +176,7 @@ func TestDateArithmetic(t *testing.T) {
} }
func TestDateBefore(t *testing.T) { func TestDateBefore(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
d1, d2 LocalDate d1, d2 LocalDate
want bool want bool
@@ -183,6 +193,7 @@ func TestDateBefore(t *testing.T) {
} }
func TestDateAfter(t *testing.T) { func TestDateAfter(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
d1, d2 LocalDate d1, d2 LocalDate
want bool want bool
@@ -198,6 +209,7 @@ func TestDateAfter(t *testing.T) {
} }
func TestTimeToString(t *testing.T) { func TestTimeToString(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
str string str string
time LocalTime time LocalTime
@@ -212,11 +224,14 @@ func TestTimeToString(t *testing.T) {
gotTime, err := ParseLocalTime(test.str) gotTime, err := ParseLocalTime(test.str)
if err != nil { if err != nil {
t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err) t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err)
continue continue
} }
if gotTime != test.time { if gotTime != test.time {
t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time) t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time)
} }
if test.roundTrip { if test.roundTrip {
gotStr := test.time.String() gotStr := test.time.String()
if gotStr != test.str { if gotStr != test.str {
@@ -227,6 +242,7 @@ func TestTimeToString(t *testing.T) {
} }
func TestTimeOf(t *testing.T) { func TestTimeOf(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
time time.Time time time.Time
want LocalTime want LocalTime
@@ -241,6 +257,7 @@ func TestTimeOf(t *testing.T) {
} }
func TestTimeIsValid(t *testing.T) { func TestTimeIsValid(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
time LocalTime time LocalTime
want bool want bool
@@ -265,23 +282,27 @@ func TestTimeIsValid(t *testing.T) {
} }
func TestDateTimeToString(t *testing.T) { func TestDateTimeToString(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
str string str string
dateTime LocalDateTime dateTime LocalDateTime
roundTrip bool // ParseLocalDateTime(str).String() == str? roundTrip bool // ParseLocalDateTime(str).String() == str?
}{ }{
{"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, true}, {"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 0}}, true},
{"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 600}}, true}, {"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 600}}, true},
{"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, false}, {"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 0}}, false},
} { } {
gotDateTime, err := ParseLocalDateTime(test.str) gotDateTime, err := ParseLocalDateTime(test.str)
if err != nil { if err != nil {
t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err) t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err)
continue continue
} }
if gotDateTime != test.dateTime { if gotDateTime != test.dateTime {
t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime) t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime)
} }
if test.roundTrip { if test.roundTrip {
gotStr := test.dateTime.String() gotStr := test.dateTime.String()
if gotStr != test.str { if gotStr != test.str {
@@ -292,6 +313,7 @@ func TestDateTimeToString(t *testing.T) {
} }
func TestParseDateTimeErrors(t *testing.T) { func TestParseDateTimeErrors(t *testing.T) {
for _, str := range []string{ for _, str := range []string{
"", "",
"2016-03-22", // just a date "2016-03-22", // just a date
@@ -306,14 +328,19 @@ func TestParseDateTimeErrors(t *testing.T) {
} }
func TestDateTimeOf(t *testing.T) { func TestDateTimeOf(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
time time.Time time time.Time
want LocalDateTime want LocalDateTime
}{ }{
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), {
LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}}}, time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local),
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}},
LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}}}, },
{
time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}},
},
} { } {
if got := LocalDateTimeOf(test.time); got != test.want { if got := LocalDateTimeOf(test.time); got != test.want {
t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want) t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
@@ -322,6 +349,7 @@ func TestDateTimeOf(t *testing.T) {
} }
func TestDateTimeIsValid(t *testing.T) { func TestDateTimeIsValid(t *testing.T) {
// No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid. // No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid.
for _, test := range []struct { for _, test := range []struct {
dt LocalDateTime dt LocalDateTime
@@ -339,19 +367,22 @@ func TestDateTimeIsValid(t *testing.T) {
} }
func TestDateTimeIn(t *testing.T) { func TestDateTimeIn(t *testing.T) {
dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}} dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}}
got := dt.In(time.UTC)
want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC) want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC)
if !got.Equal(want) { if got := dt.In(time.UTC); !got.Equal(want) {
t.Errorf("got %v, want %v", got, want) t.Errorf("got %v, want %v", got, want)
} }
} }
func TestDateTimeBefore(t *testing.T) { func TestDateTimeBefore(t *testing.T) {
d1 := LocalDate{2016, 12, 31} d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1} d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8} t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9} t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct { for _, test := range []struct {
dt1, dt2 LocalDateTime dt1, dt2 LocalDateTime
want bool want bool
@@ -368,10 +399,12 @@ func TestDateTimeBefore(t *testing.T) {
} }
func TestDateTimeAfter(t *testing.T) { func TestDateTimeAfter(t *testing.T) {
d1 := LocalDate{2016, 12, 31} d1 := LocalDate{2016, 12, 31}
d2 := LocalDate{2017, 1, 1} d2 := LocalDate{2017, 1, 1}
t1 := LocalTime{5, 6, 7, 8} t1 := LocalTime{5, 6, 7, 8}
t2 := LocalTime{5, 6, 7, 9} t2 := LocalTime{5, 6, 7, 9}
for _, test := range []struct { for _, test := range []struct {
dt1, dt2 LocalDateTime dt1, dt2 LocalDateTime
want bool want bool
@@ -388,6 +421,7 @@ func TestDateTimeAfter(t *testing.T) {
} }
func TestMarshalJSON(t *testing.T) { func TestMarshalJSON(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
value interface{} value interface{}
want string want string
@@ -400,6 +434,7 @@ func TestMarshalJSON(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got := string(bgot); got != test.want { if got := string(bgot); got != test.want {
t.Errorf("%#v: got %s, want %s", test.value, got, test.want) t.Errorf("%#v: got %s, want %s", test.value, got, test.want)
} }
@@ -407,9 +442,13 @@ func TestMarshalJSON(t *testing.T) {
} }
func TestUnmarshalJSON(t *testing.T) { func TestUnmarshalJSON(t *testing.T) {
var d LocalDate
var tm LocalTime var (
var dt LocalDateTime d LocalDate
tm LocalTime
dt LocalDateTime
)
for _, test := range []struct { for _, test := range []struct {
data string data string
ptr interface{} ptr interface{}
@@ -423,12 +462,14 @@ func TestUnmarshalJSON(t *testing.T) {
if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil { if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil {
t.Fatalf("%s: %v", test.data, err) t.Fatalf("%s: %v", test.data, err)
} }
if !cmpEqual(test.ptr, test.want) { if !cmpEqual(test.ptr, test.want) {
t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want) t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want)
} }
} }
for _, bad := range []string{"", `""`, `"bad"`, `"1987-04-15x"`, for _, bad := range []string{
"", `""`, `"bad"`, `"1987-04-15x"`,
`19870415`, // a JSON number `19870415`, // a JSON number
`11987-04-15x`, // not a JSON string `11987-04-15x`, // not a JSON string
@@ -436,9 +477,11 @@ func TestUnmarshalJSON(t *testing.T) {
if json.Unmarshal([]byte(bad), &d) == nil { if json.Unmarshal([]byte(bad), &d) == nil {
t.Errorf("%q, LocalDate: got nil, want error", bad) t.Errorf("%q, LocalDate: got nil, want error", bad)
} }
if json.Unmarshal([]byte(bad), &tm) == nil { if json.Unmarshal([]byte(bad), &tm) == nil {
t.Errorf("%q, LocalTime: got nil, want error", bad) t.Errorf("%q, LocalTime: got nil, want error", bad)
} }
if json.Unmarshal([]byte(bad), &dt) == nil { if json.Unmarshal([]byte(bad), &dt) == nil {
t.Errorf("%q, LocalDateTime: got nil, want error", bad) t.Errorf("%q, LocalDateTime: got nil, want error", bad)
} }
+330 -194
View File
@@ -2,7 +2,7 @@ package toml
import ( import (
"bytes" "bytes"
"errors" "encoding"
"fmt" "fmt"
"io" "io"
"reflect" "reflect"
@@ -18,16 +18,134 @@ import (
func Marshal(v interface{}) ([]byte, error) { func Marshal(v interface{}) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
enc := NewEncoder(&buf) enc := NewEncoder(&buf)
err := enc.Encode(v) err := enc.Encode(v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// Encoder writes a TOML document to an output stream. // Encoder writes a TOML document to an output stream.
type Encoder struct { type Encoder struct {
// output
w io.Writer w io.Writer
// global settings
tablesInline bool
arraysMultiline bool
indentSymbol string
indentTables bool
}
// NewEncoder returns a new Encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
indentSymbol: " ",
}
}
// SetTablesInline forces the encoder to emit all tables inline.
//
// This behavior can be controlled on an individual struct field basis with the
// inline tag:
//
// MyField `inline:"true"`
func (enc *Encoder) SetTablesInline(inline bool) {
enc.tablesInline = inline
}
// SetArraysMultiline forces the encoder to emit all arrays with one element per
// line.
//
// This behavior can be controlled on an individual struct field basis with the multiline tag:
//
// MyField `multiline:"true"`
func (enc *Encoder) SetArraysMultiline(multiline bool) {
enc.arraysMultiline = multiline
}
// SetIndentSymbol defines the string that should be used for indentation. The
// provided string is repeated for each indentation level. Defaults to two
// spaces.
func (enc *Encoder) SetIndentSymbol(s string) {
enc.indentSymbol = s
}
// SetIndentTables forces the encoder to intent tables and array tables.
func (enc *Encoder) SetIndentTables(indent bool) {
enc.indentTables = indent
}
// Encode writes a TOML representation of v to the stream.
//
// If v cannot be represented to TOML it returns an error.
//
// Encoding rules
//
// A top level slice containing only maps or structs is encoded as [[table
// array]].
//
// All slices not matching rule 1 are encoded as [array]. As a result, any map
// or struct they contain is encoded as an {inline table}.
//
// Nil interfaces and nil pointers are not supported.
//
// Keys in key-values always have one part.
//
// Intermediate tables are always printed.
//
// By default, strings are encoded as literal string, unless they contain either
// a newline character or a single quote. In that case they are emitted as quoted
// strings.
//
// When encoding structs, fields are encoded in order of definition, with their
// exact name.
//
// Struct tags
//
// The following struct tags are available to tweak encoding on a per-field
// basis:
//
// toml:"foo"
// Changes the name of the key to use for the field to foo.
//
// multiline:"true"
// When the field contains a string, it will be emitted as a quoted
// multi-line TOML string.
//
// inline:"true"
// When the field would normally be encoded as a table, it is instead
// encoded as an inline table.
func (enc *Encoder) Encode(v interface{}) error {
var (
b []byte
ctx encoderCtx
)
ctx.inline = enc.tablesInline
if v == nil {
return fmt.Errorf("toml: cannot encode a nil interface")
}
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
if err != nil {
return err
}
_, err = enc.w.Write(b)
if err != nil {
return fmt.Errorf("toml: cannot write: %w", err)
}
return nil
}
type valueOptions struct {
multiline bool
} }
type encoderCtx struct { type encoderCtx struct {
@@ -46,11 +164,14 @@ type encoderCtx struct {
// Set to true to skip the first table header in an array table. // Set to true to skip the first table header in an array table.
skipTableHeader bool skipTableHeader bool
options valueOptions // Should the next table be encoded as inline
} inline bool
type valueOptions struct { // Indentation level
multiline bool indent int
// Options coming from struct tags
options valueOptions
} }
func (ctx *encoderCtx) shiftKey() { func (ctx *encoderCtx) shiftKey() {
@@ -70,62 +191,36 @@ func (ctx *encoderCtx) clearKey() {
ctx.hasKey = false ctx.hasKey = false
} }
// NewEncoder returns a new Encoder that writes to w. func (ctx *encoderCtx) isRoot() bool {
func NewEncoder(w io.Writer) *Encoder { return len(ctx.parentKey) == 0 && !ctx.hasKey
return &Encoder{
w: w,
}
}
// Encode writes a TOML representation of v to the stream.
//
// If v cannot be represented to TOML it returns an error.
//
// Encoding rules:
//
// 1. A top level slice containing only maps or structs is encoded as [[table
// array]].
//
// 2. All slices not matching rule 1 are encoded as [array]. As a result, any
// map or struct they contain is encoded as an {inline table}.
//
// 3. Nil interfaces and nil pointers are not supported.
//
// 4. Keys in key-values always have one part.
//
// 5. Intermediate tables are always printed.
//
// By default, strings are encoded as literal string, unless they contain either
// a newline character or a single quote. In that case they are emited as quoted
// strings.
//
// When encoding structs, fields are encoded in order of definition, with their
// exact name. The following struct tags are available:
//
// `toml:"foo"`: changes the name of the key to use for the field to foo.
//
// `multiline:"true"`: when the field contains a string, it will be emitted as
// a quoted multi-line TOML string.
func (enc *Encoder) Encode(v interface{}) error {
var b []byte
var ctx encoderCtx
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
if err != nil {
return err
}
_, err = enc.w.Write(b)
return err
} }
//nolint:cyclop,funlen
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
switch i := v.Interface().(type) { if !v.IsZero() {
case time.Time: // TODO: add TextMarshaler i, ok := v.Interface().(time.Time)
b = i.AppendFormat(b, time.RFC3339) if ok {
return i.AppendFormat(b, time.RFC3339), nil
}
}
if v.Type().Implements(textMarshalerType) {
if ctx.isRoot() {
return nil, fmt.Errorf("toml: type %s implementing the TextMarshaler interface cannot be a root element", v.Type())
}
text, err := v.Interface().(encoding.TextMarshaler).MarshalText()
if err != nil {
return nil, err
}
b = enc.encodeString(b, string(text), ctx.options)
return b, nil return b, nil
} }
// containers
switch v.Kind() { switch v.Kind() {
// containers
case reflect.Map: case reflect.Map:
return enc.encodeMap(b, ctx, v) return enc.encodeMap(b, ctx, v)
case reflect.Struct: case reflect.Struct:
@@ -134,21 +229,20 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
return enc.encodeSlice(b, ctx, v) return enc.encodeSlice(b, ctx, v)
case reflect.Interface: case reflect.Interface:
if v.IsNil() { if v.IsNil() {
return nil, errNilInterface return nil, fmt.Errorf("toml: encoding a nil interface is not supported")
} }
return enc.encode(b, ctx, v.Elem()) return enc.encode(b, ctx, v.Elem())
case reflect.Ptr: case reflect.Ptr:
if v.IsNil() { if v.IsNil() {
return enc.encode(b, ctx, reflect.Zero(v.Type().Elem())) return enc.encode(b, ctx, reflect.Zero(v.Type().Elem()))
} }
return enc.encode(b, ctx, v.Elem()) return enc.encode(b, ctx, v.Elem())
}
// values // values
var err error
switch v.Kind() {
case reflect.String: case reflect.String:
b, err = enc.encodeString(b, v.String(), ctx.options) b = enc.encodeString(b, v.String(), ctx.options)
case reflect.Float32: case reflect.Float32:
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32) b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
case reflect.Float64: case reflect.Float64:
@@ -164,10 +258,7 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
b = strconv.AppendInt(b, v.Int(), 10) b = strconv.AppendInt(b, v.Int(), 10)
default: default:
err = fmt.Errorf("unsupported encode value kind: %s", v.Kind()) return nil, fmt.Errorf("toml: cannot encode value of type %s", v.Kind())
}
if err != nil {
return nil, err
} }
return b, nil return b, nil
@@ -188,10 +279,7 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
if !ctx.hasKey { if !ctx.hasKey {
panic("caller of encodeKv should have set the key in the context") panic("caller of encodeKv should have set the key in the context")
} }
b = enc.indent(ctx.indent, b)
if isNil(v) {
return b, nil
}
b, err = enc.encodeKey(b, ctx.key) b, err = enc.encodeKey(b, ctx.key)
if err != nil { if err != nil {
@@ -217,30 +305,31 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
const literalQuote = '\'' const literalQuote = '\''
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) ([]byte, error) { func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
if needsQuoting(v) { if needsQuoting(v) {
b = enc.encodeQuotedString(options.multiline, b, v) return enc.encodeQuotedString(options.multiline, b, v)
} else {
b = enc.encodeLiteralString(b, v)
} }
return b, nil
return enc.encodeLiteralString(b, v)
} }
func needsQuoting(v string) bool { func needsQuoting(v string) bool {
return strings.ContainsAny(v, "'\b\f\n\r\t") return strings.ContainsAny(v, "'\b\f\n\r\t")
} }
// caller should have checked that the string does not contain new lines or ' // caller should have checked that the string does not contain new lines or ' .
func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte { func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
b = append(b, literalQuote) b = append(b, literalQuote)
b = append(b, v...) b = append(b, v...)
b = append(b, literalQuote) b = append(b, literalQuote)
return b return b
} }
//nolint:cyclop
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte { func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
const hextable = "0123456789ABCDEF"
stringQuote := `"` stringQuote := `"`
if multiline { if multiline {
stringQuote = `"""` stringQuote = `"""`
} }
@@ -250,6 +339,16 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
b = append(b, '\n') b = append(b, '\n')
} }
const (
hextable = "0123456789ABCDEF"
// U+0000 to U+0008, U+000A to U+001F, U+007F
nul = 0x0
bs = 0x8
lf = 0xa
us = 0x1f
del = 0x7f
)
for _, r := range []byte(v) { for _, r := range []byte(v) {
switch r { switch r {
case '\\': case '\\':
@@ -272,7 +371,7 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
b = append(b, `\t`...) b = append(b, `\t`...)
default: default:
switch { switch {
case r >= 0x0 && r <= 0x8, r >= 0xA && r <= 0x1F, r == 0x7F: case r >= nul && r <= bs, r >= lf && r <= us, r == del:
b = append(b, `\u00`...) b = append(b, `\u00`...)
b = append(b, hextable[r>>4]) b = append(b, hextable[r>>4])
b = append(b, hextable[r&0x0f]) b = append(b, hextable[r&0x0f])
@@ -280,33 +379,37 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
b = append(b, r) b = append(b, r)
} }
} }
// U+0000 to U+0008, U+000A to U+001F, U+007F
} }
b = append(b, stringQuote...) b = append(b, stringQuote...)
return b return b
} }
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ // called should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte { func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...) return append(b, v...)
} }
func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) { func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error) {
if len(key) == 0 { if len(ctx.parentKey) == 0 {
return b, nil return b, nil
} }
b = enc.indent(ctx.indent, b)
b = append(b, '[') b = append(b, '[')
var err error var err error
b, err = enc.encodeKey(b, key[0])
b, err = enc.encodeKey(b, ctx.parentKey[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, k := range key[1:] { for _, k := range ctx.parentKey[1:] {
b = append(b, '.') b = append(b, '.')
b, err = enc.encodeKey(b, k) b, err = enc.encodeKey(b, k)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -318,6 +421,7 @@ func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) {
return b, nil return b, nil
} }
//nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) { func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
needsQuotation := false needsQuotation := false
cannotUseLiteral := false cannotUseLiteral := false
@@ -326,32 +430,37 @@ func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue continue
} }
if c == '\n' { if c == '\n' {
return nil, fmt.Errorf("TOML does not support multiline keys") return nil, fmt.Errorf("toml: new line characters in keys are not supported")
} }
if c == literalQuote { if c == literalQuote {
cannotUseLiteral = true cannotUseLiteral = true
} }
needsQuotation = true needsQuotation = true
} }
if cannotUseLiteral { switch {
b = enc.encodeQuotedString(false, b, k) case cannotUseLiteral:
} else if needsQuotation { return enc.encodeQuotedString(false, b, k), nil
b = enc.encodeLiteralString(b, k) case needsQuotation:
} else { return enc.encodeLiteralString(b, k), nil
b = enc.encodeUnquotedKey(b, k) default:
return enc.encodeUnquotedKey(b, k), nil
} }
return b, nil
} }
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Type().Key().Kind() != reflect.String { if v.Type().Key().Kind() != reflect.String {
return nil, fmt.Errorf("type '%s' not supported as map key", v.Type().Key().Kind()) return nil, fmt.Errorf("toml: type %s is not supported as a map key", v.Type().Key().Kind())
} }
t := table{} var (
t table
emptyValueOptions valueOptions
)
iter := v.MapRange() iter := v.MapRange()
for iter.Next() { for iter.Next() {
@@ -362,15 +471,10 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
continue continue
} }
table, err := willConvertToTableOrArrayTable(v) if willConvertToTableOrArrayTable(ctx, v) {
if err != nil { t.pushTable(k, v, emptyValueOptions)
return nil, err
}
if table {
t.pushTable(k, v, valueOptions{})
} else { } else {
t.pushKV(k, v, valueOptions{}) t.pushKV(k, v, emptyValueOptions)
} }
} }
@@ -405,13 +509,10 @@ func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options}) t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
} }
func (t *table) hasKVs() bool {
return len(t.kvs) > 0
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
t := table{} var t table
//nolint:godox
// TODO: cache this? // TODO: cache this?
typ := v.Type() typ := v.Type()
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
@@ -438,167 +539,167 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
willConvert, err := willConvertToTableOrArrayTable(f) options := valueOptions{
if err != nil { multiline: fieldBoolTag(fieldType, "multiline"),
return nil, err
} }
options := valueOptions{} inline := fieldBoolTag(fieldType, "inline")
ml, ok := fieldType.Tag.Lookup("multiline") if inline || !willConvertToTableOrArrayTable(ctx, f) {
if ok {
options.multiline = ml == "true"
}
if willConvert {
t.pushTable(k, f, options)
} else {
t.pushKV(k, f, options) t.pushKV(k, f, options)
} else {
t.pushTable(k, f, options)
} }
} }
return enc.encodeTable(b, ctx, t) return enc.encodeTable(b, ctx, t)
} }
func fieldBoolTag(field reflect.StructField, tag string) bool {
x, ok := field.Tag.Lookup(tag)
return ok && x == "true"
}
//nolint:cyclop
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) { func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error var err error
ctx.shiftKey() ctx.shiftKey()
if ctx.insideKv { if ctx.insideKv || (ctx.inline && !ctx.isRoot()) {
b = append(b, '{') return enc.encodeTableInline(b, ctx, t)
first := true
for _, kv := range t.kvs {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
}
for _, table := range t.tables {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(table.Key)
b, err = enc.encode(b, ctx, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
b = append(b, "}\n"...)
return b, nil
} }
if !ctx.skipTableHeader { if !ctx.skipTableHeader {
b, err = enc.encodeTableHeader(b, ctx.parentKey) b, err = enc.encodeTableHeader(ctx, b)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if enc.indentTables && len(ctx.parentKey) > 0 {
ctx.indent++
}
} }
ctx.skipTableHeader = false ctx.skipTableHeader = false
for _, kv := range t.kvs { for _, kv := range t.kvs {
ctx.setKey(kv.Key) ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value) b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil { if err != nil {
return nil, err return nil, err
} }
b = append(b, '\n') b = append(b, '\n')
} }
for _, table := range t.tables { for _, table := range t.tables {
ctx.setKey(table.Key) ctx.setKey(table.Key)
ctx.options = table.Options
b, err = enc.encode(b, ctx, table.Value) b, err = enc.encode(b, ctx, table.Value)
if err != nil { if err != nil {
return nil, err return nil, err
} }
b = append(b, '\n') b = append(b, '\n')
} }
return b, nil return b, nil
} }
var errNilInterface = errors.New("nil interface not supported") func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var errNilPointer = errors.New("nil pointer not supported") var err error
func willConvertToTable(v reflect.Value) (bool, error) { b = append(b, '{')
switch v.Interface().(type) {
case time.Time: // TODO: add TextMarshaler first := true
return false, nil for _, kv := range t.kvs {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
}
if len(t.tables) > 0 {
panic("inline table cannot contain nested tables, online key-values")
}
b = append(b, "}"...)
return b, nil
}
func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
if !v.IsValid() {
return false
}
if v.Type() == timeType || v.Type().Implements(textMarshalerType) {
return false
} }
t := v.Type() t := v.Type()
switch t.Kind() { switch t.Kind() {
case reflect.Map, reflect.Struct: case reflect.Map, reflect.Struct:
return true, nil return !ctx.inline
case reflect.Interface: case reflect.Interface:
if v.IsNil() { return willConvertToTable(ctx, v.Elem())
return false, errNilInterface
}
return willConvertToTable(v.Elem())
case reflect.Ptr: case reflect.Ptr:
if v.IsNil() { if v.IsNil() {
return false, nil return false
} }
return willConvertToTable(v.Elem())
return willConvertToTable(ctx, v.Elem())
default: default:
return false, nil return false
} }
} }
func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) { func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
t := v.Type() t := v.Type()
if t.Kind() == reflect.Interface { if t.Kind() == reflect.Interface {
if v.IsNil() { return willConvertToTableOrArrayTable(ctx, v.Elem())
return false, errNilInterface
}
return willConvertToTableOrArrayTable(v.Elem())
} }
if t.Kind() == reflect.Slice { if t.Kind() == reflect.Slice {
if v.Len() == 0 { if v.Len() == 0 {
// An empty slice should be a kv = []. // An empty slice should be a kv = [].
return false, nil return false
} }
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
t, err := willConvertToTable(v.Index(i)) t := willConvertToTable(ctx, v.Index(i))
if err != nil {
return false, err
}
if !t { if !t {
return false, nil return false
} }
} }
return true, nil
return true
} }
return willConvertToTable(v) return willConvertToTable(ctx, v)
} }
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Len() == 0 { if v.Len() == 0 {
b = append(b, "[]"...) b = append(b, "[]"...)
return b, nil return b, nil
} }
allTables, err := willConvertToTableOrArrayTable(v) if willConvertToTableOrArrayTable(ctx, v) {
if err != nil {
return nil, err
}
if allTables {
return enc.encodeSliceAsArrayTable(b, ctx, v) return enc.encodeSliceAsArrayTable(b, ctx, v)
} }
@@ -608,54 +709,89 @@ func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]by
// caller should have checked that v is a slice that only contains values that // caller should have checked that v is a slice that only contains values that
// encode into tables. // encode into tables.
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Len() == 0 {
return b, nil
}
ctx.shiftKey() ctx.shiftKey()
var err error var err error
scratch := make([]byte, 0, 64) scratch := make([]byte, 0, 64)
scratch = append(scratch, "[["...) scratch = append(scratch, "[["...)
for i, k := range ctx.parentKey { for i, k := range ctx.parentKey {
if i > 0 { if i > 0 {
scratch = append(scratch, '.') scratch = append(scratch, '.')
} }
scratch, err = enc.encodeKey(scratch, k) scratch, err = enc.encodeKey(scratch, k)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
scratch = append(scratch, "]]\n"...) scratch = append(scratch, "]]\n"...)
ctx.skipTableHeader = true ctx.skipTableHeader = true
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
b = append(b, scratch...) b = append(b, scratch...)
b, err = enc.encode(b, ctx, v.Index(i)) b, err = enc.encode(b, ctx, v.Index(i))
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
return b, nil return b, nil
} }
func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
multiline := ctx.options.multiline || enc.arraysMultiline
separator := ", "
b = append(b, '[') b = append(b, '[')
subCtx := ctx
subCtx.options = valueOptions{}
if multiline {
separator = ",\n"
b = append(b, '\n')
subCtx.indent++
}
var err error var err error
first := true first := true
for i := 0; i < v.Len(); i++ {
if !first {
b = append(b, ", "...)
}
first = false
b, err = enc.encode(b, ctx, v.Index(i)) for i := 0; i < v.Len(); i++ {
if first {
first = false
} else {
b = append(b, separator...)
}
if multiline {
b = enc.indent(subCtx.indent, b)
}
b, err = enc.encode(b, subCtx, v.Index(i))
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if multiline {
b = append(b, '\n')
b = enc.indent(ctx.indent, b)
}
b = append(b, ']') b = append(b, ']')
return b, nil return b, nil
} }
func (enc *Encoder) indent(level int, b []byte) []byte {
for i := 0; i < level; i++ {
b = append(b, enc.indentSymbol...)
}
return b
}
+540 -7
View File
@@ -3,6 +3,7 @@ package toml_test
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"testing" "testing"
@@ -11,7 +12,14 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
//nolint:funlen
func TestMarshal(t *testing.T) { func TestMarshal(t *testing.T) {
someInt := 42
type structInline struct {
A interface{} `inline:"true"`
}
examples := []struct { examples := []struct {
desc string desc string
v interface{} v interface{}
@@ -65,8 +73,6 @@ hello = 'world'`,
a = 'test'`, a = 'test'`,
}, },
{ {
// TODO: this test is flaky because output changes depending on
// the map iteration order.
desc: "map in map in map and string with values", desc: "map in map in map and string with values",
v: map[string]interface{}{ v: map[string]interface{}{
"this": map[string]interface{}{ "this": map[string]interface{}{
@@ -89,6 +95,16 @@ a = 'test'`,
}, },
expected: `array = ['one', 'two', 'three']`, expected: `array = ['one', 'two', 'three']`,
}, },
{
desc: "empty string array",
v: map[string][]string{},
expected: ``,
},
{
desc: "map",
v: map[string][]string{},
expected: ``,
},
{ {
desc: "nested string arrays", desc: "nested string arrays",
v: map[string][][]string{ v: map[string][][]string{
@@ -104,7 +120,7 @@ a = 'test'`,
expected: `array = ['a string', ['one', 'two'], 'last']`, expected: `array = ['a string', ['one', 'two'], 'last']`,
}, },
{ {
desc: "slice of maps", desc: "array of maps",
v: map[string][]map[string]string{ v: map[string][]map[string]string{
"top": { "top": {
{"map1.1": "v1.1"}, {"map1.1": "v1.1"},
@@ -157,7 +173,7 @@ K2 = 'v2'
`, `,
}, },
{ {
desc: "structs in slice with interfaces", desc: "structs in array with interfaces",
v: map[string]interface{}{ v: map[string]interface{}{
"root": map[string]interface{}{ "root": map[string]interface{}{
"nested": []interface{}{ "nested": []interface{}{
@@ -234,17 +250,349 @@ name = 'Alice'
hello hello
world"""`, world"""`,
}, },
{
desc: "inline field",
v: struct {
A map[string]string `inline:"true"`
B map[string]string
}{
A: map[string]string{
"isinline": "yes",
},
B: map[string]string{
"isinline": "no",
},
},
expected: `
A = {isinline = 'yes'}
[B]
isinline = 'no'
`,
},
{
desc: "mutiline array int",
v: struct {
A []int `multiline:"true"`
B []int
}{
A: []int{1, 2, 3, 4},
B: []int{1, 2, 3, 4},
},
expected: `
A = [
1,
2,
3,
4
]
B = [1, 2, 3, 4]
`,
},
{
desc: "mutiline array in array",
v: struct {
A [][]int `multiline:"true"`
}{
A: [][]int{{1, 2}, {3, 4}},
},
expected: `
A = [
[1, 2],
[3, 4]
]
`,
},
{
desc: "nil interface not supported at root",
v: nil,
err: true,
},
{
desc: "nil interface not supported in slice",
v: map[string]interface{}{
"a": []interface{}{"a", nil, 2},
},
err: true,
},
{
desc: "nil pointer in slice uses zero value",
v: struct {
A []*int
}{
A: []*int{nil},
},
expected: `A = [0]`,
},
{
desc: "nil pointer in slice uses zero value",
v: struct {
A []*int
}{
A: []*int{nil},
},
expected: `A = [0]`,
},
{
desc: "pointer in slice",
v: struct {
A []*int
}{
A: []*int{&someInt},
},
expected: `A = [42]`,
},
{
desc: "inline table in inline table",
v: structInline{
A: structInline{
A: structInline{
A: "hello",
},
},
},
expected: `A = {A = {A = 'hello'}}`,
},
{
desc: "empty slice in map",
v: map[string][]string{
"a": {},
},
expected: `a = []`,
},
{
desc: "map in slice",
v: map[string][]map[string]string{
"a": {{"hello": "world"}},
},
expected: `
[[a]]
hello = 'world'`,
},
{
desc: "newline in map in slice",
v: map[string][]map[string]string{
"a\n": {{"hello": "world"}},
},
err: true,
},
{
desc: "newline in map in slice",
v: map[string][]map[string]*customTextMarshaler{
"a": {{"hello": &customTextMarshaler{1}}},
},
err: true,
},
{
desc: "empty slice of empty struct",
v: struct {
A []struct{}
}{
A: []struct{}{},
},
expected: `A = []`,
},
{
desc: "nil field is ignored",
v: struct {
A interface{}
}{
A: nil,
},
expected: ``,
},
{
desc: "private fields are ignored",
v: struct {
Public string
private string
}{
Public: "shown",
private: "hidden",
},
expected: `Public = 'shown'`,
},
{
desc: "fields tagged - are ignored",
v: struct {
Public string `toml:"-"`
private string
}{
Public: "hidden",
},
expected: ``,
},
{
desc: "nil value in map is ignored",
v: map[string]interface{}{
"A": nil,
},
expected: ``,
},
{
desc: "new line in table key",
v: map[string]interface{}{
"hello\nworld": 42,
},
err: true,
},
{
desc: "new line in parent of nested table key",
v: map[string]interface{}{
"hello\nworld": map[string]interface{}{
"inner": 42,
},
},
err: true,
},
{
desc: "new line in nested table key",
v: map[string]interface{}{
"parent": map[string]interface{}{
"in\ner": map[string]interface{}{
"foo": 42,
},
},
},
err: true,
},
{
desc: "invalid map key",
v: map[int]interface{}{},
err: true,
},
{
desc: "unhandled type",
v: struct {
A chan int
}{
A: make(chan int),
},
err: true,
},
{
desc: "numbers",
v: struct {
A float32
B uint64
C uint32
D uint16
E uint8
F uint
G int64
H int32
I int16
J int8
K int
}{
A: 1.1,
B: 42,
C: 42,
D: 42,
E: 42,
F: 42,
G: 42,
H: 42,
I: 42,
J: 42,
K: 42,
},
expected: `
A = 1.1
B = 42
C = 42
D = 42
E = 42
F = 42
G = 42
H = 42
I = 42
J = 42
K = 42`,
},
} }
for _, e := range examples { for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
b, err := toml.Marshal(e.v) b, err := toml.Marshal(e.v)
if e.err { if e.err {
require.Error(t, err) require.Error(t, err)
} else {
require.NoError(t, err) return
equalStringsIgnoreNewlines(t, e.expected, string(b))
} }
require.NoError(t, err)
equalStringsIgnoreNewlines(t, e.expected, string(b))
// make sure the output is always valid TOML
defaultMap := map[string]interface{}{}
err = toml.Unmarshal(b, &defaultMap)
require.NoError(t, err)
testWithAllFlags(t, func(t *testing.T, flags int) {
t.Helper()
var buf bytes.Buffer
enc := toml.NewEncoder(&buf)
setFlags(enc, flags)
err := enc.Encode(e.v)
require.NoError(t, err)
inlineMap := map[string]interface{}{}
err = toml.Unmarshal(buf.Bytes(), &inlineMap)
require.NoError(t, err)
require.Equal(t, defaultMap, inlineMap)
})
})
}
}
type flagsSetters []struct {
name string
f func(enc *toml.Encoder, flag bool)
}
var allFlags = flagsSetters{
{"arrays-multiline", (*toml.Encoder).SetArraysMultiline},
{"tables-inline", (*toml.Encoder).SetTablesInline},
{"indent-tables", (*toml.Encoder).SetIndentTables},
}
func setFlags(enc *toml.Encoder, flags int) {
for i := 0; i < len(allFlags); i++ {
enabled := flags&1 > 0
allFlags[i].f(enc, enabled)
}
}
func testWithAllFlags(t *testing.T, testfn func(t *testing.T, flags int)) {
t.Helper()
testWithFlags(t, 0, allFlags, testfn)
}
func testWithFlags(t *testing.T, flags int, setters flagsSetters, testfn func(t *testing.T, flags int)) {
t.Helper()
if len(setters) == 0 {
testfn(t, flags)
return
}
s := setters[0]
for _, enabled := range []bool{false, true} {
name := fmt.Sprintf("%s=%t", s.name, enabled)
newFlags := flags << 1
if enabled {
newFlags++
}
t.Run(name, func(t *testing.T) {
testWithFlags(t, newFlags, setters[1:], testfn)
}) })
} }
} }
@@ -255,6 +603,137 @@ func equalStringsIgnoreNewlines(t *testing.T, expected string, actual string) {
assert.Equal(t, strings.Trim(expected, cutset), strings.Trim(actual, cutset)) assert.Equal(t, strings.Trim(expected, cutset), strings.Trim(actual, cutset))
} }
//nolint:funlen
func TestMarshalIndentTables(t *testing.T) {
examples := []struct {
desc string
v interface{}
expected string
}{
{
desc: "one kv",
v: map[string]interface{}{
"foo": "bar",
},
expected: `foo = 'bar'`,
},
{
desc: "one level table",
v: map[string]map[string]string{
"foo": {
"one": "value1",
"two": "value2",
},
},
expected: `
[foo]
one = 'value1'
two = 'value2'
`,
},
{
desc: "two levels table",
v: map[string]interface{}{
"root": "value0",
"level1": map[string]interface{}{
"one": "value1",
"level2": map[string]interface{}{
"two": "value2",
},
},
},
expected: `
root = 'value0'
[level1]
one = 'value1'
[level1.level2]
two = 'value2'
`,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
var buf strings.Builder
enc := toml.NewEncoder(&buf)
enc.SetIndentTables(true)
err := enc.Encode(e.v)
require.NoError(t, err)
equalStringsIgnoreNewlines(t, e.expected, buf.String())
})
}
}
type customTextMarshaler struct {
value int64
}
func (c *customTextMarshaler) MarshalText() ([]byte, error) {
if c.value == 1 {
return nil, fmt.Errorf("cannot represent 1 because this is a silly test")
}
return []byte(fmt.Sprintf("::%d", c.value)), nil
}
func TestMarshalTextMarshaler_NoRoot(t *testing.T) {
c := customTextMarshaler{}
_, err := toml.Marshal(&c)
require.Error(t, err)
}
func TestMarshalTextMarshaler_Error(t *testing.T) {
m := map[string]interface{}{"a": &customTextMarshaler{value: 1}}
_, err := toml.Marshal(m)
require.Error(t, err)
}
func TestMarshalTextMarshaler_ErrorInline(t *testing.T) {
type s struct {
A map[string]interface{} `inline:"true"`
}
d := s{
A: map[string]interface{}{"a": &customTextMarshaler{value: 1}},
}
_, err := toml.Marshal(d)
require.Error(t, err)
}
func TestMarshalTextMarshaler(t *testing.T) {
m := map[string]interface{}{"a": &customTextMarshaler{value: 2}}
r, err := toml.Marshal(m)
require.NoError(t, err)
equalStringsIgnoreNewlines(t, "a = '::2'", string(r))
}
type brokenWriter struct{}
func (b *brokenWriter) Write([]byte) (int, error) {
return 0, fmt.Errorf("dead")
}
func TestEncodeToBrokenWriter(t *testing.T) {
w := brokenWriter{}
enc := toml.NewEncoder(&w)
err := enc.Encode(map[string]string{"hello": "world"})
require.Error(t, err)
}
func TestEncoderSetIndentSymbol(t *testing.T) {
var w strings.Builder
enc := toml.NewEncoder(&w)
enc.SetIndentTables(true)
enc.SetIndentSymbol(">>>")
err := enc.Encode(map[string]map[string]string{"parent": {"hello": "world"}})
require.NoError(t, err)
expected := `
[parent]
>>>hello = 'world'`
equalStringsIgnoreNewlines(t, expected, w.String())
}
func TestIssue436(t *testing.T) { func TestIssue436(t *testing.T) {
data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`) data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`)
@@ -273,3 +752,57 @@ c = 'd'
` `
equalStringsIgnoreNewlines(t, expected, buf.String()) equalStringsIgnoreNewlines(t, expected, buf.String())
} }
func TestIssue424(t *testing.T) {
type Message1 struct {
Text string
}
type Message2 struct {
Text string `multiline:"true"`
}
msg1 := Message1{"Hello\\World"}
msg2 := Message2{"Hello\\World"}
toml1, err := toml.Marshal(msg1)
require.NoError(t, err)
toml2, err := toml.Marshal(msg2)
require.NoError(t, err)
msg1parsed := Message1{}
err = toml.Unmarshal(toml1, &msg1parsed)
require.NoError(t, err)
require.Equal(t, msg1, msg1parsed)
msg2parsed := Message2{}
err = toml.Unmarshal(toml2, &msg2parsed)
require.NoError(t, err)
require.Equal(t, msg2, msg2parsed)
}
func ExampleMarshal() {
type MyConfig struct {
Version int
Name string
Tags []string
}
cfg := MyConfig{
Version: 2,
Name: "go-toml",
Tags: []string{"go", "toml"},
}
b, err := toml.Marshal(cfg)
if err != nil {
panic(err)
}
fmt.Println(string(b))
// Output:
// Version = 2
// Name = 'go-toml'
// Tags = ['go', 'toml']
}
+283 -177
View File
File diff suppressed because it is too large Load Diff
+14 -60
View File
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
//nolint:funlen
func TestParser_AST_Numbers(t *testing.T) { func TestParser_AST_Numbers(t *testing.T) {
examples := []struct { examples := []struct {
desc string desc string
@@ -132,6 +133,7 @@ func TestParser_AST_Numbers(t *testing.T) {
} }
for _, e := range examples { for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
p := parser{} p := parser{}
p.Reset([]byte(`A = ` + e.input)) p.Reset([]byte(`A = ` + e.input))
@@ -155,19 +157,16 @@ func TestParser_AST_Numbers(t *testing.T) {
} }
} }
type astRoot []astNode type (
type astNode struct { astNode struct {
Kind ast.Kind Kind ast.Kind
Data []byte Data []byte
Children []astNode Children []astNode
} }
)
func compareAST(t *testing.T, expected astRoot, actual *ast.Root) { func compareNode(t *testing.T, e astNode, n *ast.Node) {
it := actual.Iterator() t.Helper()
compareIterator(t, expected, it)
}
func compareNode(t *testing.T, e astNode, n ast.Node) {
require.Equal(t, e.Kind, n.Kind) require.Equal(t, e.Kind, n.Kind)
require.Equal(t, e.Data, n.Data) require.Equal(t, e.Data, n.Data)
@@ -175,6 +174,7 @@ func compareNode(t *testing.T, e astNode, n ast.Node) {
} }
func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) { func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) {
t.Helper()
idx := 0 idx := 0
for actual.Next() { for actual.Next() {
@@ -195,54 +195,7 @@ func compareIterator(t *testing.T, expected []astNode, actual ast.Iterator) {
} }
} }
func (r astRoot) toOrig() *ast.Root { //nolint:funlen
builder := &ast.Builder{}
var last ast.Reference
for i, n := range r {
ref := builder.Push(ast.Node{
Kind: n.Kind,
Data: n.Data,
})
if i > 0 {
builder.Chain(last, ref)
}
last = ref
if len(n.Children) > 0 {
c := childrenToOrig(builder, n.Children)
builder.AttachChild(ref, c)
}
}
return builder.Tree()
}
func childrenToOrig(b *ast.Builder, nodes []astNode) ast.Reference {
var first ast.Reference
var last ast.Reference
for i, n := range nodes {
ref := b.Push(ast.Node{
Kind: n.Kind,
Data: n.Data,
})
if i == 0 {
first = ref
} else {
b.Chain(last, ref)
}
last = ref
if len(n.Children) > 0 {
c := childrenToOrig(b, n.Children)
b.AttachChild(ref, c)
}
}
return first
}
func TestParser_AST(t *testing.T) { func TestParser_AST(t *testing.T) {
examples := []struct { examples := []struct {
desc string desc string
@@ -380,6 +333,7 @@ func TestParser_AST(t *testing.T) {
} }
for _, e := range examples { for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
p := parser{} p := parser{}
p.Reset([]byte(e.input)) p.Reset([]byte(e.input))
+77 -72
View File
@@ -1,36 +1,44 @@
package toml package toml
import "fmt" func scanFollows(b []byte, pattern string) bool {
n := len(pattern)
func scanFollows(pattern []byte) func(b []byte) bool { return len(b) >= n && string(b[:n]) == pattern
return func(b []byte) bool {
if len(b) < len(pattern) {
return false
}
for i, c := range pattern {
if b[i] != c {
return false
}
}
return true
}
} }
var scanFollowsMultilineBasicStringDelimiter = scanFollows([]byte{'"', '"', '"'}) func scanFollowsMultilineBasicStringDelimiter(b []byte) bool {
var scanFollowsMultilineLiteralStringDelimiter = scanFollows([]byte{'\'', '\'', '\''}) return scanFollows(b, `"""`)
var scanFollowsTrue = scanFollows([]byte{'t', 'r', 'u', 'e'}) }
var scanFollowsFalse = scanFollows([]byte{'f', 'a', 'l', 's', 'e'})
var scanFollowsInf = scanFollows([]byte{'i', 'n', 'f'})
var scanFollowsNan = scanFollows([]byte{'n', 'a', 'n'})
func scanUnquotedKey(b []byte) ([]byte, []byte, error) { func scanFollowsMultilineLiteralStringDelimiter(b []byte) bool {
//unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _ return scanFollows(b, `'''`)
}
func scanFollowsTrue(b []byte) bool {
return scanFollows(b, `true`)
}
func scanFollowsFalse(b []byte) bool {
return scanFollows(b, `false`)
}
func scanFollowsInf(b []byte) bool {
return scanFollows(b, `inf`)
}
func scanFollowsNan(b []byte) bool {
return scanFollows(b, `nan`)
}
func scanUnquotedKey(b []byte) ([]byte, []byte) {
// unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
for i := 0; i < len(b); i++ { for i := 0; i < len(b); i++ {
if !isUnquotedKeyChar(b[i]) { if !isUnquotedKeyChar(b[i]) {
return b[:i], b[i:], nil return b[:i], b[i:]
} }
} }
return b, b[len(b):], nil
return b, b[len(b):]
} }
func isUnquotedKeyChar(r byte) bool { func isUnquotedKeyChar(r byte) bool {
@@ -38,9 +46,9 @@ func isUnquotedKeyChar(r byte) bool {
} }
func scanLiteralString(b []byte) ([]byte, []byte, error) { func scanLiteralString(b []byte) ([]byte, []byte, error) {
//literal-string = apostrophe *literal-char apostrophe // literal-string = apostrophe *literal-char apostrophe
//apostrophe = %x27 ; ' apostrophe // apostrophe = %x27 ; ' apostrophe
//literal-char = %x09 / %x20-26 / %x28-7E / non-ascii // literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
for i := 1; i < len(b); i++ { for i := 1; i < len(b); i++ {
switch b[i] { switch b[i] {
case '\'': case '\'':
@@ -49,24 +57,22 @@ func scanLiteralString(b []byte) ([]byte, []byte, error) {
return nil, nil, newDecodeError(b[i:i+1], "literal strings cannot have new lines") return nil, nil, newDecodeError(b[i:i+1], "literal strings cannot have new lines")
} }
} }
return nil, nil, newDecodeError(b[len(b):], "unterminated literal string") return nil, nil, newDecodeError(b[len(b):], "unterminated literal string")
} }
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) { func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
//ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body // ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
//ml-literal-string-delim // ml-literal-string-delim
//ml-literal-string-delim = 3apostrophe // ml-literal-string-delim = 3apostrophe
//ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ] // ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
// //
//mll-content = mll-char / newline // mll-content = mll-char / newline
//mll-char = %x09 / %x20-26 / %x28-7E / non-ascii // mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
//mll-quotes = 1*2apostrophe // mll-quotes = 1*2apostrophe
for i := 3; i < len(b); i++ { for i := 3; i < len(b); i++ {
switch b[i] { if b[i] == '\'' && scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
case '\'': return b[:i+3], b[i+3:], nil
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
return b[:i+3], b[i+3:], nil
}
} }
} }
@@ -74,13 +80,16 @@ func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
} }
func scanWindowsNewline(b []byte) ([]byte, []byte, error) { func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
if len(b) < 2 { const lenCRLF = 2
return nil, nil, fmt.Errorf(`windows new line missing \n`) if len(b) < lenCRLF {
return nil, nil, newDecodeError(b, "windows new line expected")
} }
if b[1] != '\n' { if b[1] != '\n' {
return nil, nil, fmt.Errorf(`windows new line should be \r\n`) return nil, nil, newDecodeError(b, `windows new line should be \r\n`)
} }
return b[:2], b[2:], nil
return b[:lenCRLF], b[lenCRLF:], nil
} }
func scanWhitespace(b []byte) ([]byte, []byte) { func scanWhitespace(b []byte) ([]byte, []byte) {
@@ -92,34 +101,32 @@ func scanWhitespace(b []byte) ([]byte, []byte) {
return b[:i], b[i:] return b[:i], b[i:]
} }
} }
return b, b[len(b):] return b, b[len(b):]
} }
func scanComment(b []byte) ([]byte, []byte, error) { //nolint:unparam
//;; Comment func scanComment(b []byte) ([]byte, []byte) {
// comment-start-symbol = %x23 ; #
// non-ascii = %x80-D7FF / %xE000-10FFFF
// non-eol = %x09 / %x20-7F / non-ascii
// //
//comment-start-symbol = %x23 ; # // comment = comment-start-symbol *non-eol
//non-ascii = %x80-D7FF / %xE000-10FFFF
//non-eol = %x09 / %x20-7F / non-ascii
//
//comment = comment-start-symbol *non-eol
for i := 1; i < len(b); i++ { for i := 1; i < len(b); i++ {
switch b[i] { if b[i] == '\n' {
case '\n': return b[:i], b[i:]
return b[:i], b[i:], nil
} }
} }
return b, nil, nil
return b, nil
} }
// TODO perform validation on the string?
func scanBasicString(b []byte) ([]byte, []byte, error) { func scanBasicString(b []byte) ([]byte, []byte, error) {
//basic-string = quotation-mark *basic-char quotation-mark // basic-string = quotation-mark *basic-char quotation-mark
//quotation-mark = %x22 ; " // quotation-mark = %x22 ; "
//basic-char = basic-unescaped / escaped // basic-char = basic-unescaped / escaped
//basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii // basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
//escaped = escape escape-seq-char // escaped = escape escape-seq-char
for i := 1; i < len(b); i++ { for i := 1; i < len(b); i++ {
switch b[i] { switch b[i] {
case '"': case '"':
@@ -134,22 +141,20 @@ func scanBasicString(b []byte) ([]byte, []byte, error) {
} }
} }
return nil, nil, fmt.Errorf(`basic string not terminated by "`) return nil, nil, newDecodeError(b[len(b):], `basic string not terminated by "`)
} }
// TODO perform validation on the string?
func scanMultilineBasicString(b []byte) ([]byte, []byte, error) { func scanMultilineBasicString(b []byte) ([]byte, []byte, error) {
//ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body // ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
//ml-basic-string-delim // ml-basic-string-delim
//ml-basic-string-delim = 3quotation-mark // ml-basic-string-delim = 3quotation-mark
//ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ] // ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
// //
//mlb-content = mlb-char / newline / mlb-escaped-nl // mlb-content = mlb-char / newline / mlb-escaped-nl
//mlb-char = mlb-unescaped / escaped // mlb-char = mlb-unescaped / escaped
//mlb-quotes = 1*2quotation-mark // mlb-quotes = 1*2quotation-mark
//mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii // mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
//mlb-escaped-nl = escape ws newline *( wschar / newline ) // mlb-escaped-nl = escape ws newline *( wschar / newline )
for i := 3; i < len(b); i++ { for i := 3; i < len(b); i++ {
switch b[i] { switch b[i] {
case '"': case '"':
+107
View File
@@ -0,0 +1,107 @@
package toml
import (
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/internal/danger"
"github.com/pelletier/go-toml/v2/internal/tracker"
)
type strict struct {
Enabled bool
// Tracks the current key being processed.
key tracker.KeyTracker
missing []decodeError
}
func (s *strict) EnterTable(node *ast.Node) {
if !s.Enabled {
return
}
s.key.UpdateTable(node)
}
func (s *strict) EnterArrayTable(node *ast.Node) {
if !s.Enabled {
return
}
s.key.UpdateArrayTable(node)
}
func (s *strict) EnterKeyValue(node *ast.Node) {
if !s.Enabled {
return
}
s.key.Push(node)
}
func (s *strict) ExitKeyValue(node *ast.Node) {
if !s.Enabled {
return
}
s.key.Pop(node)
}
func (s *strict) MissingTable(node *ast.Node) {
if !s.Enabled {
return
}
s.missing = append(s.missing, decodeError{
highlight: keyLocation(node),
message: "missing table",
key: s.key.Key(),
})
}
func (s *strict) MissingField(node *ast.Node) {
if !s.Enabled {
return
}
s.missing = append(s.missing, decodeError{
highlight: keyLocation(node),
message: "missing field",
key: s.key.Key(),
})
}
func (s *strict) Error(doc []byte) error {
if !s.Enabled || len(s.missing) == 0 {
return nil
}
err := &StrictMissingError{
Errors: make([]DecodeError, 0, len(s.missing)),
}
for _, derr := range s.missing {
derr := derr
err.Errors = append(err.Errors, *wrapDecodeError(doc, &derr))
}
return err
}
func keyLocation(node *ast.Node) []byte {
k := node.Key()
hasOne := k.Next()
if !hasOne {
panic("should not be called with empty key")
}
start := k.Node().Data
end := k.Node().Data
for k.Next() {
end = k.Node().Data
}
return danger.BytesRange(start, end)
}
-554
View File
@@ -1,554 +0,0 @@
package toml
import (
"fmt"
"math"
"reflect"
"strings"
"sync"
)
type target interface {
// Dereferences the target.
get() reflect.Value
// Store a string at the target.
setString(v string) error
// Store a boolean at the target
setBool(v bool) error
// Store an int64 at the target
setInt64(v int64) error
// Store a float64 at the target
setFloat64(v float64) error
// Stores any value at the target
set(v reflect.Value) error
}
// valueTarget just contains a reflect.Value that can be set.
// It is used for struct fields.
type valueTarget reflect.Value
func (t valueTarget) get() reflect.Value {
return reflect.Value(t)
}
func (t valueTarget) set(v reflect.Value) error {
reflect.Value(t).Set(v)
return nil
}
func (t valueTarget) setString(v string) error {
t.get().SetString(v)
return nil
}
func (t valueTarget) setBool(v bool) error {
t.get().SetBool(v)
return nil
}
func (t valueTarget) setInt64(v int64) error {
t.get().SetInt(v)
return nil
}
func (t valueTarget) setFloat64(v float64) error {
t.get().SetFloat(v)
return nil
}
// interfaceTarget wraps an other target to dereference on get.
type interfaceTarget struct {
x target
}
func (t interfaceTarget) get() reflect.Value {
return t.x.get().Elem()
}
func (t interfaceTarget) set(v reflect.Value) error {
return t.x.set(v)
}
func (t interfaceTarget) setString(v string) error {
return t.x.setString(v)
}
func (t interfaceTarget) setBool(v bool) error {
return t.x.setBool(v)
}
func (t interfaceTarget) setInt64(v int64) error {
return t.x.setInt64(v)
}
func (t interfaceTarget) setFloat64(v float64) error {
return t.x.setFloat64(v)
}
// mapTarget targets a specific key of a map.
type mapTarget struct {
v reflect.Value
k reflect.Value
}
func (t mapTarget) get() reflect.Value {
return t.v.MapIndex(t.k)
}
func (t mapTarget) set(v reflect.Value) error {
t.v.SetMapIndex(t.k, v)
return nil
}
func (t mapTarget) setString(v string) error {
return t.set(reflect.ValueOf(v))
}
func (t mapTarget) setBool(v bool) error {
return t.set(reflect.ValueOf(v))
}
func (t mapTarget) setInt64(v int64) error {
return t.set(reflect.ValueOf(v))
}
func (t mapTarget) setFloat64(v float64) error {
return t.set(reflect.ValueOf(v))
}
// makes sure that the value pointed at by t is indexable (Slice, Array), or
// dereferences to an indexable (Ptr, Interface).
func ensureValueIndexable(t target) error {
f := t.get()
switch f.Type().Kind() {
case reflect.Slice:
if f.IsNil() {
return t.set(reflect.MakeSlice(f.Type(), 0, 0))
}
case reflect.Interface:
if f.IsNil() || f.Elem().Type() != sliceInterfaceType {
return t.set(reflect.MakeSlice(sliceInterfaceType, 0, 0))
}
if f.Elem().Type().Kind() != reflect.Slice {
return fmt.Errorf("interface is pointing to a %s, not a slice", f.Kind())
}
case reflect.Ptr:
if f.IsNil() {
ptr := reflect.New(f.Type().Elem())
err := t.set(ptr)
if err != nil {
return err
}
f = t.get()
}
return ensureValueIndexable(valueTarget(f.Elem()))
case reflect.Array:
// arrays are always initialized.
default:
return fmt.Errorf("cannot initialize a slice in %s", f.Kind())
}
return nil
}
var sliceInterfaceType = reflect.TypeOf([]interface{}{})
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
func ensureMapIfInterface(x target) {
v := x.get()
if v.Kind() == reflect.Interface && v.IsNil() {
newElement := reflect.MakeMap(mapStringInterfaceType)
x.set(newElement)
}
}
func setString(t target, v string) error {
f := t.get()
switch f.Kind() {
case reflect.String:
return t.setString(v)
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign string to a %s", f.Kind())
}
}
func setBool(t target, v bool) error {
f := t.get()
switch f.Kind() {
case reflect.Bool:
return t.setBool(v)
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign bool to a %s", f.String())
}
}
const maxInt = int64(^uint(0) >> 1)
const minInt = -maxInt - 1
func setInt64(t target, v int64) error {
f := t.get()
switch f.Kind() {
case reflect.Int64:
return t.setInt64(v)
case reflect.Int32:
if v < math.MinInt32 || v > math.MaxInt32 {
return fmt.Errorf("integer %d does not fit in an int32", v)
}
return t.set(reflect.ValueOf(int32(v)))
case reflect.Int16:
if v < math.MinInt16 || v > math.MaxInt16 {
return fmt.Errorf("integer %d does not fit in an int16", v)
}
return t.set(reflect.ValueOf(int16(v)))
case reflect.Int8:
if v < math.MinInt8 || v > math.MaxInt8 {
return fmt.Errorf("integer %d does not fit in an int8", v)
}
return t.set(reflect.ValueOf(int8(v)))
case reflect.Int:
if v < minInt || v > maxInt {
return fmt.Errorf("integer %d does not fit in an int", v)
}
return t.set(reflect.ValueOf(int(v)))
case reflect.Uint64:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint64", v)
}
return t.set(reflect.ValueOf(uint64(v)))
case reflect.Uint32:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint32", v)
}
if v > math.MaxUint32 {
return fmt.Errorf("integer %d cannot be stored in an uint32", v)
}
return t.set(reflect.ValueOf(uint32(v)))
case reflect.Uint16:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint16", v)
}
if v > math.MaxUint16 {
return fmt.Errorf("integer %d cannot be stored in an uint16", v)
}
return t.set(reflect.ValueOf(uint16(v)))
case reflect.Uint8:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint8", v)
}
if v > math.MaxUint8 {
return fmt.Errorf("integer %d cannot be stored in an uint8", v)
}
return t.set(reflect.ValueOf(uint8(v)))
case reflect.Uint:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint", v)
}
return t.set(reflect.ValueOf(uint(v)))
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign int64 to a %s", f.String())
}
}
func setFloat64(t target, v float64) error {
f := t.get()
switch f.Kind() {
case reflect.Float64:
return t.setFloat64(v)
case reflect.Float32:
if v > math.MaxFloat32 {
return fmt.Errorf("float %f cannot be stored in a float32", v)
}
return t.set(reflect.ValueOf(float32(v)))
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign float64 to a %s", f.String())
}
}
// Returns the element at idx of the value pointed at by target, or an error if
// t does not point to an indexable.
// If the target points to an Array and idx is out of bounds, it returns
// (nil, nil) as this is not a fatal error (the unmarshaler will skip).
func elementAt(t target, idx int) (target, error) {
f := t.get()
switch f.Kind() {
case reflect.Slice:
// TODO: use the idx function argument and avoid alloc if possible.
idx := f.Len()
err := t.set(reflect.Append(f, reflect.New(f.Type().Elem()).Elem()))
if err != nil {
return nil, err
}
return valueTarget(t.get().Index(idx)), nil
case reflect.Array:
if idx >= f.Len() {
return nil, nil
}
return valueTarget(f.Index(idx)), nil
case reflect.Interface:
if f.IsNil() {
panic("interface should have been initialized")
}
ifaceElem := f.Elem()
if ifaceElem.Kind() != reflect.Slice {
return nil, fmt.Errorf("cannot elementAt on a %s", f.Kind())
}
idx := ifaceElem.Len()
newElem := reflect.New(ifaceElem.Type().Elem()).Elem()
newSlice := reflect.Append(ifaceElem, newElem)
err := t.set(newSlice)
if err != nil {
return nil, err
}
return valueTarget(t.get().Elem().Index(idx)), nil
case reflect.Ptr:
return elementAt(valueTarget(f.Elem()), idx)
default:
return nil, fmt.Errorf("cannot elementAt on a %s", f.Kind())
}
}
func (d *decoder) scopeTableTarget(append bool, t target, name string) (target, bool, error) {
x := t.get()
switch x.Kind() {
// Kinds that need to recurse
case reflect.Interface:
t, err := scopeInterface(append, t)
if err != nil {
return t, false, err
}
return d.scopeTableTarget(append, t, name)
case reflect.Ptr:
t, err := scopePtr(t)
if err != nil {
return t, false, err
}
return d.scopeTableTarget(append, t, name)
case reflect.Slice:
t, err := scopeSlice(append, t)
if err != nil {
return t, false, err
}
append = false
return d.scopeTableTarget(append, t, name)
case reflect.Array:
t, err := d.scopeArray(append, t)
if err != nil {
return t, false, err
}
append = false
return d.scopeTableTarget(append, t, name)
// Terminal kinds
case reflect.Struct:
return scopeStruct(x, name)
case reflect.Map:
if x.IsNil() {
t.set(reflect.MakeMap(x.Type()))
x = t.get()
}
return scopeMap(x, name)
default:
panic(fmt.Errorf("can't scope on a %s", x.Kind()))
}
}
func scopeInterface(append bool, t target) (target, error) {
err := initInterface(append, t)
if err != nil {
return t, err
}
return interfaceTarget{t}, nil
}
func scopePtr(t target) (target, error) {
err := initPtr(t)
if err != nil {
return t, err
}
return valueTarget(t.get().Elem()), nil
}
func initPtr(t target) error {
x := t.get()
if !x.IsNil() {
return nil
}
return t.set(reflect.New(x.Type().Elem()))
}
// initInterface makes sure that the interface pointed at by the target is not
// nil.
// Returns the target to the initialized value of the target.
func initInterface(append bool, t target) error {
x := t.get()
if x.Kind() != reflect.Interface {
panic("this should only be called on interfaces")
}
if !x.IsNil() && (x.Elem().Type() == sliceInterfaceType || x.Elem().Type() == mapStringInterfaceType) {
return nil
}
var newElement reflect.Value
if append {
newElement = reflect.MakeSlice(sliceInterfaceType, 0, 0)
} else {
newElement = reflect.MakeMap(mapStringInterfaceType)
}
err := t.set(newElement)
if err != nil {
return err
}
return nil
}
func scopeSlice(append bool, t target) (target, error) {
v := t.get()
if append {
newElem := reflect.New(v.Type().Elem())
newSlice := reflect.Append(v, newElem.Elem())
err := t.set(newSlice)
if err != nil {
return t, err
}
v = t.get()
}
return valueTarget(v.Index(v.Len() - 1)), nil
}
func (d *decoder) scopeArray(append bool, t target) (target, error) {
v := t.get()
idx := d.arrayIndex(append, v)
if idx >= v.Len() {
return nil, fmt.Errorf("not enough space in the array")
}
return valueTarget(v.Index(idx)), nil
}
func scopeMap(v reflect.Value, name string) (target, bool, error) {
k := reflect.ValueOf(name)
keyType := v.Type().Key()
if !k.Type().AssignableTo(keyType) {
if !k.Type().ConvertibleTo(keyType) {
return nil, false, fmt.Errorf("cannot convert string into map key type %s", keyType)
}
k = k.Convert(keyType)
}
if !v.MapIndex(k).IsValid() {
newElem := reflect.New(v.Type().Elem())
v.SetMapIndex(k, newElem.Elem())
}
return mapTarget{
v: v,
k: k,
}, true, nil
}
type fieldPathsMap = map[string][]int
type fieldPathsCache struct {
m map[reflect.Type]fieldPathsMap
l sync.RWMutex
}
func (c *fieldPathsCache) get(t reflect.Type) (fieldPathsMap, bool) {
c.l.RLock()
paths, ok := c.m[t]
c.l.RUnlock()
return paths, ok
}
func (c *fieldPathsCache) set(t reflect.Type, m fieldPathsMap) {
c.l.Lock()
c.m[t] = m
c.l.Unlock()
}
var globalFieldPathsCache = fieldPathsCache{
m: map[reflect.Type]fieldPathsMap{},
l: sync.RWMutex{},
}
func scopeStruct(v reflect.Value, name string) (target, bool, error) {
// TODO: cache this, and reduce allocations
fieldPaths, ok := globalFieldPathsCache.get(v.Type())
if !ok {
fieldPaths = map[string][]int{}
path := make([]int, 0, 16)
var walk func(reflect.Value)
walk = func(v reflect.Value) {
t := v.Type()
for i := 0; i < t.NumField(); i++ {
l := len(path)
path = append(path, i)
f := t.Field(i)
if f.PkgPath != "" {
// only consider exported fields
} else if f.Anonymous {
walk(v.Field(i))
} else {
fieldName, ok := f.Tag.Lookup("toml")
if !ok {
fieldName = f.Name
}
pathCopy := make([]int, len(path))
copy(pathCopy, path)
fieldPaths[fieldName] = pathCopy
// extra copy for the case-insensitive match
fieldPaths[strings.ToLower(fieldName)] = pathCopy
}
path = path[:l]
}
}
walk(v)
globalFieldPathsCache.set(v.Type(), fieldPaths)
}
path, ok := fieldPaths[name]
if !ok {
path, ok = fieldPaths[strings.ToLower(name)]
}
if !ok {
return nil, false, nil
}
return valueTarget(v.FieldByIndex(path)), true, nil
}
-184
View File
@@ -1,184 +0,0 @@
package toml
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStructTarget_Ensure(t *testing.T) {
examples := []struct {
desc string
input reflect.Value
name string
test func(v reflect.Value, err error)
}{
{
desc: "handle a nil slice of string",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.False(t, v.IsNil())
},
},
{
desc: "handle an existing slice of string",
input: reflect.ValueOf(&struct{ A []string }{A: []string{"foo"}}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
require.False(t, v.IsNil())
s := v.Interface().([]string)
assert.Equal(t, []string{"foo"}, s)
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d := decoder{}
target, _, err := d.scopeTableTarget(false, valueTarget(e.input), e.name)
require.NoError(t, err)
err = ensureValueIndexable(target)
v := target.get()
e.test(v, err)
})
}
}
func TestStructTarget_SetString(t *testing.T) {
str := "value"
examples := []struct {
desc string
input reflect.Value
name string
test func(v reflect.Value, err error)
}{
{
desc: "sets a string",
input: reflect.ValueOf(&struct{ A string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.Equal(t, str, v.String())
},
},
{
desc: "fails on a float",
input: reflect.ValueOf(&struct{ A float64 }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.Error(t, err)
},
},
{
desc: "fails on a slice",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.Error(t, err)
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d := decoder{}
target, _, err := d.scopeTableTarget(false, valueTarget(e.input), e.name)
require.NoError(t, err)
err = setString(target, str)
v := target.get()
e.test(v, err)
})
}
}
func TestPushNew(t *testing.T) {
t.Run("slice of strings", func(t *testing.T) {
type Doc struct {
A []string
}
d := Doc{}
dec := decoder{}
x, _, err := dec.scopeTableTarget(false, valueTarget(reflect.ValueOf(&d).Elem()), "A")
require.NoError(t, err)
n, err := elementAt(x, 0)
require.NoError(t, err)
require.NoError(t, n.setString("hello"))
require.Equal(t, []string{"hello"}, d.A)
n, err = elementAt(x, 1)
require.NoError(t, err)
require.NoError(t, n.setString("world"))
require.Equal(t, []string{"hello", "world"}, d.A)
})
t.Run("slice of interfaces", func(t *testing.T) {
type Doc struct {
A []interface{}
}
d := Doc{}
dec := decoder{}
x, _, err := dec.scopeTableTarget(false, valueTarget(reflect.ValueOf(&d).Elem()), "A")
require.NoError(t, err)
n, err := elementAt(x, 0)
require.NoError(t, err)
require.NoError(t, setString(n, "hello"))
require.Equal(t, []interface{}{"hello"}, d.A)
n, err = elementAt(x, 1)
require.NoError(t, err)
require.NoError(t, setString(n, "world"))
require.Equal(t, []interface{}{"hello", "world"}, d.A)
})
}
func TestScope_Struct(t *testing.T) {
examples := []struct {
desc string
input reflect.Value
name string
err bool
found bool
idx []int
}{
{
desc: "simple field",
input: reflect.ValueOf(&struct{ A string }{}).Elem(),
name: "A",
idx: []int{0},
found: true,
},
{
desc: "fails not-exported field",
input: reflect.ValueOf(&struct{ a string }{}).Elem(),
name: "a",
err: false,
found: false,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
dec := decoder{}
x, found, err := dec.scopeTableTarget(false, valueTarget(e.input), e.name)
assert.Equal(t, e.found, found)
if e.err {
assert.Error(t, err)
}
if found {
x2, ok := x.(valueTarget)
require.True(t, ok)
x2.get()
}
})
}
}
+1
View File
@@ -59,6 +59,7 @@ val = string / boolean / array / inline-table / date-time / float / integer
;; String ;; String
string = ml-basic-string / basic-string / ml-literal-string / literal-string string = ml-basic-string / basic-string / ml-literal-string / literal-string
;; Basic String ;; Basic String
basic-string = quotation-mark *basic-char quotation-mark basic-string = quotation-mark *basic-char quotation-mark
+27 -13
View File
@@ -30,6 +30,7 @@ func testgenValid(t *testing.T, input string, jsonRef string) {
t.Logf("Input TOML:\n%s", input) t.Logf("Input TOML:\n%s", input)
doc := map[string]interface{}{} doc := map[string]interface{}{}
err := toml.Unmarshal([]byte(input), &doc) err := toml.Unmarshal([]byte(input), &doc)
if err != nil { if err != nil {
t.Fatalf("failed parsing toml: %s", err) t.Fatalf("failed parsing toml: %s", err)
@@ -49,25 +50,23 @@ func testgenValid(t *testing.T, input string, jsonRef string) {
require.Equal(t, refDoc, doc2) require.Equal(t, refDoc, doc2)
} }
type testGenDescNode struct {
Type string
Value interface{}
}
func testgenBuildRefDoc(jsonRef string) map[string]interface{} { func testgenBuildRefDoc(jsonRef string) map[string]interface{} {
descTree := map[string]interface{}{} descTree := map[string]interface{}{}
err := json.Unmarshal([]byte(jsonRef), &descTree) err := json.Unmarshal([]byte(jsonRef), &descTree)
if err != nil { if err != nil {
panic(fmt.Errorf("reference doc should be valid JSON: %s", err)) panic(fmt.Sprintf("reference doc should be valid JSON: %s", err))
} }
doc := testGenTranslateDesc(descTree) doc := testGenTranslateDesc(descTree)
if doc == nil { if doc == nil {
return map[string]interface{}{} return map[string]interface{}{}
} }
return doc.(map[string]interface{}) return doc.(map[string]interface{})
} }
//nolint:funlen,gocognit,cyclop
func testGenTranslateDesc(input interface{}) interface{} { func testGenTranslateDesc(input interface{}) interface{} {
a, ok := input.([]interface{}) a, ok := input.([]interface{})
if ok { if ok {
@@ -75,48 +74,61 @@ func testGenTranslateDesc(input interface{}) interface{} {
for i, v := range a { for i, v := range a {
xs[i] = testGenTranslateDesc(v) xs[i] = testGenTranslateDesc(v)
} }
return xs return xs
} }
d := input.(map[string]interface{}) d, ok := input.(map[string]interface{})
if !ok {
panic(fmt.Sprintf("input should be valid map[string]: %v", input))
}
var dtype string var (
var dvalue interface{} dtype string
dvalue interface{}
)
//nolint:nestif
if len(d) == 2 { if len(d) == 2 {
dtypeiface, ok := d["type"] dtypeiface, ok := d["type"]
if ok { if ok {
dvalue, ok = d["value"] dvalue, ok = d["value"]
if ok { if ok {
dtype = dtypeiface.(string) dtype = dtypeiface.(string)
switch dtype { switch dtype {
case "string": case "string":
return dvalue.(string) return dvalue.(string)
case "float": case "float":
v, err := strconv.ParseFloat(dvalue.(string), 64) v, err := strconv.ParseFloat(dvalue.(string), 64)
if err != nil { if err != nil {
panic(fmt.Errorf("invalid float '%s': %s", dvalue, err)) panic(fmt.Sprintf("invalid float '%s': %s", dvalue, err))
} }
return v return v
case "integer": case "integer":
v, err := strconv.ParseInt(dvalue.(string), 10, 64) v, err := strconv.ParseInt(dvalue.(string), 10, 64)
if err != nil { if err != nil {
panic(fmt.Errorf("invalid int '%s': %s", dvalue, err)) panic(fmt.Sprintf("invalid int '%s': %s", dvalue, err))
} }
return v return v
case "bool": case "bool":
return dvalue.(string) == "true" return dvalue.(string) == "true"
case "datetime": case "datetime":
dt, err := time.Parse("2006-01-02T15:04:05Z", dvalue.(string)) dt, err := time.Parse("2006-01-02T15:04:05Z", dvalue.(string))
if err != nil { if err != nil {
panic(fmt.Errorf("invalid datetime '%s': %s", dvalue, err)) panic(fmt.Sprintf("invalid datetime '%s': %s", dvalue, err))
} }
return dt return dt
case "array": case "array":
if dvalue == nil { if dvalue == nil {
return nil return nil
} }
a := dvalue.([]interface{}) a := dvalue.([]interface{})
xs := make([]interface{}, len(a)) xs := make([]interface{}, len(a))
for i, v := range a { for i, v := range a {
@@ -125,7 +137,8 @@ func testGenTranslateDesc(input interface{}) interface{} {
return xs return xs
} }
panic(fmt.Errorf("unknown type: %s", dtype))
panic(fmt.Sprintf("unknown type: %s", dtype))
} }
} }
} }
@@ -134,5 +147,6 @@ func testGenTranslateDesc(input interface{}) interface{} {
for k, v := range d { for k, v := range d {
dest[k] = testGenTranslateDesc(v) dest[k] = testGenTranslateDesc(v)
} }
return dest return dest
} }
+75 -1
View File
@@ -6,26 +6,31 @@ import (
) )
func TestInvalidDatetimeMalformedNoLeads(t *testing.T) { func TestInvalidDatetimeMalformedNoLeads(t *testing.T) {
input := `no-leads = 1987-7-05T17:45:00Z` input := `no-leads = 1987-7-05T17:45:00Z`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidDatetimeMalformedNoSecs(t *testing.T) { func TestInvalidDatetimeMalformedNoSecs(t *testing.T) {
input := `no-secs = 1987-07-05T17:45Z` input := `no-secs = 1987-07-05T17:45Z`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidDatetimeMalformedNoT(t *testing.T) { func TestInvalidDatetimeMalformedNoT(t *testing.T) {
input := `no-t = 1987-07-0517:45:00Z` input := `no-t = 1987-07-0517:45:00Z`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidDatetimeMalformedWithMilli(t *testing.T) { func TestInvalidDatetimeMalformedWithMilli(t *testing.T) {
input := `with-milli = 1987-07-5T17:45:00.12Z` input := `with-milli = 1987-07-5T17:45:00.12Z`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidDuplicateKeyTable(t *testing.T) { func TestInvalidDuplicateKeyTable(t *testing.T) {
input := `[fruit] input := `[fruit]
type = "apple" type = "apple"
@@ -35,71 +40,84 @@ apple = "yes"`
} }
func TestInvalidDuplicateKeys(t *testing.T) { func TestInvalidDuplicateKeys(t *testing.T) {
input := `dupe = false input := `dupe = false
dupe = true` dupe = true`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidDuplicateTables(t *testing.T) { func TestInvalidDuplicateTables(t *testing.T) {
input := `[a] input := `[a]
[a]` [a]`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidEmptyImplicitTable(t *testing.T) { func TestInvalidEmptyImplicitTable(t *testing.T) {
input := `[naughty..naughty]` input := `[naughty..naughty]`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidEmptyTable(t *testing.T) { func TestInvalidEmptyTable(t *testing.T) {
input := `[]` input := `[]`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidFloatNoLeadingZero(t *testing.T) { func TestInvalidFloatNoLeadingZero(t *testing.T) {
input := `answer = .12345 input := `answer = .12345
neganswer = -.12345` neganswer = -.12345`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidFloatNoTrailingDigits(t *testing.T) { func TestInvalidFloatNoTrailingDigits(t *testing.T) {
input := `answer = 1. input := `answer = 1.
neganswer = -1.` neganswer = -1.`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidKeyEmpty(t *testing.T) { func TestInvalidKeyEmpty(t *testing.T) {
input := ` = 1` input := ` = 1`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidKeyHash(t *testing.T) { func TestInvalidKeyHash(t *testing.T) {
input := `a# = 1` input := `a# = 1`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidKeyNewline(t *testing.T) { func TestInvalidKeyNewline(t *testing.T) {
input := `a input := `a
= 1` = 1`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidKeyOpenBracket(t *testing.T) { func TestInvalidKeyOpenBracket(t *testing.T) {
input := `[abc = 1` input := `[abc = 1`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidKeySingleOpenBracket(t *testing.T) { func TestInvalidKeySingleOpenBracket(t *testing.T) {
input := `[` input := `[`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidKeySpace(t *testing.T) { func TestInvalidKeySpace(t *testing.T) {
input := `a b = 1` input := `a b = 1`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidKeyStartBracket(t *testing.T) { func TestInvalidKeyStartBracket(t *testing.T) {
input := `[a] input := `[a]
[xyz = 5 [xyz = 5
[b]` [b]`
@@ -107,31 +125,37 @@ func TestInvalidKeyStartBracket(t *testing.T) {
} }
func TestInvalidKeyTwoEquals(t *testing.T) { func TestInvalidKeyTwoEquals(t *testing.T) {
input := `key= = 1` input := `key= = 1`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidStringBadByteEscape(t *testing.T) { func TestInvalidStringBadByteEscape(t *testing.T) {
input := `naughty = "\xAg"` input := `naughty = "\xAg"`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidStringBadEscape(t *testing.T) { func TestInvalidStringBadEscape(t *testing.T) {
input := `invalid-escape = "This string has a bad \a escape character."` input := `invalid-escape = "This string has a bad \a escape character."`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidStringByteEscapes(t *testing.T) { func TestInvalidStringByteEscapes(t *testing.T) {
input := `answer = "\x33"` input := `answer = "\x33"`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidStringNoClose(t *testing.T) { func TestInvalidStringNoClose(t *testing.T) {
input := `no-ending-quote = "One time, at band camp` input := `no-ending-quote = "One time, at band camp`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTableArrayImplicit(t *testing.T) { func TestInvalidTableArrayImplicit(t *testing.T) {
input := "# This test is a bit tricky. It should fail because the first use of\n" + input := "# This test is a bit tricky. It should fail because the first use of\n" +
"# `[[albums.songs]]` without first declaring `albums` implies that `albums`\n" + "# `[[albums.songs]]` without first declaring `albums` implies that `albums`\n" +
"# must be a table. The alternative would be quite weird. Namely, it wouldn't\n" + "# must be a table. The alternative would be quite weird. Namely, it wouldn't\n" +
@@ -150,46 +174,54 @@ func TestInvalidTableArrayImplicit(t *testing.T) {
} }
func TestInvalidTableArrayMalformedBracket(t *testing.T) { func TestInvalidTableArrayMalformedBracket(t *testing.T) {
input := `[[albums] input := `[[albums]
name = "Born to Run"` name = "Born to Run"`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTableArrayMalformedEmpty(t *testing.T) { func TestInvalidTableArrayMalformedEmpty(t *testing.T) {
input := `[[]] input := `[[]]
name = "Born to Run"` name = "Born to Run"`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTableEmpty(t *testing.T) { func TestInvalidTableEmpty(t *testing.T) {
input := `[]` input := `[]`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTableNestedBracketsClose(t *testing.T) { func TestInvalidTableNestedBracketsClose(t *testing.T) {
input := `[a]b] input := `[a]b]
zyx = 42` zyx = 42`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTableNestedBracketsOpen(t *testing.T) { func TestInvalidTableNestedBracketsOpen(t *testing.T) {
input := `[a[b] input := `[a[b]
zyx = 42` zyx = 42`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTableWhitespace(t *testing.T) { func TestInvalidTableWhitespace(t *testing.T) {
input := `[invalid key]` input := `[invalid key]`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTableWithPound(t *testing.T) { func TestInvalidTableWithPound(t *testing.T) {
input := `[key#group] input := `[key#group]
answer = 42` answer = 42`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTextAfterArrayEntries(t *testing.T) { func TestInvalidTextAfterArrayEntries(t *testing.T) {
input := `array = [ input := `array = [
"Is there life after an array separator?", No "Is there life after an array separator?", No
"Entry" "Entry"
@@ -198,21 +230,25 @@ func TestInvalidTextAfterArrayEntries(t *testing.T) {
} }
func TestInvalidTextAfterInteger(t *testing.T) { func TestInvalidTextAfterInteger(t *testing.T) {
input := `answer = 42 the ultimate answer?` input := `answer = 42 the ultimate answer?`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTextAfterString(t *testing.T) { func TestInvalidTextAfterString(t *testing.T) {
input := `string = "Is there life after strings?" No.` input := `string = "Is there life after strings?" No.`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTextAfterTable(t *testing.T) { func TestInvalidTextAfterTable(t *testing.T) {
input := `[error] this shouldn't be here` input := `[error] this shouldn't be here`
testgenInvalid(t, input) testgenInvalid(t, input)
} }
func TestInvalidTextBeforeArraySeparator(t *testing.T) { func TestInvalidTextBeforeArraySeparator(t *testing.T) {
input := `array = [ input := `array = [
"Is there life before an array separator?" No, "Is there life before an array separator?" No,
"Entry" "Entry"
@@ -221,6 +257,7 @@ func TestInvalidTextBeforeArraySeparator(t *testing.T) {
} }
func TestInvalidTextInArray(t *testing.T) { func TestInvalidTextInArray(t *testing.T) {
input := `array = [ input := `array = [
"Entry 1", "Entry 1",
I don't belong, I don't belong,
@@ -230,6 +267,7 @@ func TestInvalidTextInArray(t *testing.T) {
} }
func TestValidArrayEmpty(t *testing.T) { func TestValidArrayEmpty(t *testing.T) {
input := `thevoid = [[[[[]]]]]` input := `thevoid = [[[[[]]]]]`
jsonRef := `{ jsonRef := `{
"thevoid": { "type": "array", "value": [ "thevoid": { "type": "array", "value": [
@@ -246,6 +284,7 @@ func TestValidArrayEmpty(t *testing.T) {
} }
func TestValidArrayNospaces(t *testing.T) { func TestValidArrayNospaces(t *testing.T) {
input := `ints = [1,2,3]` input := `ints = [1,2,3]`
jsonRef := `{ jsonRef := `{
"ints": { "ints": {
@@ -261,6 +300,7 @@ func TestValidArrayNospaces(t *testing.T) {
} }
func TestValidArraysHetergeneous(t *testing.T) { func TestValidArraysHetergeneous(t *testing.T) {
input := `mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]` input := `mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]`
jsonRef := `{ jsonRef := `{
"mixed": { "mixed": {
@@ -285,6 +325,7 @@ func TestValidArraysHetergeneous(t *testing.T) {
} }
func TestValidArraysNested(t *testing.T) { func TestValidArraysNested(t *testing.T) {
input := `nest = [["a"], ["b"]]` input := `nest = [["a"], ["b"]]`
jsonRef := `{ jsonRef := `{
"nest": { "nest": {
@@ -303,6 +344,7 @@ func TestValidArraysNested(t *testing.T) {
} }
func TestValidArrays(t *testing.T) { func TestValidArrays(t *testing.T) {
input := `ints = [1, 2, 3] input := `ints = [1, 2, 3]
floats = [1.1, 2.1, 3.1] floats = [1.1, 2.1, 3.1]
strings = ["a", "b", "c"] strings = ["a", "b", "c"]
@@ -349,6 +391,7 @@ dates = [
} }
func TestValidBool(t *testing.T) { func TestValidBool(t *testing.T) {
input := `t = true input := `t = true
f = false` f = false`
jsonRef := `{ jsonRef := `{
@@ -359,6 +402,7 @@ f = false`
} }
func TestValidCommentsEverywhere(t *testing.T) { func TestValidCommentsEverywhere(t *testing.T) {
input := `# Top comment. input := `# Top comment.
# Top comment. # Top comment.
# Top comment. # Top comment.
@@ -368,7 +412,7 @@ func TestValidCommentsEverywhere(t *testing.T) {
[group] # Comment [group] # Comment
answer = 42 # Comment answer = 42 # Comment
# no-extraneous-keys-please = 999 # no-extraneous-keys-please = 999
# Inbetween comment. # In between comment.
more = [ # Comment more = [ # Comment
# What about multiple # comments? # What about multiple # comments?
# Can you handle it? # Can you handle it?
@@ -399,6 +443,7 @@ more = [ # Comment
} }
func TestValidDatetime(t *testing.T) { func TestValidDatetime(t *testing.T) {
input := `bestdayever = 1987-07-05T17:45:00Z` input := `bestdayever = 1987-07-05T17:45:00Z`
jsonRef := `{ jsonRef := `{
"bestdayever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"} "bestdayever": {"type": "datetime", "value": "1987-07-05T17:45:00Z"}
@@ -407,12 +452,14 @@ func TestValidDatetime(t *testing.T) {
} }
func TestValidEmpty(t *testing.T) { func TestValidEmpty(t *testing.T) {
input := `` input := ``
jsonRef := `{}` jsonRef := `{}`
testgenValid(t, input, jsonRef) testgenValid(t, input, jsonRef)
} }
func TestValidExample(t *testing.T) { func TestValidExample(t *testing.T) {
input := `best-day-ever = 1987-07-05T17:45:00Z input := `best-day-ever = 1987-07-05T17:45:00Z
[numtheory] [numtheory]
@@ -436,6 +483,7 @@ perfection = [6, 28, 496]`
} }
func TestValidFloat(t *testing.T) { func TestValidFloat(t *testing.T) {
input := `pi = 3.14 input := `pi = 3.14
negpi = -3.14` negpi = -3.14`
jsonRef := `{ jsonRef := `{
@@ -446,6 +494,7 @@ negpi = -3.14`
} }
func TestValidImplicitAndExplicitAfter(t *testing.T) { func TestValidImplicitAndExplicitAfter(t *testing.T) {
input := `[a.b.c] input := `[a.b.c]
answer = 42 answer = 42
@@ -465,6 +514,7 @@ better = 43`
} }
func TestValidImplicitAndExplicitBefore(t *testing.T) { func TestValidImplicitAndExplicitBefore(t *testing.T) {
input := `[a] input := `[a]
better = 43 better = 43
@@ -484,6 +534,7 @@ answer = 42`
} }
func TestValidImplicitGroups(t *testing.T) { func TestValidImplicitGroups(t *testing.T) {
input := `[a.b.c] input := `[a.b.c]
answer = 42` answer = 42`
jsonRef := `{ jsonRef := `{
@@ -499,6 +550,7 @@ answer = 42`
} }
func TestValidInteger(t *testing.T) { func TestValidInteger(t *testing.T) {
input := `answer = 42 input := `answer = 42
neganswer = -42` neganswer = -42`
jsonRef := `{ jsonRef := `{
@@ -509,6 +561,7 @@ neganswer = -42`
} }
func TestValidKeyEqualsNospace(t *testing.T) { func TestValidKeyEqualsNospace(t *testing.T) {
input := `answer=42` input := `answer=42`
jsonRef := `{ jsonRef := `{
"answer": {"type": "integer", "value": "42"} "answer": {"type": "integer", "value": "42"}
@@ -517,6 +570,7 @@ func TestValidKeyEqualsNospace(t *testing.T) {
} }
func TestValidKeySpace(t *testing.T) { func TestValidKeySpace(t *testing.T) {
input := `"a b" = 1` input := `"a b" = 1`
jsonRef := `{ jsonRef := `{
"a b": {"type": "integer", "value": "1"} "a b": {"type": "integer", "value": "1"}
@@ -525,6 +579,7 @@ func TestValidKeySpace(t *testing.T) {
} }
func TestValidKeySpecialChars(t *testing.T) { func TestValidKeySpecialChars(t *testing.T) {
input := "\"~!@$^&*()_+-`1234567890[]|/?><.,;:'\" = 1\n" input := "\"~!@$^&*()_+-`1234567890[]|/?><.,;:'\" = 1\n"
jsonRef := "{\n" + jsonRef := "{\n" +
" \"~!@$^&*()_+-`1234567890[]|/?><.,;:'\": {\n" + " \"~!@$^&*()_+-`1234567890[]|/?><.,;:'\": {\n" +
@@ -535,6 +590,7 @@ func TestValidKeySpecialChars(t *testing.T) {
} }
func TestValidLongFloat(t *testing.T) { func TestValidLongFloat(t *testing.T) {
input := `longpi = 3.141592653589793 input := `longpi = 3.141592653589793
neglongpi = -3.141592653589793` neglongpi = -3.141592653589793`
jsonRef := `{ jsonRef := `{
@@ -545,6 +601,7 @@ neglongpi = -3.141592653589793`
} }
func TestValidLongInteger(t *testing.T) { func TestValidLongInteger(t *testing.T) {
input := `answer = 9223372036854775807 input := `answer = 9223372036854775807
neganswer = -9223372036854775808` neganswer = -9223372036854775808`
jsonRef := `{ jsonRef := `{
@@ -555,6 +612,7 @@ neganswer = -9223372036854775808`
} }
func TestValidMultilineString(t *testing.T) { func TestValidMultilineString(t *testing.T) {
input := `multiline_empty_one = """""" input := `multiline_empty_one = """"""
multiline_empty_two = """ multiline_empty_two = """
""" """
@@ -612,6 +670,7 @@ equivalent_three = """\
} }
func TestValidRawMultilineString(t *testing.T) { func TestValidRawMultilineString(t *testing.T) {
input := `oneline = '''This string has a ' quote character.''' input := `oneline = '''This string has a ' quote character.'''
firstnl = ''' firstnl = '''
This string has a ' quote character.''' This string has a ' quote character.'''
@@ -639,6 +698,7 @@ in it.'''`
} }
func TestValidRawString(t *testing.T) { func TestValidRawString(t *testing.T) {
input := `backspace = 'This string has a \b backspace character.' input := `backspace = 'This string has a \b backspace character.'
tab = 'This string has a \t tab character.' tab = 'This string has a \t tab character.'
newline = 'This string has a \n new line character.' newline = 'This string has a \n new line character.'
@@ -680,6 +740,7 @@ backslash = 'This string has a \\ backslash character.'`
} }
func TestValidStringEmpty(t *testing.T) { func TestValidStringEmpty(t *testing.T) {
input := `answer = ""` input := `answer = ""`
jsonRef := `{ jsonRef := `{
"answer": { "answer": {
@@ -691,6 +752,7 @@ func TestValidStringEmpty(t *testing.T) {
} }
func TestValidStringEscapes(t *testing.T) { func TestValidStringEscapes(t *testing.T) {
input := `backspace = "This string has a \b backspace character." input := `backspace = "This string has a \b backspace character."
tab = "This string has a \t tab character." tab = "This string has a \t tab character."
newline = "This string has a \n new line character." newline = "This string has a \n new line character."
@@ -752,6 +814,7 @@ notunicode4 = "This string does not have a unicode \\\u0075 escape."`
} }
func TestValidStringSimple(t *testing.T) { func TestValidStringSimple(t *testing.T) {
input := `answer = "You are not drinking enough whisky."` input := `answer = "You are not drinking enough whisky."`
jsonRef := `{ jsonRef := `{
"answer": { "answer": {
@@ -763,6 +826,7 @@ func TestValidStringSimple(t *testing.T) {
} }
func TestValidStringWithPound(t *testing.T) { func TestValidStringWithPound(t *testing.T) {
input := `pound = "We see no # comments here." input := `pound = "We see no # comments here."
poundcomment = "But there are # some comments here." # Did I # mess you up?` poundcomment = "But there are # some comments here." # Did I # mess you up?`
jsonRef := `{ jsonRef := `{
@@ -776,6 +840,7 @@ poundcomment = "But there are # some comments here." # Did I # mess you up?`
} }
func TestValidTableArrayImplicit(t *testing.T) { func TestValidTableArrayImplicit(t *testing.T) {
input := `[[albums.songs]] input := `[[albums.songs]]
name = "Glory Days"` name = "Glory Days"`
jsonRef := `{ jsonRef := `{
@@ -789,6 +854,7 @@ name = "Glory Days"`
} }
func TestValidTableArrayMany(t *testing.T) { func TestValidTableArrayMany(t *testing.T) {
input := `[[people]] input := `[[people]]
first_name = "Bruce" first_name = "Bruce"
last_name = "Springsteen" last_name = "Springsteen"
@@ -820,6 +886,7 @@ last_name = "Seger"`
} }
func TestValidTableArrayNest(t *testing.T) { func TestValidTableArrayNest(t *testing.T) {
input := `[[albums]] input := `[[albums]]
name = "Born to Run" name = "Born to Run"
@@ -859,6 +926,7 @@ name = "Born in the USA"
} }
func TestValidTableArrayOne(t *testing.T) { func TestValidTableArrayOne(t *testing.T) {
input := `[[people]] input := `[[people]]
first_name = "Bruce" first_name = "Bruce"
last_name = "Springsteen"` last_name = "Springsteen"`
@@ -874,6 +942,7 @@ last_name = "Springsteen"`
} }
func TestValidTableEmpty(t *testing.T) { func TestValidTableEmpty(t *testing.T) {
input := `[a]` input := `[a]`
jsonRef := `{ jsonRef := `{
"a": {} "a": {}
@@ -882,6 +951,7 @@ func TestValidTableEmpty(t *testing.T) {
} }
func TestValidTableSubEmpty(t *testing.T) { func TestValidTableSubEmpty(t *testing.T) {
input := `[a] input := `[a]
[a.b]` [a.b]`
jsonRef := `{ jsonRef := `{
@@ -891,6 +961,7 @@ func TestValidTableSubEmpty(t *testing.T) {
} }
func TestValidTableWhitespace(t *testing.T) { func TestValidTableWhitespace(t *testing.T) {
input := `["valid key"]` input := `["valid key"]`
jsonRef := `{ jsonRef := `{
"valid key": {} "valid key": {}
@@ -899,6 +970,7 @@ func TestValidTableWhitespace(t *testing.T) {
} }
func TestValidTableWithPound(t *testing.T) { func TestValidTableWithPound(t *testing.T) {
input := `["key#group"] input := `["key#group"]
answer = 42` answer = 42`
jsonRef := `{ jsonRef := `{
@@ -910,6 +982,7 @@ answer = 42`
} }
func TestValidUnicodeEscape(t *testing.T) { func TestValidUnicodeEscape(t *testing.T) {
input := `answer4 = "\u03B4" input := `answer4 = "\u03B4"
answer8 = "\U000003B4"` answer8 = "\U000003B4"`
jsonRef := `{ jsonRef := `{
@@ -920,6 +993,7 @@ answer8 = "\U000003B4"`
} }
func TestValidUnicodeLiteral(t *testing.T) { func TestValidUnicodeLiteral(t *testing.T) {
input := `answer = "δ"` input := `answer = "δ"`
jsonRef := `{ jsonRef := `{
"answer": {"type": "string", "value": "δ"} "answer": {"type": "string", "value": "δ"}
+13
View File
@@ -0,0 +1,13 @@
package toml
import (
"encoding"
"reflect"
"time"
)
var timeType = reflect.TypeOf(time.Time{})
var textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem()
var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
var sliceInterfaceType = reflect.TypeOf([]interface{}{})
+958 -286
View File
File diff suppressed because it is too large Load Diff
+1266 -11
View File
File diff suppressed because it is too large Load Diff